Files
note2any/src/xiaohongshu/login-modal.ts
2025-10-08 09:18:20 +08:00

464 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 文件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();
}
}