diff --git a/BUGFIX_ASYNC_LOADING_ISSUE.md b/BUGFIX_ASYNC_LOADING_ISSUE.md deleted file mode 100644 index e69de29..0000000 diff --git a/DEBUG_LOADING_ISSUE.md b/DEBUG_LOADING_ISSUE.md deleted file mode 100644 index e69de29..0000000 diff --git a/LICENSE b/LICENSE index db4b010..9a406e9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 sunbooshi +Copyright (c) 2025 Gavin Chan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md deleted file mode 100644 index e69de29..0000000 diff --git a/build.sh b/build.sh index cc7eb02..dea2005 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,15 @@ set -e # 出错立即退出 # 1. 构建 -npm run build +echo "🏗️ 开始构建..." +if npm run build; then + echo "✅ 构建成功" + echo +else + echo "❌ 构建失败,脚本终止" + echo + exit 1 +fi # 2. 目标目录 PLUGIN_DIR=~/myweb/.obsidian/plugins/note-to-mp @@ -16,12 +24,12 @@ for FILE in "${FILES[@]}"; do if [ -f "$TARGET" ]; then mkdir -p "$(dirname "$BACKUP")" cp -f "$TARGET" "$BACKUP" - echo "已备份 $TARGET -> $BACKUP" + echo "💾 已备份 $TARGET -> $BACKUP" fi if [ -f "$FILE" ]; then cp -f "$FILE" "$TARGET" - echo "已更新 $TARGET" + echo "📂 已更新 $TARGET" else echo "⚠️ 源文件 $FILE 不存在,跳过" fi @@ -31,7 +39,12 @@ done if [ -d "assets" ]; then mkdir -p "$PLUGIN_DIR/assets" rsync -a --delete assets/ "$PLUGIN_DIR/assets/" >/dev/null - echo "已同步 assets -> $PLUGIN_DIR/assets/" + echo "🎨 已同步 assets -> $PLUGIN_DIR/assets/" + echo else echo "⚠️ 源目录 assets 不存在,跳过" + echo fi + +echo "✅ 部署完成!" +echo diff --git a/ARCHITECTURE_COMPARISON.md b/docs/ARCHITECTURE_COMPARISON.md similarity index 100% rename from ARCHITECTURE_COMPARISON.md rename to docs/ARCHITECTURE_COMPARISON.md diff --git a/ARCHITECTURE_QUICK_REFERENCE.md b/docs/ARCHITECTURE_QUICK_REFERENCE.md similarity index 100% rename from ARCHITECTURE_QUICK_REFERENCE.md rename to docs/ARCHITECTURE_QUICK_REFERENCE.md diff --git a/ARCHITECTURE_REFACTORING_COMPLETE.md b/docs/ARCHITECTURE_REFACTORING_COMPLETE.md similarity index 100% rename from ARCHITECTURE_REFACTORING_COMPLETE.md rename to docs/ARCHITECTURE_REFACTORING_COMPLETE.md diff --git a/BUGFIX_LOADING_AND_STYLE_ISSUE.md b/docs/BUGFIX_LOADING_AND_STYLE_ISSUE.md similarity index 100% rename from BUGFIX_LOADING_AND_STYLE_ISSUE.md rename to docs/BUGFIX_LOADING_AND_STYLE_ISSUE.md diff --git a/CHANGELOG.md b/docs/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to docs/CHANGELOG.md diff --git a/PLATFORM_REFACTORING_SUMMARY.md b/docs/PLATFORM_REFACTORING_SUMMARY.md similarity index 100% rename from PLATFORM_REFACTORING_SUMMARY.md rename to docs/PLATFORM_REFACTORING_SUMMARY.md diff --git a/README_PLATFORM_SELECTOR_DONE.md b/docs/README_PLATFORM_SELECTOR_DONE.md similarity index 100% rename from README_PLATFORM_SELECTOR_DONE.md rename to docs/README_PLATFORM_SELECTOR_DONE.md diff --git a/README_XIAOHONGSHU_LAYOUT_DONE.md b/docs/README_XIAOHONGSHU_LAYOUT_DONE.md similarity index 100% rename from README_XIAOHONGSHU_LAYOUT_DONE.md rename to docs/README_XIAOHONGSHU_LAYOUT_DONE.md diff --git a/SLICE_IMAGE_GUIDE.md b/docs/SLICE_IMAGE_GUIDE.md similarity index 100% rename from SLICE_IMAGE_GUIDE.md rename to docs/SLICE_IMAGE_GUIDE.md diff --git a/XIAOHONGSHU_COMPACT_LAYOUT.md b/docs/XIAOHONGSHU_COMPACT_LAYOUT.md similarity index 100% rename from XIAOHONGSHU_COMPACT_LAYOUT.md rename to docs/XIAOHONGSHU_COMPACT_LAYOUT.md diff --git a/XIAOHONGSHU_FEATURE_SUMMARY.md b/docs/XIAOHONGSHU_FEATURE_SUMMARY.md similarity index 100% rename from XIAOHONGSHU_FEATURE_SUMMARY.md rename to docs/XIAOHONGSHU_FEATURE_SUMMARY.md diff --git a/XIAOHONGSHU_KEEP_PLATFORM_SELECTOR.md b/docs/XIAOHONGSHU_KEEP_PLATFORM_SELECTOR.md similarity index 100% rename from XIAOHONGSHU_KEEP_PLATFORM_SELECTOR.md rename to docs/XIAOHONGSHU_KEEP_PLATFORM_SELECTOR.md diff --git a/XIAOHONGSHU_LAYOUT_CHANGE_LOG.md b/docs/XIAOHONGSHU_LAYOUT_CHANGE_LOG.md similarity index 100% rename from XIAOHONGSHU_LAYOUT_CHANGE_LOG.md rename to docs/XIAOHONGSHU_LAYOUT_CHANGE_LOG.md diff --git a/XIAOHONGSHU_PREVIEW_GUIDE.md b/docs/XIAOHONGSHU_PREVIEW_GUIDE.md similarity index 100% rename from XIAOHONGSHU_PREVIEW_GUIDE.md rename to docs/XIAOHONGSHU_PREVIEW_GUIDE.md diff --git a/XIAOHONGSHU_STYLE_OPTIMIZATION.md b/docs/XIAOHONGSHU_STYLE_OPTIMIZATION.md similarity index 100% rename from XIAOHONGSHU_STYLE_OPTIMIZATION.md rename to docs/XIAOHONGSHU_STYLE_OPTIMIZATION.md diff --git a/XIAOHONGSHU_UI_LAYOUT.md b/docs/XIAOHONGSHU_UI_LAYOUT.md similarity index 100% rename from XIAOHONGSHU_UI_LAYOUT.md rename to docs/XIAOHONGSHU_UI_LAYOUT.md diff --git a/architecture.md b/docs/architecture.md similarity index 100% rename from architecture.md rename to docs/architecture.md diff --git a/create_milestone.md b/docs/create_milestone.md similarity index 100% rename from create_milestone.md rename to docs/create_milestone.md diff --git a/detaildesign.md b/docs/detaildesign.md similarity index 100% rename from detaildesign.md rename to docs/detaildesign.md diff --git a/diagrams.md b/docs/diagrams.md similarity index 100% rename from diagrams.md rename to docs/diagrams.md diff --git a/image-pipeline.md b/docs/image-pipeline.md similarity index 100% rename from image-pipeline.md rename to docs/image-pipeline.md diff --git a/mp_todolist.md b/docs/mp_todolist.md similarity index 100% rename from mp_todolist.md rename to docs/mp_todolist.md diff --git a/render-service-blueprint.md b/docs/render-service-blueprint.md similarity index 100% rename from render-service-blueprint.md rename to docs/render-service-blueprint.md diff --git a/xhs_todolist.md b/docs/xhs_todolist.md similarity index 100% rename from xhs_todolist.md rename to docs/xhs_todolist.md diff --git a/xhspublisher.md b/docs/xhspublisher.md similarity index 100% rename from xhspublisher.md rename to docs/xhspublisher.md diff --git a/xiaohongshu-design.md b/docs/xiaohongshu-design.md similarity index 100% rename from xiaohongshu-design.md rename to docs/xiaohongshu-design.md diff --git a/xiaohongshu-summary.md b/docs/xiaohongshu-summary.md similarity index 100% rename from xiaohongshu-summary.md rename to docs/xiaohongshu-summary.md diff --git a/src/xiaohongshu/automation-notes.md b/docs/xiaohongshu/automation-notes.md similarity index 100% rename from src/xiaohongshu/automation-notes.md rename to docs/xiaohongshu/automation-notes.md diff --git a/src/xiaohongshu/completion-summary.md b/docs/xiaohongshu/completion-summary.md similarity index 100% rename from src/xiaohongshu/completion-summary.md rename to docs/xiaohongshu/completion-summary.md diff --git a/src/xiaohongshu/debug-guide.md b/docs/xiaohongshu/debug-guide.md similarity index 100% rename from src/xiaohongshu/debug-guide.md rename to docs/xiaohongshu/debug-guide.md diff --git a/images/xhs/note2mdtest_3.png b/images/xhs/note2mdtest_3.png index 37f1a90..fac806b 100644 Binary files a/images/xhs/note2mdtest_3.png and b/images/xhs/note2mdtest_3.png differ diff --git a/manifest.json b/manifest.json index e05f008..f8d150c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "note-to-mp", - "name": "NoteToMP", - "version": "1.3.0", + "name": "NoteToAny", + "version": "1.3.4", "minAppVersion": "1.4.5", - "description": "Send notes to WeChat MP drafts, or copy notes to WeChat MP editor, perfect preservation of note styles, support code highlighting, line numbers in code, and support local image uploads.", - "author": "Sun Booshi", - "authorUrl": "https://sunboshi.tech", + "description": "xiaohongshu/mp publisher ", + "author": "Gavin chan", + "authorUrl": "https://biboer.cn", "isDesktopOnly": false } diff --git a/src/platform-chooser.ts b/src/platform-chooser.ts index 64036cd..65de878 100644 --- a/src/platform-chooser.ts +++ b/src/platform-chooser.ts @@ -53,7 +53,7 @@ export class PlatformChooser { constructor(container: HTMLElement, options: PlatformChooserOptions = {}) { this.container = container; - this.currentPlatform = options.defaultPlatform || 'wechat'; + this.currentPlatform = options.defaultPlatform || 'xiaohongshu'; if (options.onPlatformChange) { this.onChange = (platform) => { options.onPlatformChange!(platform); @@ -72,15 +72,16 @@ export class PlatformChooser { * 渲染平台选择器 UI */ render(): void { - // 创建平台选择行 - const lineDiv = this.container.createDiv({ cls: 'toolbar-line platform-selector-line' }); + // 将容器作为单层 Grid 行使用 + this.container.addClass('platform-selector-line'); + this.container.addClass('platform-chooser-grid'); // 创建标签 - const platformLabel = lineDiv.createDiv({ cls: 'style-label' }); + const platformLabel = this.container.createDiv({ cls: 'style-label' }); platformLabel.innerText = '发布平台'; // 创建选择器 - const platformSelect = lineDiv.createEl('select', { cls: 'platform-select' }); + const platformSelect = this.container.createEl('select', { cls: 'platform-select' }); this.selectElement = platformSelect; // 添加平台选项 diff --git a/src/preview-manager.ts b/src/preview-manager.ts index 79998c9..65becf4 100644 --- a/src/preview-manager.ts +++ b/src/preview-manager.ts @@ -37,7 +37,7 @@ export class PreviewManager { private xhsContainer: HTMLDivElement | null = null; // 状态 - private currentPlatform: PlatformType = 'wechat'; + private currentPlatform: PlatformType = 'xiaohongshu'; private currentFile: TFile | null = null; constructor(container: HTMLElement, app: App, render: ArticleRender) { @@ -68,8 +68,8 @@ export class PreviewManager { // 3. 创建并构建小红书预览 this.createXiaohongshuPreview(); - // 4. 初始显示微信平台 - await this.switchPlatform('wechat'); + // 4. 初始显示小红书平台 + await this.switchPlatform('xiaohongshu'); console.log('[PreviewManager] 界面构建完成'); } diff --git a/src/settings.ts b/src/settings.ts index dafc062..ae1c2ca 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -51,6 +51,7 @@ export class NMPSettings { sliceImageSavePath: string; // 切图保存路径 sliceImageWidth: number; // 切图宽度(像素) sliceImageAspectRatio: string; // 横竖比例,格式 "3:4" + xhsPreviewWidth: number; // 小红书预览宽度(像素) private static instance: NMPSettings; @@ -100,6 +101,7 @@ export class NMPSettings { this.sliceImageSavePath = '/Users/gavin/note2mp/images/xhs'; this.sliceImageWidth = 1080; this.sliceImageAspectRatio = '3:4'; + this.xhsPreviewWidth = 540; } resetStyelAndHighlight() { @@ -136,7 +138,8 @@ export class NMPSettings { batchPublishPresets = [], sliceImageSavePath, sliceImageWidth, - sliceImageAspectRatio + sliceImageAspectRatio, + xhsPreviewWidth } = data; const settings = NMPSettings.getInstance(); @@ -166,6 +169,9 @@ export class NMPSettings { if (sliceImageSavePath) settings.sliceImageSavePath = sliceImageSavePath; if (sliceImageWidth !== undefined && Number.isFinite(sliceImageWidth)) settings.sliceImageWidth = Math.max(100, parseInt(sliceImageWidth)); if (sliceImageAspectRatio) settings.sliceImageAspectRatio = sliceImageAspectRatio; + if (xhsPreviewWidth !== undefined && Number.isFinite(xhsPreviewWidth)) { + settings.xhsPreviewWidth = Math.max(100, parseInt(xhsPreviewWidth)); + } settings.getExpiredDate(); settings.isLoaded = true; @@ -200,6 +206,7 @@ export class NMPSettings { 'sliceImageSavePath': settings.sliceImageSavePath, 'sliceImageWidth': settings.sliceImageWidth, 'sliceImageAspectRatio': settings.sliceImageAspectRatio, + 'xhsPreviewWidth': settings.xhsPreviewWidth, } } @@ -221,4 +228,4 @@ export class NMPSettings { if (this.expireat == null) return false; return this.expireat > new Date(); } -} \ No newline at end of file +} diff --git a/src/wechat/wechat-preview.ts b/src/wechat/wechat-preview.ts index 9fd7be1..a58d3e1 100644 --- a/src/wechat/wechat-preview.ts +++ b/src/wechat/wechat-preview.ts @@ -93,25 +93,23 @@ export class WechatPreview { * 构建工具栏 */ private buildToolbar(parent: HTMLDivElement): void { - let lineDiv; + // 单层 Grid:所有控件直接挂到 parent(.preview-toolbar) // 公众号选择 if (this.settings.wxInfo.length > 1 || Platform.isDesktop) { - lineDiv = parent.createDiv({ cls: 'toolbar-line' }); - - const wxLabel = lineDiv.createDiv({ cls: 'style-label' }); + const wxLabel = parent.createDiv({ cls: 'style-label' }); wxLabel.innerText = '公众号'; - - const wxSelect = lineDiv.createEl('select', { cls: 'wechat-select' }); + + const wxSelect = parent.createEl('select', { cls: 'wechat-select' }); wxSelect.onchange = async () => { this.currentAppId = wxSelect.value; this.onAppIdChanged(); }; - + const defaultOp = wxSelect.createEl('option'); defaultOp.value = ''; defaultOp.text = '请在设置里配置公众号'; - + for (let i = 0; i < this.settings.wxInfo.length; i++) { const op = wxSelect.createEl('option'); const wx = this.settings.wxInfo[i]; @@ -125,8 +123,8 @@ export class WechatPreview { this.wechatSelect = wxSelect; if (Platform.isDesktop) { - const separator = lineDiv.createDiv({ cls: 'toolbar-separator' }); - const openBtn = lineDiv.createEl('button', { text: '🌐 去公众号后台', cls: 'toolbar-button purple-gradient' }); + //parent.createDiv({ cls: 'toolbar-separator' }); + const openBtn = parent.createEl('button', { text: '🌐 去公众号后台', cls: 'toolbar-button purple-gradient' }); openBtn.onclick = async () => { const { shell } = require('electron'); shell.openExternal('https://mp.weixin.qq.com'); @@ -135,21 +133,15 @@ export class WechatPreview { } } - // 操作按钮行 - lineDiv = parent.createDiv({ cls: 'toolbar-line flex-wrap' }); - - const refreshBtn = lineDiv.createEl('button', { text: '🔄 刷新', cls: 'toolbar-button purple-gradient' }); - refreshBtn.onclick = async () => { - if (this.onRefreshCallback) { - await this.onRefreshCallback(); - } - }; + // 操作按钮(直接平铺在 Grid 中) + const refreshBtn = parent.createEl('button', { text: '🔄 刷新', cls: 'toolbar-button purple-gradient' }); + refreshBtn.onclick = async () => { if (this.onRefreshCallback) await this.onRefreshCallback(); }; - const postBtn = lineDiv.createEl('button', { text: '📝 发布', cls: 'toolbar-button' }); + const postBtn = parent.createEl('button', { text: '📝 发布', cls: 'toolbar-button' }); postBtn.onclick = async () => await this.postArticle(); if (Platform.isDesktop && this.settings.isAuthKeyVaild()) { - const htmlBtn = lineDiv.createEl('button', { text: '💾 导出HTML', cls: 'toolbar-button' }); + const htmlBtn = parent.createEl('button', { text: '💾 导出HTML', cls: 'toolbar-button' }); htmlBtn.onclick = async () => await this.exportHTML(); } @@ -166,11 +158,10 @@ export class WechatPreview { * 构建封面选择器 */ private buildCoverSelector(parent: HTMLDivElement): void { - const lineDiv = parent.createDiv({ cls: 'toolbar-line' }); - const coverTitle = lineDiv.createDiv({ cls: 'style-label' }); + const coverTitle = parent.createDiv({ cls: 'style-label' }); coverTitle.innerText = '封面'; - this.useDefaultCover = lineDiv.createEl('input', { cls: 'input-style' }); + this.useDefaultCover = parent.createEl('input', { cls: 'input-style' }); this.useDefaultCover.setAttr('type', 'radio'); this.useDefaultCover.setAttr('name', 'cover'); this.useDefaultCover.setAttr('value', 'default'); @@ -182,11 +173,11 @@ export class WechatPreview { } }; - const defaultLabel = lineDiv.createEl('label'); + const defaultLabel = parent.createEl('label'); defaultLabel.innerText = '默认'; defaultLabel.setAttr('for', 'default-cover'); - this.useLocalCover = lineDiv.createEl('input', { cls: 'input-style' }); + this.useLocalCover = parent.createEl('input', { cls: 'input-style' }); this.useLocalCover.setAttr('type', 'radio'); this.useLocalCover.setAttr('name', 'cover'); this.useLocalCover.setAttr('value', 'local'); @@ -198,11 +189,11 @@ export class WechatPreview { } }; - const localLabel = lineDiv.createEl('label'); + const localLabel = parent.createEl('label'); localLabel.setAttr('for', 'local-cover'); localLabel.innerText = '上传'; - this.coverEl = lineDiv.createEl('input', { cls: 'upload-input' }); + this.coverEl = parent.createEl('input', { cls: 'upload-input' }); this.coverEl.setAttr('type', 'file'); this.coverEl.setAttr('placeholder', '封面图片'); this.coverEl.setAttr('accept', '.png, .jpg, .jpeg'); @@ -214,12 +205,10 @@ export class WechatPreview { * 构建样式选择器 */ private buildStyleSelector(parent: HTMLDivElement): void { - const lineDiv = parent.createDiv({ cls: 'toolbar-line flex-wrap' }); - - const cssStyle = lineDiv.createDiv({ cls: 'style-label' }); + const cssStyle = parent.createDiv({ cls: 'style-label' }); cssStyle.innerText = '样式'; - const selectBtn = lineDiv.createEl('select', { cls: 'style-select' }); + const selectBtn = parent.createEl('select', { cls: 'style-select' }); selectBtn.onchange = async () => { this.currentTheme = selectBtn.value; this.render.updateStyle(selectBtn.value); @@ -233,12 +222,12 @@ export class WechatPreview { } this.themeSelect = selectBtn; - const separator = lineDiv.createDiv({ cls: 'toolbar-separator' }); + parent.createDiv({ cls: 'toolbar-separator' }); - const highlightStyle = lineDiv.createDiv({ cls: 'style-label' }); + const highlightStyle = parent.createDiv({ cls: 'style-label' }); highlightStyle.innerText = '代码高亮'; - const highlightStyleBtn = lineDiv.createEl('select', { cls: 'style-select' }); + const highlightStyleBtn = parent.createEl('select', { cls: 'style-select' }); highlightStyleBtn.onchange = async () => { this.currentHighlight = highlightStyleBtn.value; this.render.updateHighLight(highlightStyleBtn.value); diff --git a/src/xiaohongshu/xhs-preview.ts b/src/xiaohongshu/xhs-preview.ts index 9bc0c36..729839b 100644 --- a/src/xiaohongshu/xhs-preview.ts +++ b/src/xiaohongshu/xhs-preview.ts @@ -15,6 +15,9 @@ import AssetsManager from '../assets'; import { paginateArticle, renderPage, PageInfo } from './paginator'; import { sliceCurrentPage, sliceAllPages } from './slice'; +const XHS_PREVIEW_DEFAULT_WIDTH = 540; +const XHS_PREVIEW_WIDTH_OPTIONS = [1080, 720, 540, 360]; + /** * 小红书预览视图类 */ @@ -29,6 +32,7 @@ export class XiaohongshuPreview { topToolbar!: HTMLDivElement; templateSelect!: HTMLSelectElement; fontSizeInput!: HTMLInputElement; + previewWidthSelect!: HTMLSelectElement; pageContainer!: HTMLDivElement; bottomToolbar!: HTMLDivElement; @@ -110,6 +114,30 @@ export class XiaohongshuPreview { option.text = name; }); + const previewWidthLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' }); + previewWidthLabel.innerText = '预览宽度'; + this.previewWidthSelect = this.topToolbar.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); + } + }; + // 字号控制(可直接编辑) const fontSizeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' }); fontSizeLabel.innerText = '字号'; @@ -205,6 +233,7 @@ export class XiaohongshuPreview { if (this.currentThemeClass) classes.push('note-to-mp'); const pageElement = wrapper.createDiv({ cls: classes.join(' ') }); renderPage(pageElement, page.content, this.settings); + this.applyPreviewSizing(wrapper, pageElement); // 应用字体设置 this.applyFontSettings(pageElement); @@ -213,6 +242,49 @@ export class XiaohongshuPreview { this.pageNumberDisplay.innerText = `${this.currentPageIndex + 1}/${this.pages.length}`; } + /** + * 根据设置的宽度和横竖比应用预览尺寸与缩放 + */ + private applyPreviewSizing(wrapper: HTMLElement, pageElement: HTMLElement): void { + const configuredWidth = this.settings.sliceImageWidth || 1080; + const actualWidth = Math.max(1, configuredWidth); + const ratio = this.parseAspectRatio(this.settings.sliceImageAspectRatio); + const actualHeight = Math.round((actualWidth * ratio.height) / ratio.width); + const previewWidthSetting = this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH; + const previewWidth = Math.max(1, previewWidthSetting); + const scale = Math.max(previewWidth / actualWidth, 0.01); + const previewHeight = Math.max(1, Math.round(actualHeight * scale)); + + wrapper.style.width = `${previewWidth}px`; + wrapper.style.height = `${previewHeight}px`; + + pageElement.style.width = `${actualWidth}px`; + pageElement.style.height = `${actualHeight}px`; + pageElement.style.transform = `scale(${scale})`; + pageElement.style.position = 'absolute'; + pageElement.style.top = '0'; + pageElement.style.left = '0'; + } + + private async onPreviewWidthChanged(newWidth: number): Promise { + if (newWidth <= 0) return; + if (this.settings.xhsPreviewWidth === newWidth) return; + this.settings.xhsPreviewWidth = newWidth; + await this.persistSettings(); + this.renderCurrentPage(); + } + + /** + * 解析横竖比例字符串 + */ + private parseAspectRatio(ratio: string | undefined): { width: number; height: number } { + const parts = (ratio ?? '').split(':').map(part => parseFloat(part.trim())); + if (parts.length === 2 && isFinite(parts[0]) && isFinite(parts[1]) && parts[0] > 0 && parts[1] > 0) { + return { width: parts[0], height: parts[1] }; + } + return { width: 3, height: 4 }; + } + /** * 应用字体设置(仅字号,字体从主题读取) */ @@ -341,6 +413,17 @@ export class XiaohongshuPreview { } } + private async persistSettings(): Promise { + try { + const plugin = (this.app as any)?.plugins?.getPlugin?.('note-to-mp'); + if (plugin?.saveSettings) { + await plugin.saveSettings(); + } + } catch (error) { + console.warn('[XiaohongshuPreview] 保存设置失败', error); + } + } + /** * 显示小红书预览视图 */ @@ -365,6 +448,7 @@ export class XiaohongshuPreview { destroy(): void { this.topToolbar = null as any; this.templateSelect = null as any; + this.previewWidthSelect = null as any; this.fontSizeInput = null as any; this.pageContainer = null as any; this.bottomToolbar = null as any; diff --git a/styles.css b/styles.css index a4fc570..23af5bb 100644 --- a/styles.css +++ b/styles.css @@ -2,26 +2,94 @@ /* =========================================================== */ /* UI 样式 */ +/* 共用样式与去重 */ /* =========================================================== */ + +/* 主题变量统一常用色值/阴影/渐变 */ +:root { + --c-bg: #ffffff; + --c-border: #dadce0; + --c-text-muted: #5f6368; + --c-primary: #1e88e5; + --c-primary-dark: #1565c0; + --c-purple: #667eea; + --c-purple-dark: #764ba2; + --c-blue-2: #42a5f5; + + --shadow-sm: 0 1px 3px rgba(0,0,0,0.08); + --shadow-overlay: 0 2px 4px rgba(0,0,0,0.04); + --shadow-primary-2: 0 2px 6px rgba(30, 136, 229, 0.3); + --shadow-primary-4: 0 4px 8px rgba(30, 136, 229, 0.4); + --shadow-purple-2: 0 2px 6px rgba(102, 126, 234, 0.3); + --shadow-purple-4: 0 4px 8px rgba(102, 126, 234, 0.4); + + --grad-primary: linear-gradient(135deg, var(--c-primary) 0%, var(--c-primary-dark) 100%); + --grad-purple: linear-gradient(135deg, var(--c-purple) 0%, var(--c-purple-dark) 100%); + --grad-blue: linear-gradient(135deg, var(--c-blue-2) 0%, var(--c-primary) 100%); + --grad-toolbar: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + --grad-toolbar-bottom: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + --grad-xhs-bg: linear-gradient(135deg, #f5f7fa 0%, #e8eaf6 100%); +} + +/* 通用按钮外观(不含背景与尺寸) */ +.copy-button, +.refresh-button, +.toolbar-button, +.msg-ok-btn, +.xhs-slice-btn { + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + transition: all 0.2s ease; +} + +/* 通用按钮 hover 的位移效果(各自保留独立阴影) */ +.copy-button:hover, +.refresh-button:hover, +.toolbar-button:hover, +.msg-ok-btn:hover { + transform: translateY(-1px); +} + +/* 下拉选择的通用外观(各自保留尺寸差异) */ +.platform-select, +.wechat-select, +.style-select { + border: 1px solid var(--c-border); + border-radius: 6px; + background: white; + font-size: 13px; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: var(--shadow-sm); +} + +/* 平台与公众号选择的相同 hover/focus 效果(style-select 单独增强) */ +.platform-select:hover, +.wechat-select:hover { border-color: var(--c-primary); } +.platform-select:focus, +.wechat-select:focus { outline: none; border-color: var(--c-primary); } .note-preview { + grid-template-rows: auto 1fr; + grid-template-columns: 1fr; + display: grid; min-height: 100%; width: 100%; height: 100%; - background-color: #fff; - display: flex; - flex-direction: column; + background-color: var(--c-bg); } /* 预览内部平台容器需要可伸缩: */ -.wechat-preview-container, .xiaohongshu-preview-container { +.wechat-preview-container:not([style*="display: none"]), +.xiaohongshu-preview-container:not([style*="display: none"]) { flex: 1; - display: flex; - flex-direction: column; + display: grid !important; + grid-template-rows: auto 1fr; min-height: 0; /* 允许内部滚动区域正确计算高度 */ } .render-div { - flex: 1; overflow-y: auto; padding: 10px; -webkit-user-select: text; @@ -44,55 +112,43 @@ .preview-toolbar { position: relative; - min-height: 100px; - padding: 4px 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, max-content)); + gap: 12px; + align-items: center; + min-height: auto; + padding: 8px 12px; border-bottom: 1px solid #e8eaed; - background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); - box-shadow: 0 2px 4px rgba(0,0,0,0.04); + background: var(--grad-toolbar); + box-shadow: var(--shadow-overlay); } .copy-button { margin-right: 10px; padding: 6px 14px; - background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%); + background: var(--grad-primary); color: white; - border: none; - border-radius: 6px; - cursor: pointer; font-size: 13px; - font-weight: 500; - transition: all 0.2s ease; - box-shadow: 0 2px 6px rgba(30, 136, 229, 0.3); + box-shadow: var(--shadow-primary-2); } -.copy-button:hover { - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(30, 136, 229, 0.4); -} +.copy-button:hover { box-shadow: var(--shadow-primary-4); } .refresh-button { margin-right: 10px; padding: 6px 14px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: var(--grad-purple); color: white; - border: none; - border-radius: 6px; - cursor: pointer; font-size: 13px; - font-weight: 500; - transition: all 0.2s ease; - box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3); + box-shadow: var(--shadow-purple-2); } -.refresh-button:hover { - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4); -} +.refresh-button:hover { box-shadow: var(--shadow-purple-4); } .upload-input { margin-left: 10px; padding: 6px 10px; - border: 1px solid #dadce0; + border: 1px solid var(--c-border); border-radius: 6px; font-size: 13px; transition: all 0.2s ease; @@ -102,9 +158,10 @@ cursor: pointer; } -.upload-input:focus { +.upload-input:focus, +.style-select:focus { outline: none; - border-color: #1e88e5; + border-color: var(--c-primary); box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1); } @@ -114,26 +171,24 @@ height: 16px; margin: 0 6px 0 0; cursor: pointer; - accent-color: #1e88e5; + accent-color: var(--c-primary); } /* Label 标签样式 */ label { font-size: 13px; - color: #5f6368; + color: var(--c-text-muted); cursor: pointer; user-select: none; transition: color 0.2s ease; } -label:hover { - color: #1e88e5; -} +label:hover { color: var(--c-primary); } .style-label { margin-right: 10px; font-size: 13px; - color: #5f6368; + color: var(--c-text-muted); font-weight: 500; white-space: nowrap; } @@ -142,25 +197,14 @@ label:hover { margin-right: 10px; width: 120px; padding: 6px 10px; - border: 1px solid #dadce0; - border-radius: 6px; - background: white; - font-size: 13px; - cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .style-select:hover { - border-color: #1e88e5; + border-color: var(--c-primary); box-shadow: 0 2px 6px rgba(30, 136, 229, 0.2); } -.style-select:focus { - outline: none; - border-color: #1e88e5; - box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1); -} +/* focus 规则见与 .upload-input:focus 的组合声明 */ .msg-view { position: absolute; @@ -186,22 +230,14 @@ label:hover { .msg-ok-btn { padding: 10px 24px; margin: 0 8px; - background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%); + background: var(--grad-primary); color: white; - border: none; - border-radius: 6px; - cursor: pointer; font-size: 14px; - font-weight: 500; - transition: all 0.2s ease; - box-shadow: 0 2px 6px rgba(30, 136, 229, 0.3); + box-shadow: var(--shadow-primary-2); min-width: 80px; } -.msg-ok-btn:hover { - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(30, 136, 229, 0.4); -} +.msg-ok-btn:hover { box-shadow: var(--shadow-primary-4); } .msg-ok-btn:active { transform: translateY(0); @@ -214,7 +250,9 @@ label:hover { border-radius: 10px; } .note-mpcard-content { - display: flex; + display: grid; + grid-auto-flow: column; + align-items: center; } .note-mpcard-headimg { border: none !important; @@ -246,11 +284,10 @@ label:hover { } .loading-wrapper { - display: flex; + display: grid; width: 100%; height: 100%; - align-items: center; - justify-content: center; + place-items: center; } .loading-spinner { @@ -276,23 +313,38 @@ label:hover { /* =========================================================== */ .toolbar-line { - display: flex; + display: grid; + grid-auto-flow: column; align-items: center; gap: 12px; padding: 8px 12px; background: white; border-radius: 6px; margin: 8px 10px; - box-shadow: 0 1px 3px rgba(0,0,0,0.08); + box-shadow: var(--shadow-sm); } .toolbar-line.flex-wrap { - flex-wrap: wrap; + grid-auto-flow: row; + grid-template-columns: repeat(auto-fit, minmax(160px, max-content)); } .platform-selector-line { background: linear-gradient(135deg, #fff3e0 0%, #ffffff 100%) !important; - border-left: 4px solid #1e88e5; + border-left: 4px solid var(--c-primary); +} + +/* 平台选择容器:单层 Grid 排列 */ +.platform-chooser-container.platform-chooser-grid { + display: grid; + grid-auto-flow: column; + align-items: center; + gap: 12px; + padding: 8px 12px; + background: white; /* 被 .platform-selector-line 的背景覆写 */ + border-radius: 6px; + margin: 8px 10px; + box-shadow: var(--shadow-sm); } /* =========================================================== */ @@ -301,81 +353,39 @@ label:hover { .platform-select { padding: 6px 12px; - border: 1px solid #dadce0; - border-radius: 6px; - background: white; - font-size: 13px; - cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); min-width: 150px; font-weight: 500; } -.platform-select:hover { - border-color: #1e88e5; -} - -.platform-select:focus { - outline: none; - border-color: #1e88e5; -} - /* =========================================================== */ /* 微信公众号选择器样式 */ /* =========================================================== */ .wechat-select { padding: 6px 12px; - border: 1px solid #dadce0; - border-radius: 6px; - background: white; - font-size: 13px; - cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); min-width: 200px; } -.wechat-select:hover { - border-color: #1e88e5; -} - -.wechat-select:focus { - outline: none; - border-color: #1e88e5; -} - /* =========================================================== */ /* 按钮样式 */ /* =========================================================== */ .toolbar-button { padding: 6px 14px; - background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%); + background: var(--grad-primary); color: white; - border: none; - border-radius: 6px; - cursor: pointer; font-size: 13px; - font-weight: 500; - transition: all 0.2s ease; - box-shadow: 0 2px 6px rgba(30, 136, 229, 0.3); + box-shadow: var(--shadow-primary-2); } -.toolbar-button:hover { - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(30, 136, 229, 0.4); -} +.toolbar-button:hover { box-shadow: var(--shadow-primary-4); } .toolbar-button.purple-gradient { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3); + background: var(--grad-purple); + box-shadow: var(--shadow-purple-2); } -.toolbar-button.purple-gradient:hover { - box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4); -} +.toolbar-button.purple-gradient:hover { box-shadow: var(--shadow-purple-4); } /* =========================================================== */ /* 分隔线样式 */ @@ -384,7 +394,7 @@ label:hover { .toolbar-separator { width: 1px; height: 24px; - background: #dadce0; + background: var(--c-border); margin: 0 4px; } @@ -398,8 +408,10 @@ label:hover { } .doc-modal-content { - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: auto auto 1fr; + row-gap: 8px; + min-height: 0; } .doc-modal-title { @@ -413,7 +425,7 @@ label:hover { } .doc-modal-iframe { - flex: 1; + min-height: 0; } /* =========================================================== */ @@ -421,9 +433,10 @@ label:hover { /* =========================================================== */ .setting-help-section { - display: flex; - flex-direction: row; + display: grid; + grid-auto-flow: column; align-items: center; + column-gap: 10px; } .setting-help-title { @@ -449,40 +462,36 @@ label:hover { height: 100%; } -.xhs-preview-container { - display: flex; - flex-direction: column; +.xhs-preview-container:not([style*="display: none"]) { + display: grid !important; + grid-template-rows: auto 1fr auto; height: 100%; - background: linear-gradient(135deg, #f5f7fa 0%, #e8eaf6 100%); + background: var(--grad-xhs-bg); + min-height: 0; } .xhs-page-container { - flex: 1; overflow-y: auto; overflow-x: hidden; - display: flex; - flex-direction: column; - align-items: center; + display: grid; + align-content: start; + justify-content: center; padding: 0px; background: radial-gradient(ellipse at top, rgba(255,255,255,0.1) 0%, transparent 70%); - min-height: 0; /* 允许 flex 子项正确收缩和滚动 */ + min-height: 0; /* 允许子项正确收缩和滚动 */ } /* 小红书单页包裹器:为缩放后的页面预留正确的布局空间 */ .xhs-page-wrapper { - /* 显示尺寸(缩放后):540 × 720 */ - width: 540px; - height: 720px; margin: 0px auto; position: relative; - overflow: visible; + overflow: hidden; } /* 小红书单页样式:实际尺寸 1080×1440,通过 scale 缩放到 540×720 */ .xhs-page { - /* 实际尺寸由 renderPage 设置(1080×1440) */ + /* 实际尺寸与缩放由代码在运行时设置 */ transform-origin: top left; - transform: scale(0.5); /* 540/1080 = 0.5 */ background: white; box-shadow: 0 4px 12px rgba(0,0,0,0.1); border-radius: 8px; @@ -494,26 +503,26 @@ label:hover { } .xhs-top-toolbar { - display: flex; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, max-content)); align-items: center; gap: 12px; padding: 8px 12px; - background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + background: var(--grad-toolbar); border-bottom: 1px solid #e8eaed; - box-shadow: 0 2px 4px rgba(0,0,0,0.04); - flex-wrap: wrap; + box-shadow: var(--shadow-overlay); } .toolbar-label { font-size: 11px; - color: #5f6368; + color: var(--c-text-muted); font-weight: 500; white-space: nowrap; } .xhs-select { padding: 4px 8px; - border: 1px solid #dadce0; + border: 1px solid var(--c-border); border-radius: 4px; background: white; font-size: 11px; @@ -522,20 +531,21 @@ label:hover { } .xhs-select:hover { - border-color: #1e88e5; + border-color: var(--c-primary); } .xhs-select:focus { outline: none; - border-color: #1e88e5; + border-color: var(--c-primary); } .font-size-group { - display: flex; + display: grid; + grid-auto-flow: column; align-items: center; gap: 6px; background: white; - border: 1px solid #dadce0; + border: 1px solid var(--c-border); border-radius: 4px; padding: 2px; } @@ -565,7 +575,8 @@ label:hover { } .xhs-page-navigation { - display: flex; + display: grid; + grid-auto-flow: column; justify-content: center; align-items: center; gap: 16px; @@ -577,20 +588,20 @@ label:hover { .xhs-nav-btn { width: 36px; height: 36px; - border: 1px solid #dadce0; + border: 1px solid var(--c-border); border-radius: 50%; cursor: pointer; font-size: 20px; background: white; color: #5f6368; transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0,0,0,0.08); + box-shadow: var(--shadow-sm); } .xhs-nav-btn:hover { - background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%); + background: var(--grad-primary); color: white; - border-color: #1e88e5; + border-color: var(--c-primary); } .xhs-page-number { @@ -602,26 +613,22 @@ label:hover { } .xhs-bottom-toolbar { - display: flex; + display: grid; + grid-auto-flow: column; justify-content: center; gap: 12px; padding: 12px 16px; - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + background: var(--grad-toolbar-bottom); border-top: 1px solid #e8eaed; box-shadow: 0 -2px 4px rgba(0,0,0,0.04); } .xhs-slice-btn { padding: 8px 20px; - background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%); + background: var(--grad-primary); color: white; - border: none; - border-radius: 6px; - cursor: pointer; font-size: 13px; - font-weight: 500; - transition: all 0.2s ease; - box-shadow: 0 2px 6px rgba(30, 136, 229, 0.3); + box-shadow: var(--shadow-primary-2); } .xhs-slice-btn:hover { @@ -630,7 +637,7 @@ label:hover { } .xhs-slice-btn.secondary { - background: linear-gradient(135deg, #42a5f5 0%, #1e88e5 100%); + background: var(--grad-blue); box-shadow: 0 2px 6px rgba(66, 165, 245, 0.3); } @@ -660,7 +667,8 @@ label:hover { } .xhs-code-container { - display: flex; + display: grid; + grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; margin-bottom: 20px; @@ -704,7 +712,8 @@ label:hover { } .xhs-button-container { - display: flex; + display: grid; + grid-auto-flow: column; justify-content: center; gap: 15px; margin-top: 20px; diff --git a/todolist.md b/todolist.md index 51af40b..477d877 100644 --- a/todolist.md +++ b/todolist.md @@ -103,7 +103,10 @@ SOLVE:obsidian控制台打印信息,定位在哪里阻塞,AI修复。 - 字变大时,一页的内容放不下,重新分页应该会增加页数。但现在重新分页当前页放不下的内容只是被剪掉了。 - 表格显示不完整。 -9. styles.css中有很多冗余。改为grid布局。 +9. styles.css中有很多冗余。改为grid布局。部分完成,这部分需要后面**手动调整**重构。🧶 ♻️ ❇️ 问题:小红书预览布局有问题❓ +10. 新建docs文件夹,把除了README和todolist以外的markdown文件放到docs中。 +✅ +