update at 2025-10-21 21:47:02

This commit is contained in:
douboer
2025-10-21 21:47:02 +08:00
parent 8d40fbb01f
commit b823d90b55
29 changed files with 2159 additions and 1626 deletions

View File

@@ -38,7 +38,7 @@ export class XiaohongshuPreview {
fontSizeInput!: HTMLInputElement;
previewWidthSelect!: HTMLSelectElement;
themeSelect!: HTMLSelectElement;
highlightSelect!: HTMLSelectElement;
platformSelect: HTMLSelectElement | null = null;
pageContainer!: HTMLDivElement;
pageNumberInput!: HTMLInputElement;
@@ -69,7 +69,8 @@ export class XiaohongshuPreview {
*/
build(): void {
this.container.empty();
this.container.addClass('xhs-preview-container');
this.container.addClass('note2any-platform-container');
// 准备样式挂载节点
if (!this.styleEl) {
this.styleEl = document.createElement('style');
@@ -79,152 +80,299 @@ export class XiaohongshuPreview {
this.container.appendChild(this.styleEl);
}
const board = this.container.createDiv({ cls: 'xhs-board' });
const templateCard = this.createGridCard(board, 'xhs-area-template');
const templateLabel = templateCard.createDiv({ cls: 'xhs-label', text: '模板' });
this.templateSelect = templateCard.createEl('select', { cls: 'xhs-select' });
['默认模板', '简约模板', '杂志模板'].forEach(name => {
const option = this.templateSelect.createEl('option');
option.value = name;
option.text = name;
// 顶部平台选择器栏
const header = this.container.createDiv({ cls: 'note2any-platform-header' });
// 平台选择器区域
const platformSelector = header.createDiv({ cls: 'note2any-platform-selector' });
const platformLabel = platformSelector.createDiv({
cls: 'note2any-platform-label',
text: '发布平台'
});
const previewCard = this.createGridCard(board, 'xhs-area-preview');
const previewLabel = previewCard.createDiv({ cls: 'xhs-label', text: '宽度' });
this.previewWidthSelect = previewCard.createEl('select', { cls: 'xhs-select' });
const currentPreviewWidth = this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH;
XHS_PREVIEW_WIDTH_OPTIONS.forEach(value => {
const option = this.previewWidthSelect.createEl('option');
option.value = String(value);
option.text = `${value}px`;
});
if (!XHS_PREVIEW_WIDTH_OPTIONS.includes(currentPreviewWidth)) {
const customOption = this.previewWidthSelect.createEl('option');
customOption.value = String(currentPreviewWidth);
customOption.text = `${currentPreviewWidth}px`;
}
this.previewWidthSelect.value = String(currentPreviewWidth);
this.previewWidthSelect.onchange = async () => {
const value = parseInt(this.previewWidthSelect.value, 10);
if (Number.isFinite(value) && value > 0) {
await this.onPreviewWidthChanged(value);
} else {
this.previewWidthSelect.value = String(this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH);
// 使用真实的 select 元素,直接应用样式
this.platformSelect = platformSelector.createEl('select', { cls: 'note2any-select note2any-platform-select' }) as HTMLSelectElement;
const wechatOption = this.platformSelect.createEl('option');
wechatOption.value = 'wechat';
wechatOption.text = '📱 公众号';
const xhsOption = this.platformSelect.createEl('option');
xhsOption.value = 'xiaohongshu';
xhsOption.text = '📔 小红书';
// 设置默认选中为小红书
this.platformSelect.value = 'xiaohongshu';
this.platformSelect.onchange = () => {
if (this.platformSelect && this.platformSelect.value === 'wechat' && this.onPlatformChangeCallback) {
this.onPlatformChangeCallback('wechat');
}
};
const fontCard = this.createGridCard(board, 'xhs-area-font');
//fontCard.createDiv({ cls: 'xhs-label', text: '字号' });
const fontSizeGroup = fontCard.createDiv({ cls: 'font-size-group' });
const decreaseBtn = fontSizeGroup.createEl('button', { text: '', cls: 'font-size-btn' });
decreaseBtn.onclick = () => this.changeFontSize(-1);
this.fontSizeInput = fontSizeGroup.createEl('input', {
cls: 'font-size-input',
attr: {
type: 'number',
min: String(XHS_FONT_SIZE_MIN),
max: String(XHS_FONT_SIZE_MAX),
value: String(XHS_FONT_SIZE_DEFAULT)
}
// 按钮组
const buttonGroup = header.createDiv({ cls: 'note2any-button-group' });
const refreshBtn = buttonGroup.createEl('button', {
text: '刷新',
cls: 'note2any-button'
});
this.fontSizeInput.onchange = () => this.onFontSizeInputChanged();
refreshBtn.onclick = () => this.refresh();
const increaseBtn = fontSizeGroup.createEl('button', { text: '', cls: 'font-size-btn' });
increaseBtn.onclick = () => this.changeFontSize(1);
// 主题选择器
const themeCard = this.createGridCard(board, 'xhs-area-theme');
const themeLabel = themeCard.createDiv({ cls: 'xhs-label', text: '主题' });
this.themeSelect = themeCard.createEl('select', { cls: 'xhs-select' });
const publishBtn = buttonGroup.createEl('button', {
text: '发布',
cls: 'note2any-button'
});
publishBtn.onclick = () => this.publish();
const accessBtn = buttonGroup.createEl('button', {
text: '访问',
cls: 'note2any-button'
});
accessBtn.onclick = () => {
// TODO: 实现小红书访问逻辑
new Notice('小红书访问功能待实现');
};
// 控制栏 (账号、主题、宽度)
const controlsRow = this.container.createDiv({ cls: 'note2any-controls-row' });
// 账号字段
const accountField = controlsRow.createDiv({ cls: 'note2any-field note2any-field-account' });
accountField.createDiv({ cls: 'note2any-field-label', text: '账号' });
const accountSelect = accountField.createEl('select', { cls: 'note2any-select' });
const accountOption = accountSelect.createEl('option');
accountOption.value = 'default';
accountOption.text = 'Value';
// 主题字段
const themeField = controlsRow.createDiv({ cls: 'note2any-field note2any-field-theme' });
themeField.createDiv({ cls: 'note2any-field-label', text: '主题' });
this.themeSelect = themeField.createEl('select', { cls: 'note2any-select' }) as HTMLSelectElement;
this.themeSelect.onchange = async () => {
// 更新全局设置
this.settings.defaultStyle = this.themeSelect.value;
this.applyThemeCSS();
await this.repaginateAndRender();
// 保存设置
const plugin = (this.app as any)?.plugins?.getPlugin?.('note2any');
if (plugin?.saveSettings) {
await plugin.saveSettings();
}
};
// 填充主题选项
for (let theme of this.assetsManager.themes) {
const option = this.themeSelect.createEl('option');
option.value = theme.className;
option.text = theme.name;
option.selected = theme.className === this.settings.defaultStyle;
}
// 高亮选择器
const highlightCard = this.createGridCard(board, 'xhs-area-highlight');
const highlightLabel = highlightCard.createDiv({ cls: 'xhs-label', text: '代码高亮' });
this.highlightSelect = highlightCard.createEl('select', { cls: 'xhs-select' });
this.highlightSelect.onchange = async () => {
// 更新全局设置
this.settings.defaultHighlight = this.highlightSelect.value;
this.applyThemeCSS();
// 宽度字段
const widthField = controlsRow.createDiv({ cls: 'note2any-field note2any-field-width' });
widthField.createDiv({ cls: 'note2any-field-label', text: '宽度' });
const currentPreviewWidth = this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH;
this.previewWidthSelect = widthField.createEl('select', {
cls: 'note2any-select'
}) as HTMLSelectElement;
// 添加宽度选项
const widthOptions = [360, 540, 720];
for (let width of widthOptions) {
const option = this.previewWidthSelect.createEl('option');
option.value = String(width);
option.text = `${width}px`;
option.selected = width === currentPreviewWidth;
}
this.previewWidthSelect.onchange = async () => {
const newWidth = parseInt(this.previewWidthSelect.value);
this.settings.xhsPreviewWidth = newWidth;
await this.repaginateAndRender();
// 保存设置
const plugin = (this.app as any)?.plugins?.getPlugin?.('note2any');
if (plugin?.saveSettings) {
await plugin.saveSettings();
}
};
// 填充高亮选项
for (let highlight of this.assetsManager.highlights) {
const option = this.highlightSelect.createEl('option');
option.value = highlight.url;
option.text = highlight.name;
option.selected = highlight.url === this.settings.defaultHighlight;
}
const contentWrapper = board.createDiv({ cls: 'xhs-area-content' });
this.pageContainer = contentWrapper.createDiv({ cls: 'xhs-page-container' });
const paginationCard = this.createGridCard(board, 'xhs-area-pagination xhs-pagination');
const prevBtn = paginationCard.createEl('button', { text: '', cls: 'xhs-nav-btn' });
prevBtn.onclick = () => this.previousPage();
const indicator = paginationCard.createDiv({ cls: 'xhs-page-indicator' });
this.pageNumberInput = indicator.createEl('input', {
cls: 'xhs-page-number-input',
attr: { type: 'text', value: '1', inputmode: 'numeric', 'aria-label': '当前页码' }
}) as HTMLInputElement;
this.pageNumberInput.onfocus = () => this.pageNumberInput.select();
this.pageNumberInput.onkeydown = (evt: KeyboardEvent) => {
if (evt.key === 'Enter') {
evt.preventDefault();
this.handlePageNumberInput();
}
};
this.pageNumberInput.oninput = () => {
const sanitized = this.pageNumberInput.value.replace(/\D/g, '');
if (sanitized !== this.pageNumberInput.value) {
this.pageNumberInput.value = sanitized;
}
};
this.pageNumberInput.onblur = () => this.handlePageNumberInput();
this.pageTotalLabel = indicator.createEl('span', { cls: 'xhs-page-number-total', text: '/1' });
const nextBtn = paginationCard.createEl('button', { text: '', cls: 'xhs-nav-btn' });
nextBtn.onclick = () => this.nextPage();
const sliceCard = this.createGridCard(board, 'xhs-area-slice');
const sliceCurrentBtn = sliceCard.createEl('button', { text: '当前页切图', cls: 'xhs-slice-btn' });
// 内容区域
this.pageContainer = this.container.createDiv({ cls: 'note2any-content-area' });
// 底部工具栏
const bottomToolbar = this.container.createDiv({ cls: 'note2any-bottom-toolbar' });
// 当前图按钮
const sliceCurrentBtn = bottomToolbar.createEl('button', {
text: '当前图',
cls: 'note2any-slice-button'
});
sliceCurrentBtn.onclick = () => this.sliceCurrentPage();
const sliceAllBtn = sliceCard.createEl('button', { text: '全部页切图', cls: 'xhs-slice-btn secondary' });
// 字体大小控制
const fontSizeControl = bottomToolbar.createDiv({ cls: 'note2any-fontsize-control' });
// 字体大小下拉选择器
const fontSizeSelectWrapper = fontSizeControl.createDiv({ cls: 'note2any-fontsize-select-wrapper' });
const fontSizeSelect = fontSizeSelectWrapper.createEl('select', {
cls: 'note2any-fontsize-select'
}) as HTMLSelectElement;
// 添加字体大小选项 (30-40)
for (let size = XHS_FONT_SIZE_MIN; size <= XHS_FONT_SIZE_MAX; size++) {
const option = fontSizeSelect.createEl('option');
option.value = String(size);
option.text = String(size);
option.selected = size === XHS_FONT_SIZE_DEFAULT;
}
fontSizeSelect.onchange = async () => {
this.currentFontSize = parseInt(fontSizeSelect.value);
this.fontSizeInput.value = String(this.currentFontSize);
await this.repaginateAndRender();
};
const stepper = fontSizeControl.createDiv({ cls: 'note2any-stepper' });
const decreaseBtn = stepper.createEl('button', {
text: '',
cls: 'note2any-stepper-button'
});
decreaseBtn.onclick = async () => {
if (this.currentFontSize > XHS_FONT_SIZE_MIN) {
this.currentFontSize--;
fontSizeSelect.value = String(this.currentFontSize);
this.fontSizeInput.value = String(this.currentFontSize);
await this.repaginateAndRender();
}
};
stepper.createDiv({ cls: 'note2any-stepper-separator' });
const increaseBtn = stepper.createEl('button', {
text: '',
cls: 'note2any-stepper-button'
});
increaseBtn.onclick = async () => {
if (this.currentFontSize < XHS_FONT_SIZE_MAX) {
this.currentFontSize++;
fontSizeSelect.value = String(this.currentFontSize);
this.fontSizeInput.value = String(this.currentFontSize);
await this.repaginateAndRender();
}
};
// 隐藏的字体输入框 (保留以兼容现有逻辑)
this.fontSizeInput = fontSizeControl.createEl('input', {
attr: {
type: 'number',
min: String(XHS_FONT_SIZE_MIN),
max: String(XHS_FONT_SIZE_MAX),
value: String(XHS_FONT_SIZE_DEFAULT),
style: 'display: none;'
}
});
this.fontSizeInput.onchange = () => {
this.currentFontSize = parseInt(this.fontSizeInput.value);
fontSizeSelect.value = String(this.currentFontSize);
};
// 分页控制
const pagination = bottomToolbar.createDiv({ cls: 'note2any-pagination' });
pagination.createDiv({ cls: 'note2any-pagination-separator' });
const prevBtn = pagination.createEl('button', {
cls: 'note2any-pagination-button',
attr: { 'aria-label': '上一页' }
});
prevBtn.innerHTML = '<svg width="32" height="32" viewBox="0 0 32 32"><path d="M20 8 L12 16 L20 24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
prevBtn.onclick = () => this.previousPage();
const pageCurrent = pagination.createEl('input', {
cls: 'note2any-pagination-current',
type: 'text',
value: '1',
attr: {
'inputmode': 'numeric',
'pattern': '[0-9]*'
}
});
// 处理页码输入 - 回车跳转
pageCurrent.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Enter') {
const targetPage = parseInt(pageCurrent.value);
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= this.pages.length) {
this.currentPageIndex = targetPage - 1;
this.renderCurrentPage();
} else {
// 恢复当前页码
pageCurrent.value = String(this.currentPageIndex + 1);
}
pageCurrent.blur();
}
});
// 失焦时跳转
pageCurrent.addEventListener('blur', () => {
const targetPage = parseInt(pageCurrent.value);
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= this.pages.length) {
this.currentPageIndex = targetPage - 1;
this.renderCurrentPage();
} else {
// 恢复当前页码
pageCurrent.value = String(this.currentPageIndex + 1);
}
});
// 聚焦时全选文本,方便输入
pageCurrent.addEventListener('focus', () => {
pageCurrent.select();
});
// 存储引用以便在其他地方更新显示
(this as any).pageCurrentDisplay = pageCurrent;
pagination.createDiv({
cls: 'note2any-pagination-separator-text',
text: '/'
});
this.pageTotalLabel = pagination.createEl('span', {
cls: 'note2any-pagination-total',
text: '68'
});
const nextBtn = pagination.createEl('button', {
cls: 'note2any-pagination-button',
attr: { 'aria-label': '下一页' }
});
nextBtn.innerHTML = '<svg width="32" height="32" viewBox="0 0 32 32"><path d="M12 8 L20 16 L12 24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
nextBtn.onclick = () => this.nextPage();
// 将可见的页码输入框设为主输入框
this.pageNumberInput = pageCurrent as HTMLInputElement;
// 存储显示元素引用以便更新
(this as any).pageCurrentDisplay = pageCurrent;
// 全部图按钮
const sliceAllBtn = bottomToolbar.createEl('button', {
text: '全部图',
cls: 'note2any-slice-button'
});
sliceAllBtn.onclick = () => this.sliceAllPages();
// 模板选择器 (隐藏,保留以兼容)
this.templateSelect = this.container.createEl('select', {
attr: { style: 'display: none;' }
});
['默认模板', '简约模板', '杂志模板'].forEach(name => {
const option = this.templateSelect.createEl('option');
option.value = name;
option.text = name;
});
}
private createGridCard(parent: HTMLElement, areaClass: string): HTMLDivElement {
return parent.createDiv({ cls: `xhs-card ${areaClass}` });
}
/**
* 渲染文章内容并分页
@@ -291,14 +439,18 @@ export class XiaohongshuPreview {
private updatePageNumberDisplay(): void {
if (!this.pageNumberInput || !this.pageTotalLabel) return;
const total = this.pages.length;
const pageCurrentDisplay = (this as any).pageCurrentDisplay;
if (total === 0) {
this.pageNumberInput.value = '0';
this.pageTotalLabel.innerText = '/0';
this.pageTotalLabel.innerText = '0';
if (pageCurrentDisplay) pageCurrentDisplay.textContent = '0';
return;
}
const current = Math.min(this.currentPageIndex + 1, total);
this.pageNumberInput.value = String(current);
this.pageTotalLabel.innerText = `/${total}`;
this.pageTotalLabel.innerText = String(total);
if (pageCurrentDisplay) pageCurrentDisplay.textContent = String(current);
}
private handlePageNumberInput(): void {
@@ -347,10 +499,11 @@ export class XiaohongshuPreview {
pageElement.style.width = `${actualWidth}px`;
pageElement.style.height = `${actualHeight}px`;
pageElement.style.transform = `scale(${scale})`;
pageElement.style.transformOrigin = 'top left';
pageElement.style.transformOrigin = 'center center';
pageElement.style.position = 'absolute';
pageElement.style.top = '0';
pageElement.style.left = '0';
pageElement.style.top = '50%';
pageElement.style.left = '50%';
pageElement.style.transform = `translate(-50%, -50%) scale(${scale})`;
}
private async onPreviewWidthChanged(newWidth: number): Promise<void> {
@@ -526,6 +679,10 @@ export class XiaohongshuPreview {
if (this.container) {
this.container.style.display = 'flex';
}
// 确保平台选择器显示正确的选项
if (this.platformSelect) {
this.platformSelect.value = 'xiaohongshu';
}
}
/**
@@ -605,9 +762,7 @@ export class XiaohongshuPreview {
if (this.themeSelect) {
this.themeSelect.value = theme;
}
if (this.highlightSelect) {
this.highlightSelect.value = highlight;
}
// 高亮已移除,保留此方法以兼容外部调用
this.applyThemeCSS();
}
}