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