update at 2025-10-08 09:18:20

This commit is contained in:
douboer
2025-10-08 09:18:20 +08:00
parent a49e389fe2
commit 584d4151fc
67 changed files with 5363 additions and 892 deletions

View File

@@ -0,0 +1,464 @@
/**
* 文件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();
}
}