464 lines
15 KiB
TypeScript
464 lines
15 KiB
TypeScript
/**
|
||
* 文件:login-modal.ts
|
||
* 功能:小红书登录模态窗口(模拟版)。
|
||
*
|
||
* 核心能力:
|
||
* - 手机号输入 / 基础格式校验
|
||
* - 验证码发送(开发模式模拟,测试码:123456 / 000000 / 888888)
|
||
* - 倒计时控制防重复发送
|
||
* - 登录按钮状态联动(依赖:手机号合法 + 已发送验证码 + 已输入验证码)
|
||
* - 登录成功回调(onLoginSuccess)并自动延迟关闭
|
||
* - 状态提示区统一信息展示(info / success / error)
|
||
*
|
||
* 设计说明:
|
||
* - 当前未接入真实短信/登录 API,仅用于流程调试与前端联动;
|
||
* - 后续可对接真实接口:替换 simulateSendCode / simulateLogin;
|
||
* - 可与 XiaohongshuAPIManager.ensureSession() / cookies 持久化策略配合使用;
|
||
* - 若引入真实验证码逻辑,可增加失败重试 / 限频提示 / 安全风控反馈。
|
||
*
|
||
* 后续扩展点:
|
||
* - 支持密码/扫码登录模式切换
|
||
* - 支持登录状态持久化展示(已登录直接提示无需重复登录)
|
||
* - 接入统一日志/埋点系统
|
||
*/
|
||
|
||
import { App, Modal, Setting, Notice, ButtonComponent, TextComponent } from 'obsidian';
|
||
import { XiaohongshuAPIManager } from './api';
|
||
|
||
/**
|
||
* XiaohongshuLoginModal
|
||
*
|
||
* 说明(中文注释):
|
||
* 小红书登录对话框,提供用户登录界面。
|
||
*
|
||
* 主要功能:
|
||
* - 手机号登录(默认13357108011)
|
||
* - 验证码发送和验证
|
||
* - 登录状态检查和反馈
|
||
* - 登录成功后自动关闭对话框
|
||
*
|
||
* 使用方式:
|
||
* - 作为模态对话框弹出
|
||
* - 支持手机验证码登录
|
||
* - 登录成功后执行回调函数
|
||
*/
|
||
export class XiaohongshuLoginModal extends Modal {
|
||
private phoneInput: TextComponent;
|
||
private codeInput: TextComponent;
|
||
private sendCodeButton: ButtonComponent;
|
||
private loginButton: ButtonComponent;
|
||
private statusDiv: HTMLElement;
|
||
|
||
private phone: string = '13357108011'; // 默认手机号
|
||
private verificationCode: string = '';
|
||
private isCodeSent: boolean = false;
|
||
private countdown: number = 0;
|
||
private countdownTimer: NodeJS.Timeout | null = null;
|
||
|
||
private onLoginSuccess?: () => void;
|
||
|
||
constructor(app: App, onLoginSuccess?: () => void) {
|
||
super(app);
|
||
this.onLoginSuccess = onLoginSuccess;
|
||
}
|
||
|
||
onOpen() {
|
||
const { contentEl } = this;
|
||
contentEl.empty();
|
||
contentEl.addClass('xiaohongshu-login-modal');
|
||
|
||
// 设置对话框样式
|
||
contentEl.style.width = '400px';
|
||
contentEl.style.padding = '20px';
|
||
|
||
// 标题
|
||
contentEl.createEl('h2', {
|
||
text: '登录小红书',
|
||
attr: { style: 'text-align: center; margin-bottom: 20px; color: #ff4757;' }
|
||
});
|
||
|
||
// 说明文字
|
||
const descEl = contentEl.createEl('p', {
|
||
text: '请使用手机号码和验证码登录小红书',
|
||
attr: { style: 'text-align: center; color: #666; margin-bottom: 30px;' }
|
||
});
|
||
|
||
// 手机号输入
|
||
new Setting(contentEl)
|
||
.setName('手机号码')
|
||
.setDesc('请输入您的手机号码')
|
||
.addText(text => {
|
||
this.phoneInput = text;
|
||
text.setPlaceholder('请输入手机号码')
|
||
.setValue(this.phone)
|
||
.onChange(value => {
|
||
this.phone = value.trim();
|
||
this.updateSendCodeButtonState();
|
||
});
|
||
|
||
// 设置输入框样式
|
||
text.inputEl.style.width = '100%';
|
||
text.inputEl.style.fontSize = '16px';
|
||
});
|
||
|
||
// 验证码输入和发送按钮
|
||
const codeContainer = contentEl.createDiv({ cls: 'code-container' });
|
||
codeContainer.style.display = 'flex';
|
||
codeContainer.style.alignItems = 'center';
|
||
codeContainer.style.gap = '10px';
|
||
codeContainer.style.marginBottom = '20px';
|
||
|
||
const codeLabel = codeContainer.createDiv({ cls: 'setting-item-name' });
|
||
codeLabel.textContent = '验证码';
|
||
codeLabel.style.minWidth = '80px';
|
||
|
||
const codeInputWrapper = codeContainer.createDiv();
|
||
codeInputWrapper.style.flex = '1';
|
||
|
||
new Setting(codeInputWrapper)
|
||
.addText(text => {
|
||
this.codeInput = text;
|
||
text.setPlaceholder('请输入验证码')
|
||
.setValue('')
|
||
.onChange(value => {
|
||
this.verificationCode = value.trim();
|
||
this.updateLoginButtonState();
|
||
});
|
||
|
||
text.inputEl.style.width = '100%';
|
||
text.inputEl.style.fontSize = '16px';
|
||
text.inputEl.disabled = true; // 初始禁用
|
||
|
||
// 回车键登录
|
||
text.inputEl.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter' && !this.loginButton.buttonEl.disabled) {
|
||
this.handleLogin();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 发送验证码按钮
|
||
this.sendCodeButton = new ButtonComponent(codeContainer)
|
||
.setButtonText('发送验证码')
|
||
.onClick(() => this.handleSendCode());
|
||
|
||
this.sendCodeButton.buttonEl.style.minWidth = '120px';
|
||
this.sendCodeButton.buttonEl.style.marginLeft = '10px';
|
||
|
||
// 状态显示区域
|
||
this.statusDiv = contentEl.createDiv({ cls: 'status-message' });
|
||
this.statusDiv.style.minHeight = '30px';
|
||
this.statusDiv.style.marginBottom = '20px';
|
||
this.statusDiv.style.textAlign = 'center';
|
||
this.statusDiv.style.fontSize = '14px';
|
||
|
||
// 按钮区域
|
||
const buttonContainer = contentEl.createDiv({ cls: 'button-container' });
|
||
buttonContainer.style.display = 'flex';
|
||
buttonContainer.style.justifyContent = 'center';
|
||
buttonContainer.style.gap = '15px';
|
||
buttonContainer.style.marginTop = '20px';
|
||
|
||
// 登录按钮
|
||
this.loginButton = new ButtonComponent(buttonContainer)
|
||
.setButtonText('登录')
|
||
.setCta()
|
||
.setDisabled(true)
|
||
.onClick(() => this.handleLogin());
|
||
|
||
this.loginButton.buttonEl.style.minWidth = '100px';
|
||
|
||
// 取消按钮
|
||
new ButtonComponent(buttonContainer)
|
||
.setButtonText('取消')
|
||
.onClick(() => this.close());
|
||
|
||
// 初始化按钮状态
|
||
this.updateSendCodeButtonState();
|
||
this.updateLoginButtonState();
|
||
|
||
// 检查是否已经登录
|
||
this.checkExistingLogin();
|
||
}
|
||
|
||
/**
|
||
* 检查现有登录状态
|
||
*/
|
||
private async checkExistingLogin() {
|
||
try {
|
||
this.showStatus('正在检查登录状态...', 'info');
|
||
|
||
const api = XiaohongshuAPIManager.getInstance();
|
||
const isLoggedIn = await api.checkLoginStatus();
|
||
|
||
if (isLoggedIn) {
|
||
this.showStatus('已登录小红书!', 'success');
|
||
setTimeout(() => {
|
||
if (this.onLoginSuccess) {
|
||
this.onLoginSuccess();
|
||
}
|
||
this.close();
|
||
}, 1500);
|
||
} else {
|
||
this.showStatus('请登录小红书账号', 'info');
|
||
}
|
||
} catch (error) {
|
||
console.warn('检查登录状态失败:', error);
|
||
this.showStatus('请登录小红书账号', 'info');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送验证码
|
||
*/
|
||
private async handleSendCode() {
|
||
if (!this.phone) {
|
||
this.showStatus('请输入手机号码', 'error');
|
||
return;
|
||
}
|
||
|
||
// 验证手机号格式
|
||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||
if (!phoneRegex.test(this.phone)) {
|
||
this.showStatus('请输入正确的手机号码', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
this.showStatus('正在发送验证码...', 'info');
|
||
this.sendCodeButton.setDisabled(true);
|
||
|
||
// TODO: 实际的验证码发送逻辑
|
||
// 这里模拟发送验证码的过程
|
||
await this.simulateSendCode();
|
||
|
||
this.isCodeSent = true;
|
||
this.codeInput.inputEl.disabled = false;
|
||
this.codeInput.inputEl.focus();
|
||
|
||
this.showStatus('验证码已发送 [开发模式: 请使用 123456]', 'success');
|
||
this.startCountdown();
|
||
|
||
} catch (error) {
|
||
this.showStatus('发送验证码失败: ' + error.message, 'error');
|
||
this.sendCodeButton.setDisabled(false);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 模拟发送验证码(实际项目中需要接入真实的验证码服务)
|
||
*/
|
||
private async simulateSendCode(): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
// 模拟网络请求延迟
|
||
setTimeout(() => {
|
||
// 这里应该调用实际的小红书验证码API
|
||
// 目前作为演示,总是成功
|
||
console.log(`[模拟] 向 ${this.phone} 发送验证码`);
|
||
console.log(`[开发模式] 请使用测试验证码: 123456`);
|
||
resolve();
|
||
}, 1000);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 开始倒计时
|
||
*/
|
||
private startCountdown() {
|
||
this.countdown = 60;
|
||
this.updateSendCodeButton();
|
||
|
||
this.countdownTimer = setInterval(() => {
|
||
this.countdown--;
|
||
this.updateSendCodeButton();
|
||
|
||
if (this.countdown <= 0) {
|
||
this.stopCountdown();
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
/**
|
||
* 停止倒计时
|
||
*/
|
||
private stopCountdown() {
|
||
if (this.countdownTimer) {
|
||
clearInterval(this.countdownTimer);
|
||
this.countdownTimer = null;
|
||
}
|
||
this.countdown = 0;
|
||
this.updateSendCodeButton();
|
||
}
|
||
|
||
/**
|
||
* 更新发送验证码按钮状态
|
||
*/
|
||
private updateSendCodeButton() {
|
||
if (this.countdown > 0) {
|
||
this.sendCodeButton.setButtonText(`重新发送(${this.countdown}s)`);
|
||
this.sendCodeButton.setDisabled(true);
|
||
} else {
|
||
this.sendCodeButton.setButtonText(this.isCodeSent ? '重新发送' : '发送验证码');
|
||
this.sendCodeButton.setDisabled(!this.phone);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新发送验证码按钮状态
|
||
*/
|
||
private updateSendCodeButtonState() {
|
||
if (this.countdown <= 0) {
|
||
this.sendCodeButton.setDisabled(!this.phone);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新登录按钮状态
|
||
*/
|
||
private updateLoginButtonState() {
|
||
const canLogin = this.phone && this.verificationCode && this.isCodeSent;
|
||
this.loginButton.setDisabled(!canLogin);
|
||
}
|
||
|
||
/**
|
||
* 处理登录
|
||
*/
|
||
private async handleLogin() {
|
||
if (!this.phone || !this.verificationCode) {
|
||
this.showStatus('请填写完整信息', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
this.showStatus('正在登录...', 'info');
|
||
this.loginButton.setDisabled(true);
|
||
|
||
// 获取小红书API实例
|
||
const api = XiaohongshuAPIManager.getInstance();
|
||
|
||
// TODO: 实际登录逻辑
|
||
// 这里应该调用小红书的验证码登录接口
|
||
const loginSuccess = await this.simulateLogin();
|
||
|
||
if (loginSuccess) {
|
||
this.showStatus('登录成功!', 'success');
|
||
|
||
// 延迟关闭对话框,让用户看到成功信息
|
||
setTimeout(() => {
|
||
if (this.onLoginSuccess) {
|
||
this.onLoginSuccess();
|
||
}
|
||
this.close();
|
||
}, 1500);
|
||
|
||
} else {
|
||
this.showStatus('登录失败,请检查验证码', 'error');
|
||
this.loginButton.setDisabled(false);
|
||
}
|
||
|
||
} catch (error) {
|
||
this.showStatus('登录失败: ' + error.message, 'error');
|
||
this.loginButton.setDisabled(false);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 模拟登录过程(实际项目中需要接入真实的登录API)
|
||
*/
|
||
private async simulateLogin(): Promise<boolean> {
|
||
return new Promise((resolve) => {
|
||
// 模拟网络请求延迟
|
||
setTimeout(() => {
|
||
// 模拟验证码验证
|
||
// 在真实环境中,这里应该调用小红书的登录API
|
||
console.log(`[模拟] 使用手机号 ${this.phone} 和验证码 ${this.verificationCode} 登录`);
|
||
|
||
// 简单的验证码验证(演示用)
|
||
// 实际项目中应该由服务器验证
|
||
const validCodes = ['123456', '000000', '888888'];
|
||
const success = validCodes.includes(this.verificationCode);
|
||
|
||
resolve(success);
|
||
}, 1500);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 显示状态信息
|
||
*/
|
||
private showStatus(message: string, type: 'info' | 'success' | 'error' = 'info') {
|
||
this.statusDiv.empty();
|
||
|
||
const messageEl = this.statusDiv.createSpan({ text: message });
|
||
|
||
// 设置不同类型的样式
|
||
switch (type) {
|
||
case 'success':
|
||
messageEl.style.color = '#27ae60';
|
||
break;
|
||
case 'error':
|
||
messageEl.style.color = '#e74c3c';
|
||
break;
|
||
case 'info':
|
||
default:
|
||
messageEl.style.color = '#3498db';
|
||
break;
|
||
}
|
||
}
|
||
|
||
onClose() {
|
||
// 清理倒计时定时器
|
||
this.stopCountdown();
|
||
|
||
const { contentEl } = this;
|
||
contentEl.empty();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 小红书登录管理器
|
||
*
|
||
* 提供便捷的登录状态检查和登录对话框调用
|
||
*/
|
||
export class XiaohongshuLoginManager {
|
||
|
||
/**
|
||
* 检查登录状态,如果未登录则弹出登录对话框
|
||
*/
|
||
static async ensureLogin(app: App): Promise<boolean> {
|
||
const api = XiaohongshuAPIManager.getInstance();
|
||
|
||
try {
|
||
const isLoggedIn = await api.checkLoginStatus();
|
||
if (isLoggedIn) {
|
||
return true;
|
||
}
|
||
} catch (error) {
|
||
console.warn('检查小红书登录状态失败:', error);
|
||
}
|
||
|
||
// 未登录,弹出登录对话框
|
||
return new Promise((resolve) => {
|
||
const loginModal = new XiaohongshuLoginModal(app, () => {
|
||
resolve(true);
|
||
});
|
||
|
||
loginModal.open();
|
||
|
||
// 如果用户取消登录,返回false
|
||
const originalClose = loginModal.close.bind(loginModal);
|
||
loginModal.close = () => {
|
||
resolve(false);
|
||
originalClose();
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 强制弹出登录对话框
|
||
*/
|
||
static showLoginModal(app: App, onSuccess?: () => void) {
|
||
const modal = new XiaohongshuLoginModal(app, onSuccess);
|
||
modal.open();
|
||
}
|
||
} |