748 lines
27 KiB
TypeScript
748 lines
27 KiB
TypeScript
/**
|
||
* 文件:batch-publish-modal.ts
|
||
* 功能:批量发布模态窗口;支持文件夹 / 多文件选择 + 多平台勾选。
|
||
* - 文件列表与过滤
|
||
* - 平台选择(公众号 / 小红书)
|
||
* - 批量触发发布逻辑
|
||
*/
|
||
|
||
import { App, Modal, Setting, TFile, Notice, ButtonComponent } from 'obsidian';
|
||
import { BatchArticleFilter, BatchFilterConfig } from './batch-filter';
|
||
import NoteToMpPlugin from './main';
|
||
// 小红书功能模块
|
||
import { XiaohongshuContentAdapter } from './xiaohongshu/adapter';
|
||
import { XiaohongshuAPIManager } from './xiaohongshu/api';
|
||
|
||
/**
|
||
* BatchPublishModal
|
||
*
|
||
* 说明(中文注释):
|
||
* 该模块负责提供一个在 Obsidian 中的模态窗口,用于:
|
||
* - 根据多个条件筛选笔记(标签、文件名、文件夹、frontmatter 等)
|
||
* - 在筛选结果中支持单选/多选/框选(鼠标拖拽)操作
|
||
* - 将选中的文章逐条发送到公众号(通过 NotePreview 的 renderMarkdown/postArticle 流程)
|
||
*
|
||
* 设计要点:
|
||
* - 使用 `BatchArticleFilter` 复用筛选逻辑,返回已排序的 `TFile[]` 列表
|
||
* - 用 `Set<TFile>` 保存选中状态,避免重复项并方便计数
|
||
* - 鼠标拖拽选择支持两种模式:常规拖拽为“添加选择”,按 Ctrl/Cmd 拖拽为“取消选择”
|
||
* - 发布流程为顺序异步执行,发送间隔有短延迟以降低请求频率
|
||
*
|
||
* 注意事项:
|
||
* - 该文件以用户交互为主,敏感的网络/身份验证交互在 `note-preview` 模块内处理
|
||
* - 对 DOM 的位置计算要考虑容器滚动偏移(scrollLeft/scrollTop),已在代码中处理
|
||
*/
|
||
|
||
export class BatchPublishModal extends Modal {
|
||
plugin: NoteToMpPlugin;
|
||
filter: BatchArticleFilter;
|
||
filteredFiles: TFile[] = [];
|
||
selectedFiles: Set<TFile> = new Set();
|
||
|
||
// UI 元素
|
||
private filterContainer: HTMLElement;
|
||
private resultsContainer: HTMLElement;
|
||
private publishButton: ButtonComponent;
|
||
|
||
// 平台选择相关(新增)
|
||
private wechatCheckbox: HTMLInputElement;
|
||
private xiaohongshuCheckbox: HTMLInputElement;
|
||
private allPlatformsCheckbox: HTMLInputElement;
|
||
|
||
// 鼠标框选相关
|
||
private isSelecting = false;
|
||
private selectionStart: { x: number; y: number } | null = null;
|
||
private selectionBox: HTMLElement | null = null;
|
||
private isCtrlPressed = false; // 跟踪 Ctrl 键状态
|
||
|
||
// 筛选配置
|
||
private filterConfig: BatchFilterConfig = {
|
||
conditions: [
|
||
{
|
||
type: 'folder',
|
||
operator: 'contains',
|
||
value: 'content/post'
|
||
}
|
||
],
|
||
logic: 'and',
|
||
orderBy: 'name',
|
||
orderDirection: 'asc'
|
||
};
|
||
|
||
constructor(app: App, plugin: NoteToMpPlugin) {
|
||
super(app);
|
||
this.plugin = plugin;
|
||
this.filter = new BatchArticleFilter(app);
|
||
}
|
||
|
||
onOpen() {
|
||
const { contentEl } = this;
|
||
contentEl.empty();
|
||
contentEl.addClass('batch-publish-modal');
|
||
|
||
// 设置模态框的整体布局
|
||
contentEl.style.display = 'flex';
|
||
contentEl.style.flexDirection = 'column';
|
||
contentEl.style.height = '80vh';
|
||
contentEl.style.maxHeight = '600px';
|
||
|
||
// 标题
|
||
contentEl.createEl('h2', { text: '批量发布到公众号' });
|
||
|
||
// 筛选条件区域
|
||
this.filterContainer = contentEl.createDiv('filter-container');
|
||
this.createFilterUI();
|
||
|
||
// 结果展示区域(可滚动)
|
||
this.resultsContainer = contentEl.createDiv('results-container');
|
||
this.resultsContainer.style.flex = '1';
|
||
this.resultsContainer.style.overflow = 'hidden';
|
||
this.resultsContainer.style.display = 'flex';
|
||
this.resultsContainer.style.flexDirection = 'column';
|
||
|
||
// 操作按钮(固定在底部)
|
||
const buttonContainer = contentEl.createDiv('button-container');
|
||
buttonContainer.style.marginTop = '20px';
|
||
buttonContainer.style.textAlign = 'center';
|
||
buttonContainer.style.paddingTop = '15px';
|
||
buttonContainer.style.borderTop = '1px solid var(--background-modifier-border)';
|
||
buttonContainer.style.flexShrink = '0';
|
||
|
||
// 发布平台选择(新增)
|
||
const platformContainer = buttonContainer.createDiv('platform-select-container');
|
||
platformContainer.style.marginBottom = '15px';
|
||
platformContainer.style.display = 'flex';
|
||
platformContainer.style.alignItems = 'center';
|
||
platformContainer.style.justifyContent = 'center';
|
||
platformContainer.style.gap = '10px';
|
||
|
||
const platformLabel = platformContainer.createSpan();
|
||
platformLabel.innerText = '发布到: ';
|
||
|
||
const wechatCheckbox = platformContainer.createEl('input', { type: 'checkbox' });
|
||
wechatCheckbox.id = 'publish-wechat';
|
||
wechatCheckbox.checked = true;
|
||
this.wechatCheckbox = wechatCheckbox;
|
||
const wechatLabel = platformContainer.createEl('label');
|
||
wechatLabel.setAttribute('for', 'publish-wechat');
|
||
wechatLabel.innerText = '微信公众号';
|
||
wechatLabel.style.marginRight = '15px';
|
||
|
||
const xiaohongshuCheckbox = platformContainer.createEl('input', { type: 'checkbox' });
|
||
xiaohongshuCheckbox.id = 'publish-xiaohongshu';
|
||
this.xiaohongshuCheckbox = xiaohongshuCheckbox;
|
||
const xiaohongshuLabel = platformContainer.createEl('label');
|
||
xiaohongshuLabel.setAttribute('for', 'publish-xiaohongshu');
|
||
xiaohongshuLabel.innerText = '小红书';
|
||
xiaohongshuLabel.style.marginRight = '15px';
|
||
|
||
const allPlatformsCheckbox = platformContainer.createEl('input', { type: 'checkbox' });
|
||
allPlatformsCheckbox.id = 'publish-all';
|
||
this.allPlatformsCheckbox = allPlatformsCheckbox;
|
||
const allPlatformsLabel = platformContainer.createEl('label');
|
||
allPlatformsLabel.setAttribute('for', 'publish-all');
|
||
allPlatformsLabel.innerText = '全部平台';
|
||
|
||
// 全部平台checkbox的联动逻辑
|
||
allPlatformsCheckbox.addEventListener('change', () => {
|
||
if (allPlatformsCheckbox.checked) {
|
||
wechatCheckbox.checked = true;
|
||
xiaohongshuCheckbox.checked = true;
|
||
}
|
||
});
|
||
|
||
// 单个平台checkbox的联动逻辑
|
||
const updateAllPlatforms = () => {
|
||
if (wechatCheckbox.checked && xiaohongshuCheckbox.checked) {
|
||
allPlatformsCheckbox.checked = true;
|
||
} else {
|
||
allPlatformsCheckbox.checked = false;
|
||
}
|
||
};
|
||
|
||
wechatCheckbox.addEventListener('change', updateAllPlatforms);
|
||
xiaohongshuCheckbox.addEventListener('change', updateAllPlatforms);
|
||
|
||
new ButtonComponent(buttonContainer)
|
||
.setButtonText('应用筛选')
|
||
.setCta()
|
||
.onClick(() => this.applyFilter());
|
||
|
||
this.publishButton = new ButtonComponent(buttonContainer)
|
||
.setButtonText('发布选中文章 (0)')
|
||
.setDisabled(true)
|
||
.onClick(() => this.publishSelected());
|
||
|
||
new ButtonComponent(buttonContainer)
|
||
.setButtonText('取消')
|
||
.onClick(() => this.close());
|
||
|
||
// 初始加载所有文章
|
||
this.applyFilter();
|
||
}
|
||
|
||
/**
|
||
* 创建筛选条件界面
|
||
*/
|
||
private createFilterUI() {
|
||
this.filterContainer.empty();
|
||
|
||
// 标签筛选
|
||
new Setting(this.filterContainer)
|
||
.setName('按标签筛选')
|
||
.setDesc('输入要筛选的标签名称')
|
||
.addText(text => {
|
||
const tagCondition = this.filterConfig.conditions.find(c => c.type === 'tag');
|
||
text.setPlaceholder('如: 篆刻')
|
||
.setValue(tagCondition?.value || '')
|
||
.onChange(value => {
|
||
this.updateTagCondition(value);
|
||
});
|
||
|
||
// 添加回车键监听
|
||
text.inputEl.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
this.applyFilter();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 文件名筛选
|
||
new Setting(this.filterContainer)
|
||
.setName('按文件名筛选')
|
||
.setDesc('输入文件名关键词')
|
||
.addText(text => {
|
||
const nameCondition = this.filterConfig.conditions.find(c => c.type === 'filename');
|
||
text.setPlaceholder('如: 故事')
|
||
.setValue(nameCondition?.value || '')
|
||
.onChange(value => {
|
||
this.updateFilenameCondition(value);
|
||
});
|
||
|
||
// 添加回车键监听
|
||
text.inputEl.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
this.applyFilter();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 文件夹筛选
|
||
new Setting(this.filterContainer)
|
||
.setName('按文件夹筛选')
|
||
.setDesc('输入文件夹路径')
|
||
.addText(text => {
|
||
const folderCondition = this.filterConfig.conditions.find(c => c.type === 'folder');
|
||
text.setPlaceholder('如: content/post')
|
||
.setValue(folderCondition?.value || 'content/post')
|
||
.onChange(value => {
|
||
this.updateFolderCondition(value);
|
||
});
|
||
|
||
// 添加回车键监听
|
||
text.inputEl.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
this.applyFilter();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 排序选项
|
||
new Setting(this.filterContainer)
|
||
.setName('排序方式')
|
||
.setDesc('选择文章排序方式')
|
||
.addDropdown(dropdown => {
|
||
dropdown.addOption('name', '按文件名')
|
||
.addOption('created', '按创建时间')
|
||
.addOption('modified', '按修改时间')
|
||
.setValue(this.filterConfig.orderBy || 'name')
|
||
.onChange(value => {
|
||
this.filterConfig.orderBy = value as any;
|
||
});
|
||
})
|
||
.addDropdown(dropdown => {
|
||
dropdown.addOption('asc', '升序')
|
||
.addOption('desc', '降序')
|
||
.setValue(this.filterConfig.orderDirection || 'asc')
|
||
.onChange(value => {
|
||
this.filterConfig.orderDirection = value as any;
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新标签条件
|
||
*/
|
||
private updateTagCondition(value: string) {
|
||
// 移除现有的标签条件
|
||
this.filterConfig.conditions = this.filterConfig.conditions.filter(c => c.type !== 'tag');
|
||
|
||
if (value.trim()) {
|
||
this.filterConfig.conditions.push({
|
||
type: 'tag',
|
||
operator: 'contains',
|
||
value: value.trim()
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新文件名条件
|
||
*/
|
||
private updateFilenameCondition(value: string) {
|
||
this.filterConfig.conditions = this.filterConfig.conditions.filter(c => c.type !== 'filename');
|
||
|
||
if (value.trim()) {
|
||
this.filterConfig.conditions.push({
|
||
type: 'filename',
|
||
operator: 'contains',
|
||
value: value.trim()
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新文件夹条件
|
||
*/
|
||
private updateFolderCondition(value: string) {
|
||
this.filterConfig.conditions = this.filterConfig.conditions.filter(c => c.type !== 'folder');
|
||
|
||
if (value.trim()) {
|
||
this.filterConfig.conditions.push({
|
||
type: 'folder',
|
||
operator: 'contains',
|
||
value: value.trim()
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 应用筛选条件
|
||
*/
|
||
private async applyFilter() {
|
||
try {
|
||
this.filteredFiles = await this.filter.filterArticles(this.filterConfig);
|
||
this.selectedFiles.clear();
|
||
this.displayResults();
|
||
this.updatePublishButton();
|
||
} catch (error) {
|
||
new Notice('筛选文章时出错: ' + error.message);
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示筛选结果
|
||
*/
|
||
private displayResults() {
|
||
this.resultsContainer.empty();
|
||
|
||
if (this.filteredFiles.length === 0) {
|
||
this.resultsContainer.createEl('p', {
|
||
text: '未找到匹配的文章',
|
||
cls: 'no-results'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 统计信息
|
||
const statsEl = this.resultsContainer.createDiv('results-stats');
|
||
statsEl.textContent = `找到 ${this.filteredFiles.length} 篇文章`;
|
||
statsEl.style.flexShrink = '0';
|
||
|
||
// 全选/取消全选
|
||
const selectAllContainer = this.resultsContainer.createDiv('select-all-container');
|
||
selectAllContainer.style.flexShrink = '0';
|
||
const selectAllCheckbox = selectAllContainer.createEl('input', {
|
||
type: 'checkbox'
|
||
});
|
||
selectAllContainer.createSpan({ text: ' 全选/取消全选' });
|
||
|
||
selectAllCheckbox.addEventListener('change', () => {
|
||
if (selectAllCheckbox.checked) {
|
||
this.filteredFiles.forEach(file => this.selectedFiles.add(file));
|
||
} else {
|
||
this.selectedFiles.clear();
|
||
}
|
||
this.updateCheckboxes();
|
||
this.updatePublishButton();
|
||
});
|
||
|
||
// 文章列表(可滚动区域)
|
||
const listContainer = this.resultsContainer.createDiv('articles-list');
|
||
listContainer.style.flex = '1';
|
||
listContainer.style.overflowY = 'auto';
|
||
listContainer.style.border = '1px solid var(--background-modifier-border)';
|
||
listContainer.style.borderRadius = '4px';
|
||
listContainer.style.padding = '10px';
|
||
listContainer.style.position = 'relative';
|
||
listContainer.style.userSelect = 'none'; // 禁用文本选择
|
||
|
||
// 添加鼠标框选功能
|
||
this.setupMouseSelection(listContainer);
|
||
|
||
this.filteredFiles.forEach(file => {
|
||
const itemEl = listContainer.createDiv('article-item');
|
||
itemEl.style.display = 'flex';
|
||
itemEl.style.alignItems = 'center';
|
||
itemEl.style.padding = '5px 0';
|
||
itemEl.style.borderBottom = '1px solid var(--background-modifier-border-hover)';
|
||
itemEl.style.cursor = 'pointer';
|
||
itemEl.setAttribute('data-file-path', file.path);
|
||
|
||
const checkbox = itemEl.createEl('input', {
|
||
type: 'checkbox'
|
||
});
|
||
checkbox.style.marginRight = '10px';
|
||
|
||
const titleEl = itemEl.createSpan({ text: file.basename });
|
||
titleEl.style.flex = '1';
|
||
|
||
const pathEl = itemEl.createEl('small', { text: file.path });
|
||
pathEl.style.color = 'var(--text-muted)';
|
||
pathEl.style.marginLeft = '10px';
|
||
|
||
checkbox.addEventListener('change', () => {
|
||
if (checkbox.checked) {
|
||
this.selectedFiles.add(file);
|
||
} else {
|
||
this.selectedFiles.delete(file);
|
||
}
|
||
this.updatePublishButton();
|
||
});
|
||
|
||
// 点击整行也能选择
|
||
itemEl.addEventListener('click', (e) => {
|
||
if (e.target !== checkbox) {
|
||
checkbox.checked = !checkbox.checked;
|
||
checkbox.dispatchEvent(new Event('change'));
|
||
}
|
||
});
|
||
|
||
// 存储引用以便后续更新
|
||
(checkbox as any)._file = file;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新所有复选框状态
|
||
*/
|
||
private updateCheckboxes() {
|
||
const checkboxes = this.resultsContainer.querySelectorAll('.articles-list input[type="checkbox"]');
|
||
checkboxes.forEach(checkbox => {
|
||
const file = (checkbox as any)._file;
|
||
if (file) {
|
||
(checkbox as HTMLInputElement).checked = this.selectedFiles.has(file);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新发布按钮
|
||
*/
|
||
private updatePublishButton() {
|
||
const count = this.selectedFiles.size;
|
||
this.publishButton.setButtonText(`发布选中文章 (${count})`);
|
||
this.publishButton.setDisabled(count === 0);
|
||
}
|
||
|
||
/**
|
||
* 发布选中的文章
|
||
*/
|
||
private async publishSelected() {
|
||
if (this.selectedFiles.size === 0) {
|
||
new Notice('请选择要发布的文章');
|
||
return;
|
||
}
|
||
|
||
// 获取选择的发布平台
|
||
const platforms = this.getSelectedPlatforms();
|
||
if (platforms.length === 0) {
|
||
new Notice('请选择至少一个发布平台');
|
||
return;
|
||
}
|
||
|
||
const files = Array.from(this.selectedFiles);
|
||
const totalTasks = files.length * platforms.length;
|
||
let completed = 0;
|
||
let failed = 0;
|
||
|
||
// 显示进度
|
||
const notice = new Notice(`开始批量发布 ${files.length} 篇文章到 ${platforms.join('、')}...`, 0);
|
||
|
||
try {
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
|
||
for (const platform of platforms) {
|
||
try {
|
||
// 更新进度
|
||
const taskIndex = i * platforms.length + platforms.indexOf(platform) + 1;
|
||
notice.setMessage(`正在发布: ${file.basename} 到 ${platform} (${taskIndex}/${totalTasks})`);
|
||
|
||
if (platform === '微信公众号') {
|
||
await this.publishToWechat(file);
|
||
} else if (platform === '小红书') {
|
||
await this.publishToXiaohongshu(file);
|
||
}
|
||
|
||
completed++;
|
||
|
||
} catch (error) {
|
||
console.error(`发布文章 ${file.basename} 到 ${platform} 失败:`, error);
|
||
failed++;
|
||
}
|
||
|
||
// 避免请求过于频繁
|
||
const taskIndex = i * platforms.length + platforms.indexOf(platform) + 1;
|
||
if (taskIndex < totalTasks) {
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 显示最终结果
|
||
notice.hide();
|
||
new Notice(`批量发布完成!成功: ${completed} 个任务,失败: ${failed} 个任务`);
|
||
|
||
} catch (error) {
|
||
notice.hide();
|
||
new Notice('批量发布过程中发生错误: ' + error.message);
|
||
console.error('批量发布错误:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取选择的发布平台
|
||
*/
|
||
private getSelectedPlatforms(): string[] {
|
||
const platforms: string[] = [];
|
||
|
||
if (this.wechatCheckbox.checked) {
|
||
platforms.push('微信公众号');
|
||
}
|
||
|
||
if (this.xiaohongshuCheckbox.checked) {
|
||
platforms.push('小红书');
|
||
}
|
||
|
||
return platforms;
|
||
}
|
||
|
||
/**
|
||
* 发布到微信公众号
|
||
*/
|
||
private async publishToWechat(file: TFile): Promise<void> {
|
||
// TODO: 重构后需要重新实现批量发布到微信
|
||
// 激活预览视图并发布
|
||
await this.plugin.activateView();
|
||
const preview = this.plugin.getNotePreview();
|
||
if (preview) {
|
||
// 临时方案:直接打开文件让用户手动发布
|
||
await preview.setFile(file);
|
||
throw new Error('批量发布功能正在重构中,请在预览视图中手动发布');
|
||
} else {
|
||
throw new Error('无法获取预览视图');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发布到小红书
|
||
*/
|
||
private async publishToXiaohongshu(file: TFile): Promise<void> {
|
||
try {
|
||
// 读取文件内容
|
||
const fileContent = await this.app.vault.read(file);
|
||
|
||
// 使用小红书适配器转换内容
|
||
const adapter = new XiaohongshuContentAdapter();
|
||
const xiaohongshuPost = adapter.adaptMarkdownToXiaohongshu(fileContent, {
|
||
addStyle: true,
|
||
generateTitle: true
|
||
});
|
||
|
||
// 验证内容
|
||
const validation = adapter.validatePost(xiaohongshuPost);
|
||
if (!validation.valid) {
|
||
throw new Error('内容验证失败: ' + validation.errors.join('; '));
|
||
}
|
||
|
||
// 获取小红书API实例
|
||
const api = XiaohongshuAPIManager.getInstance(false);
|
||
|
||
// 检查登录状态
|
||
const isLoggedIn = await api.checkLoginStatus();
|
||
if (!isLoggedIn) {
|
||
throw new Error('小红书未登录,请在预览界面登录后再试');
|
||
}
|
||
|
||
// 发布内容
|
||
const result = await api.createPost(xiaohongshuPost);
|
||
|
||
if (!result.success) {
|
||
throw new Error(result.message);
|
||
}
|
||
} catch (error) {
|
||
throw new Error(`发布到小红书失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
onClose() {
|
||
const { contentEl } = this;
|
||
contentEl.empty();
|
||
|
||
// 清理鼠标框选相关的事件监听器
|
||
this.cleanupMouseSelection();
|
||
}
|
||
|
||
/**
|
||
* 设置鼠标框选功能
|
||
*/
|
||
private setupMouseSelection(container: HTMLElement) {
|
||
container.addEventListener('mousedown', (e) => {
|
||
if (e.button !== 0) return; // 只响应左键
|
||
if (e.target !== container && !(e.target as HTMLElement).closest('.article-item')) {
|
||
return;
|
||
}
|
||
|
||
this.isSelecting = true;
|
||
this.isCtrlPressed = e.ctrlKey || e.metaKey; // 检测 Ctrl 键(Mac 上是 Cmd 键)
|
||
|
||
const containerRect = container.getBoundingClientRect();
|
||
this.selectionStart = {
|
||
x: e.clientX - containerRect.left + container.scrollLeft,
|
||
y: e.clientY - containerRect.top + container.scrollTop
|
||
};
|
||
|
||
// 创建选择框
|
||
this.selectionBox = document.createElement('div');
|
||
this.selectionBox.style.position = 'absolute';
|
||
this.selectionBox.style.border = '1px dashed var(--interactive-accent)';
|
||
this.selectionBox.style.backgroundColor = 'var(--interactive-accent-hover)';
|
||
this.selectionBox.style.opacity = '0.3';
|
||
this.selectionBox.style.zIndex = '1000';
|
||
this.selectionBox.style.pointerEvents = 'none';
|
||
|
||
// Ctrl 键时使用不同的视觉样式表示取消选中模式
|
||
if (this.isCtrlPressed) {
|
||
this.selectionBox.style.border = '1px dashed var(--text-error)';
|
||
this.selectionBox.style.backgroundColor = 'var(--background-modifier-error-hover)';
|
||
}
|
||
|
||
container.appendChild(this.selectionBox);
|
||
|
||
e.preventDefault();
|
||
});
|
||
|
||
container.addEventListener('mousemove', (e) => {
|
||
if (!this.isSelecting || !this.selectionStart || !this.selectionBox) return;
|
||
|
||
const containerRect = container.getBoundingClientRect();
|
||
const currentX = e.clientX - containerRect.left + container.scrollLeft;
|
||
const currentY = e.clientY - containerRect.top + container.scrollTop;
|
||
|
||
const left = Math.min(this.selectionStart.x, currentX);
|
||
const top = Math.min(this.selectionStart.y, currentY);
|
||
const width = Math.abs(currentX - this.selectionStart.x);
|
||
const height = Math.abs(currentY - this.selectionStart.y);
|
||
|
||
this.selectionBox.style.left = left + 'px';
|
||
this.selectionBox.style.top = top + 'px';
|
||
this.selectionBox.style.width = width + 'px';
|
||
this.selectionBox.style.height = height + 'px';
|
||
|
||
// 检测哪些文件项在选择框内
|
||
this.updateSelectionByBox(container, left, top, width, height);
|
||
});
|
||
|
||
container.addEventListener('mouseup', () => {
|
||
if (this.isSelecting) {
|
||
this.isSelecting = false;
|
||
this.selectionStart = null;
|
||
this.isCtrlPressed = false;
|
||
|
||
if (this.selectionBox) {
|
||
this.selectionBox.remove();
|
||
this.selectionBox = null;
|
||
}
|
||
|
||
this.updatePublishButton();
|
||
}
|
||
});
|
||
|
||
// 防止拖拽离开容器时无法结束选择
|
||
document.addEventListener('mouseup', () => {
|
||
if (this.isSelecting) {
|
||
this.isSelecting = false;
|
||
this.selectionStart = null;
|
||
this.isCtrlPressed = false;
|
||
|
||
if (this.selectionBox) {
|
||
this.selectionBox.remove();
|
||
this.selectionBox = null;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 根据选择框更新文件选择状态
|
||
*/
|
||
private updateSelectionByBox(container: HTMLElement, boxLeft: number, boxTop: number, boxWidth: number, boxHeight: number) {
|
||
const items = container.querySelectorAll('.article-item');
|
||
|
||
items.forEach(item => {
|
||
const itemEl = item as HTMLElement;
|
||
|
||
// 获取元素相对于容器的位置(考虑滚动)
|
||
let itemTop = 0;
|
||
let currentEl = itemEl;
|
||
while (currentEl && currentEl !== container) {
|
||
itemTop += currentEl.offsetTop;
|
||
currentEl = currentEl.offsetParent as HTMLElement;
|
||
}
|
||
|
||
const itemLeft = itemEl.offsetLeft;
|
||
const itemRight = itemLeft + itemEl.offsetWidth;
|
||
const itemBottom = itemTop + itemEl.offsetHeight;
|
||
|
||
const boxRight = boxLeft + boxWidth;
|
||
const boxBottom = boxTop + boxHeight;
|
||
|
||
// 检测是否有重叠
|
||
const isIntersecting = !(itemRight < boxLeft ||
|
||
itemLeft > boxRight ||
|
||
itemBottom < boxTop ||
|
||
itemTop > boxBottom);
|
||
|
||
if (isIntersecting) {
|
||
const checkbox = itemEl.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||
const file = (checkbox as any)._file as TFile;
|
||
|
||
if (checkbox && file) {
|
||
if (this.isCtrlPressed) {
|
||
// Ctrl+框选:取消选中
|
||
checkbox.checked = false;
|
||
this.selectedFiles.delete(file);
|
||
} else {
|
||
// 普通框选:选中
|
||
checkbox.checked = true;
|
||
this.selectedFiles.add(file);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 清理鼠标框选相关资源
|
||
*/
|
||
private cleanupMouseSelection() {
|
||
if (this.selectionBox) {
|
||
this.selectionBox.remove();
|
||
this.selectionBox = null;
|
||
}
|
||
this.isSelecting = false;
|
||
this.selectionStart = null;
|
||
}
|
||
} |