2 Commits

Author SHA1 Message Date
douboer
52110c6024 update at 2025-10-09 14:46:24 2025-10-09 14:46:24 +08:00
douboer
cab675abcc update at 2025-10-09 13:09:20 2025-10-09 13:09:20 +08:00
16 changed files with 910 additions and 63 deletions

View File

@@ -60,7 +60,7 @@
/* H2左条卡片 */ /* H2左条卡片 */
.note-to-mp h2 { .note-to-mp h2 {
font-size: 1.5rem; font-size: 1.5em;
margin: 2em 0 1.2em; margin: 2em 0 1.2em;
padding: 0.6em 1em; padding: 0.6em 1em;
background: #f5f7fa; background: #f5f7fa;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -22,13 +22,14 @@ function parseAspectRatio(ratio: string): { width: number; height: number } {
return { width: 3, height: 4 }; return { width: 3, height: 4 };
} }
const PAGE_PADDING = 40; // 与 renderPage 保持一致的页面内边距
/** /**
* 计算目标页面高度 * 计算目标页面高度
*/ */
function getTargetPageHeight(settings: NMPSettings): number { function getTargetPageHeight(settings: NMPSettings): number {
const ratio = parseAspectRatio(settings.sliceImageAspectRatio); const ratio = parseAspectRatio(settings.sliceImageAspectRatio);
const height = Math.round((settings.sliceImageWidth * ratio.height) / ratio.width); const height = Math.round((settings.sliceImageWidth * ratio.height) / ratio.width);
console.log(`[paginator] 计算页面高度: 宽度=${settings.sliceImageWidth}, 比例=${settings.sliceImageAspectRatio} (${ratio.width}:${ratio.height}), 高度=${height}`);
return height; return height;
} }
@@ -57,16 +58,37 @@ export async function paginateArticle(
const pageHeight = getTargetPageHeight(settings); const pageHeight = getTargetPageHeight(settings);
const pageWidth = settings.sliceImageWidth; const pageWidth = settings.sliceImageWidth;
// 创建临时容器用于测量 // 创建临时测量容器:与实际页面一致的宽度与内边距
const measureContainer = document.createElement('div'); const measureHost = document.createElement('div');
measureContainer.style.cssText = ` measureHost.style.cssText = `
position: absolute; position: absolute;
left: -9999px; left: -9999px;
top: 0; top: 0;
width: ${pageWidth}px; width: ${pageWidth}px;
visibility: hidden; visibility: hidden;
box-sizing: border-box;
`; `;
document.body.appendChild(measureContainer); document.body.appendChild(measureHost);
const measurePage = document.createElement('div');
measurePage.className = 'xhs-page';
measurePage.style.boxSizing = 'border-box';
measurePage.style.width = `${pageWidth}px`;
measurePage.style.padding = `${PAGE_PADDING}px`;
measurePage.style.background = 'white';
measurePage.style.position = 'relative';
measureHost.appendChild(measurePage);
const measureContent = document.createElement('div');
measureContent.className = 'xhs-page-content';
measurePage.appendChild(measureContent);
if (articleElement.classList.length > 0) {
measureContent.classList.add(...Array.from(articleElement.classList));
}
const measuredFontSize = window.getComputedStyle(articleElement).fontSize;
if (measuredFontSize) {
measureContent.style.fontSize = measuredFontSize;
}
const pages: PageInfo[] = []; const pages: PageInfo[] = [];
let currentPageContent: Element[] = []; let currentPageContent: Element[] = [];
@@ -79,51 +101,45 @@ export async function paginateArticle(
for (const child of children) { for (const child of children) {
const childClone = child.cloneNode(true) as HTMLElement; const childClone = child.cloneNode(true) as HTMLElement;
measureContainer.innerHTML = ''; measureContent.appendChild(childClone);
measureContainer.appendChild(childClone);
// 等待浏览器完成渲染 await waitForLayout();
await new Promise(resolve => setTimeout(resolve, 10));
const childHeight = childClone.offsetHeight; const totalHeight = measurePage.scrollHeight;
const isIndivisible = isIndivisibleElement(child); const isIndivisible = isIndivisibleElement(child);
const fitsCurrentPage =
totalHeight <= pageHeight ||
(!isIndivisible && totalHeight <= pageHeight * 1.1) ||
currentPageContent.length === 0;
// 判断是否需要换页 if (fitsCurrentPage) {
if (currentPageHeight + childHeight > pageHeight && currentPageContent.length > 0) {
// 如果是不可分割元素且加入后会超出,先保存当前页
if (isIndivisible) {
pages.push({
index: pageIndex++,
content: wrapPageContent(currentPageContent),
height: currentPageHeight
});
currentPageContent = [child];
currentPageHeight = childHeight;
} else {
// 可分割元素(段落等),尝试加入当前页
if (currentPageHeight + childHeight <= pageHeight * 1.1) {
// 允许 10% 的溢出容差
currentPageContent.push(child);
currentPageHeight += childHeight;
} else {
// 超出太多,换页
pages.push({
index: pageIndex++,
content: wrapPageContent(currentPageContent),
height: currentPageHeight
});
currentPageContent = [child];
currentPageHeight = childHeight;
}
}
} else {
// 加入当前页
currentPageContent.push(child); currentPageContent.push(child);
currentPageHeight += childHeight; currentPageHeight = totalHeight;
continue;
} }
// 当前页已放不下:移除刚刚加入的克隆节点
measureContent.removeChild(childClone);
await waitForLayout();
if (currentPageContent.length > 0) {
pages.push({
index: pageIndex++,
content: wrapPageContent(currentPageContent),
height: currentPageHeight
});
}
currentPageContent = [child];
measureContent.innerHTML = '';
const firstClone = child.cloneNode(true) as HTMLElement;
measureContent.appendChild(firstClone);
await waitForLayout();
currentPageHeight = measurePage.scrollHeight;
// 不可分割元素即使超过高度也直接保留在新页
} }
// 保存最后一页
if (currentPageContent.length > 0) { if (currentPageContent.length > 0) {
pages.push({ pages.push({
index: pageIndex, index: pageIndex,
@@ -132,12 +148,15 @@ export async function paginateArticle(
}); });
} }
// 清理临时容器 document.body.removeChild(measureHost);
document.body.removeChild(measureContainer);
return pages; return pages;
} }
async function waitForLayout(): Promise<void> {
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
}
/** /**
* 包装页面内容为完整的 HTML * 包装页面内容为完整的 HTML
*/ */
@@ -163,8 +182,6 @@ export function renderPage(
const actualPageWidth = settings.sliceImageWidth; const actualPageWidth = settings.sliceImageWidth;
const actualPageHeight = getTargetPageHeight(settings); const actualPageHeight = getTargetPageHeight(settings);
console.log(`[renderPage] 渲染页面: 宽=${actualPageWidth}, 高=${actualPageHeight}`);
container.innerHTML = ''; container.innerHTML = '';
// 直接设置为实际尺寸,用于切图 // 直接设置为实际尺寸,用于切图
@@ -174,7 +191,7 @@ export function renderPage(
height: ${actualPageHeight}px; height: ${actualPageHeight}px;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
padding: 40px; padding: ${PAGE_PADDING}px;
background: white; background: white;
`; `;

View File

@@ -37,7 +37,8 @@ export class XiaohongshuPreview {
pageContainer!: HTMLDivElement; pageContainer!: HTMLDivElement;
bottomToolbar!: HTMLDivElement; bottomToolbar!: HTMLDivElement;
pageNavigation!: HTMLDivElement; pageNavigation!: HTMLDivElement;
pageNumberDisplay!: HTMLSpanElement; pageNumberInput!: HTMLInputElement;
pageTotalLabel!: HTMLSpanElement;
styleEl: HTMLStyleElement | null = null; // 主题样式注入节点 styleEl: HTMLStyleElement | null = null; // 主题样式注入节点
currentThemeClass: string = ''; currentThemeClass: string = '';
@@ -167,7 +168,27 @@ export class XiaohongshuPreview {
const prevBtn = this.pageNavigation.createEl('button', { text: '', cls: 'xhs-nav-btn' }); const prevBtn = this.pageNavigation.createEl('button', { text: '', cls: 'xhs-nav-btn' });
prevBtn.onclick = () => this.previousPage(); prevBtn.onclick = () => this.previousPage();
this.pageNumberDisplay = this.pageNavigation.createEl('span', { text: '1/1', cls: 'xhs-page-number' }); const indicator = this.pageNavigation.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 = this.pageNavigation.createEl('button', { text: '', cls: 'xhs-nav-btn' }); const nextBtn = this.pageNavigation.createEl('button', { text: '', cls: 'xhs-nav-btn' });
nextBtn.onclick = () => this.nextPage(); nextBtn.onclick = () => this.nextPage();
@@ -199,15 +220,21 @@ export class XiaohongshuPreview {
const tempContainer = document.createElement('div'); const tempContainer = document.createElement('div');
tempContainer.innerHTML = articleHTML; tempContainer.innerHTML = articleHTML;
tempContainer.style.width = `${this.settings.sliceImageWidth}px`; tempContainer.style.width = `${this.settings.sliceImageWidth}px`;
tempContainer.classList.add('note-to-mp');
if (this.currentThemeClass) {
tempContainer.classList.add(this.currentThemeClass);
}
tempContainer.style.fontSize = `${this.currentFontSize}px`;
document.body.appendChild(tempContainer); document.body.appendChild(tempContainer);
try { try {
// 在分页前先应用主题与高亮,确保测量使用正确样式
this.applyThemeCSS();
this.pages = await paginateArticle(tempContainer, this.settings); this.pages = await paginateArticle(tempContainer, this.settings);
new Notice(`分页完成:共 ${this.pages.length}`); new Notice(`分页完成:共 ${this.pages.length}`);
this.currentPageIndex = 0; this.currentPageIndex = 0;
// 初次渲染时应用当前主题 // 初次渲染时应用当前主题
this.applyThemeCSS();
this.renderCurrentPage(); this.renderCurrentPage();
} finally { } finally {
document.body.removeChild(tempContainer); document.body.removeChild(tempContainer);
@@ -239,7 +266,47 @@ export class XiaohongshuPreview {
this.applyFontSettings(pageElement); this.applyFontSettings(pageElement);
// 更新页码显示 // 更新页码显示
this.pageNumberDisplay.innerText = `${this.currentPageIndex + 1}/${this.pages.length}`; this.updatePageNumberDisplay();
}
private updatePageNumberDisplay(): void {
if (!this.pageNumberInput || !this.pageTotalLabel) return;
const total = this.pages.length;
if (total === 0) {
this.pageNumberInput.value = '0';
this.pageTotalLabel.innerText = '/0';
return;
}
const current = Math.min(this.currentPageIndex + 1, total);
this.pageNumberInput.value = String(current);
this.pageTotalLabel.innerText = `/${total}`;
}
private handlePageNumberInput(): void {
if (!this.pageNumberInput) return;
const total = this.pages.length;
if (total === 0) {
this.pageNumberInput.value = '0';
if (this.pageTotalLabel) this.pageTotalLabel.innerText = '/0';
return;
}
const raw = this.pageNumberInput.value.trim();
if (raw.length === 0) {
this.updatePageNumberDisplay();
return;
}
const parsed = parseInt(raw, 10);
if (!Number.isFinite(parsed)) {
this.updatePageNumberDisplay();
return;
}
const target = Math.min(Math.max(parsed, 1), total) - 1;
if (target !== this.currentPageIndex) {
this.currentPageIndex = target;
this.renderCurrentPage();
} else {
this.updatePageNumberDisplay();
}
} }
/** /**
@@ -453,7 +520,8 @@ export class XiaohongshuPreview {
this.pageContainer = null as any; this.pageContainer = null as any;
this.bottomToolbar = null as any; this.bottomToolbar = null as any;
this.pageNavigation = null as any; this.pageNavigation = null as any;
this.pageNumberDisplay = null as any; this.pageNumberInput = null as any;
this.pageTotalLabel = null as any;
this.pages = []; this.pages = [];
this.currentFile = null; this.currentFile = null;
this.styleEl = null; this.styleEl = null;

View File

@@ -604,12 +604,38 @@ label:hover { color: var(--c-primary); }
border-color: var(--c-primary); border-color: var(--c-primary);
} }
.xhs-page-number { .xhs-page-indicator {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px; font-size: 14px;
min-width: 50px;
text-align: center;
color: #202124;
font-weight: 500; font-weight: 500;
color: #202124;
}
.xhs-page-number-input {
width: 56px;
padding: 4px 6px;
text-align: center;
border: 1px solid var(--c-border);
border-radius: 6px;
background: white;
color: inherit;
font: inherit;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.08);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.xhs-page-number-input:focus {
outline: none;
border-color: var(--c-primary);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.15);
}
.xhs-page-number-total {
font-size: 14px;
color: #5f6368;
user-select: none;
} }
.xhs-bottom-toolbar { .xhs-bottom-toolbar {

View File

@@ -95,7 +95,8 @@ SOLVEobsidian控制台打印信息定位在哪里阻塞AI修复。
7. 小红书模式,页面渲染使用选择的主题。参考微信公众号模式进行渲染。 7. 小红书模式,页面渲染使用选择的主题。参考微信公众号模式进行渲染。
8. 需求:主题、字体、字大小变化时,需要重新分页 8. 需求:小红书模式下html渲染后按照previewWidth × previewHeight 的预览窗口尺寸切割,内容不能丢失
主题、字体、字大小变化时,需要进行重新分页,保持内容不丢失。
去掉主题设置,使用全局主题设置。(❗️先简化,后续小红书和微信公众号应主题应该需要独立开。) 去掉主题设置,使用全局主题设置。(❗️先简化,后续小红书和微信公众号应主题应该需要独立开。)
去掉字体设置,使用主题字体。 去掉字体设置,使用主题字体。
字体大小支持直接编辑。 字体大小支持直接编辑。
@@ -110,3 +111,9 @@ SOLVEobsidian控制台打印信息定位在哪里阻塞AI修复。
## 经验
1. 在不确定AI是否理解或者需求是否准确的情况下先用chat模式提问看回答确定AI理解是否准确。
尤其对于较大规模的重构需求,这点很重要‼️ 。

View File

@@ -1,3 +1,3 @@
{ {
"1.3.0": "1.4.5" "1.3.6": "1.4.5"
} }

729
x Normal file
View File

@@ -0,0 +1,729 @@
/* styles.css — 全局样式表,用于渲染及导出样式。 */
/* =========================================================== */
/* 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: var(--c-bg);
}
/* 预览内部平台容器需要可伸缩: */
.wechat-preview-container:not([style*="display: none"]),
.xiaohongshu-preview-container:not([style*="display: none"]) {
flex: 1;
display: grid !important;
grid-template-rows: auto 1fr;
min-height: 0; /* 允许内部滚动区域正确计算高度 */
}
.render-div {
overflow-y: auto;
padding: 10px;
-webkit-user-select: text;
user-select: text;
min-height: 0;
}
/* 文章包裹:模拟公众号编辑器阅读宽度 */
.wechat-article-wrapper {
max-width: 720px;
margin: 0 auto;
padding: 12px 18px 80px 18px; /* 底部留白方便滚动到底部操作 */
box-sizing: border-box;
}
/* 若内部 section.note-to-mp 主题没有撑开,确保文本可见基色 */
.wechat-article-wrapper .note-to-mp {
background: transparent;
}
.preview-toolbar {
display: grid;
position: relative;
grid-template-columns: repeat(auto-fit, minmax(160px, max-content));
gap: 12px;
padding: 8px 12px;
align-items: center;
min-height: auto;
border-bottom: 1px solid #e8eaed;
background: var(--grad-toolbar);
box-shadow: var(--shadow-overlay);
}
.copy-button {
margin-right: 10px;
padding: 6px 14px;
background: var(--grad-primary);
color: white;
font-size: 13px;
box-shadow: var(--shadow-primary-2);
}
.copy-button:hover { box-shadow: var(--shadow-primary-4); }
.refresh-button {
margin-right: 10px;
padding: 6px 14px;
background: var(--grad-purple);
color: white;
font-size: 13px;
box-shadow: var(--shadow-purple-2);
}
.refresh-button:hover { box-shadow: var(--shadow-purple-4); }
.upload-input {
margin-left: 10px;
padding: 6px 10px;
border: 1px solid var(--c-border);
border-radius: 6px;
font-size: 13px;
transition: all 0.2s ease;
}
.upload-input[type="file"] {
cursor: pointer;
}
.upload-input:focus,
.style-select:focus {
outline: none;
border-color: var(--c-primary);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1);
}
/* 单选按钮样式 */
.input-style[type="radio"] {
width: 16px;
height: 16px;
margin: 0 6px 0 0;
cursor: pointer;
accent-color: var(--c-primary);
}
/* Label 标签样式 */
label {
font-size: 13px;
color: var(--c-text-muted);
cursor: pointer;
user-select: none;
transition: color 0.2s ease;
}
label:hover { color: var(--c-primary); }
.style-label {
margin-right: 10px;
font-size: 13px;
color: var(--c-text-muted);
font-weight: 500;
white-space: nowrap;
}
.style-select {
margin-right: 10px;
width: 120px;
padding: 6px 10px;
}
.style-select:hover {
border-color: var(--c-primary);
box-shadow: 0 2px 6px rgba(30, 136, 229, 0.2);
}
/* focus 规则见与 .upload-input:focus 的组合声明 */
.msg-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--background-primary);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 18px;
z-index: 9999;
display: none;
}
.msg-title {
margin-bottom: 20px;
max-width: 90%;
}
.msg-ok-btn {
padding: 10px 24px;
margin: 0 8px;
background: var(--grad-primary);
color: white;
font-size: 14px;
box-shadow: var(--shadow-primary-2);
min-width: 80px;
}
.msg-ok-btn:hover { box-shadow: var(--shadow-primary-4); }
.msg-ok-btn:active {
transform: translateY(0);
}
.note-mpcard-wrapper {
margin: 20px 20px;
background-color: rgb(250, 250, 250);
padding: 10px 20px;
border-radius: 10px;
}
.note-mpcard-content {
display: grid;
grid-auto-flow: column;
align-items: center;
}
.note-mpcard-headimg {
border: none !important;
border-radius: 27px !important;
box-shadow: none !important;
width: 54px !important;
height: 54px !important;
margin: 0 !important;
}
.note-mpcard-info {
margin-left: 10px;
}
.note-mpcard-nickname {
font-size: 17px;
font-weight: 500;
color: rgba(0, 0, 0, 0.9);
}
.note-mpcard-signature {
font-size: 14px;
color: rgba(0, 0, 0, 0.55);
}
.note-mpcard-foot {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #ececec;
font-size: 14px;
color: rgba(0, 0, 0, 0.3);
}
.loading-wrapper {
display: grid;
width: 100%;
height: 100%;
place-items: center;
}
.loading-spinner {
width: 50px; /* 可调整大小 */
height: 50px;
border: 4px solid #fcd6ff; /* 底色,浅灰 */
border-top: 4px solid #bb0cdf; /* 主色,蓝色顶部产生旋转感 */
border-radius: 50%; /* 圆形 */
animation: spin 1s linear infinite; /* 旋转动画 */
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* =========================================================== */
/* Toolbar 行样式 */
/* =========================================================== */
.toolbar-line {
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: white;
border-radius: 6px;
margin: 8px 10px;
box-shadow: var(--shadow-sm);
}
.toolbar-line.flex-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 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);
}
/* =========================================================== */
/* 平台选择器样式 */
/* =========================================================== */
.platform-select {
padding: 6px 12px;
min-width: 150px;
font-weight: 500;
}
/* =========================================================== */
/* 微信公众号选择器样式 */
/* =========================================================== */
.wechat-select {
padding: 6px 12px;
min-width: 200px;
}
/* =========================================================== */
/* 按钮样式 */
/* =========================================================== */
.toolbar-button {
padding: 6px 14px;
background: var(--grad-primary);
color: white;
font-size: 13px;
box-shadow: var(--shadow-primary-2);
}
.toolbar-button:hover { box-shadow: var(--shadow-primary-4); }
.toolbar-button.purple-gradient {
background: var(--grad-purple);
box-shadow: var(--shadow-purple-2);
}
.toolbar-button.purple-gradient:hover { box-shadow: var(--shadow-purple-4); }
/* =========================================================== */
/* 分隔线样式 */
/* =========================================================== */
.toolbar-separator {
width: 1px;
height: 24px;
background: var(--c-border);
margin: 0 4px;
}
/* =========================================================== */
/* Doc Modal 样式 */
/* =========================================================== */
.doc-modal {
width: 640px;
height: 720px;
}
.doc-modal-content {
display: grid;
grid-template-rows: auto auto 1fr;
row-gap: 8px;
min-height: 0;
}
.doc-modal-title {
margin-top: 0.5em;
}
.doc-modal-desc {
margin-bottom: 1em;
-webkit-user-select: text;
user-select: text;
}
.doc-modal-iframe {
min-height: 0;
}
/* =========================================================== */
/* Setting Tab 帮助文档样式 */
/* =========================================================== */
.setting-help-section {
display: grid;
grid-auto-flow: column;
align-items: center;
column-gap: 10px;
}
.setting-help-title {
margin-right: 10px;
}
/* =========================================================== */
/* Xiaohongshu WebView 样式 */
/* =========================================================== */
.xhs-webview {
display: none;
width: 1200px;
height: 800px;
}
/* =========================================================== */
/* Xiaohongshu Preview View 样式 */
/* =========================================================== */
.xiaohongshu-preview-container {
width: 100%;
height: 100%;
}
.xhs-preview-container:not([style*="display: none"]) {
display: grid !important;
grid-template-rows: auto 1fr auto;
height: 100%;
background: var(--grad-xhs-bg);
min-height: 0;
}
.xhs-page-container {
overflow-y: auto;
overflow-x: hidden;
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; /* 允许子项正确收缩和滚动 */
}
/* 小红书单页包裹器:为缩放后的页面预留正确的布局空间 */
.xhs-page-wrapper {
/* 显示尺寸缩放后540 × 720 */
width: 540px;
height: 720px;
margin: 0px auto;
position: relative;
overflow: visible;
}
/* 小红书单页样式:实际尺寸 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;
}
.xhs-page img {
max-width: 100%;
height: auto;
}
.xhs-top-toolbar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, max-content));
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--grad-toolbar);
border-bottom: 1px solid #e8eaed;
box-shadow: var(--shadow-overlay);
}
.toolbar-label {
font-size: 11px;
color: var(--c-text-muted);
font-weight: 500;
white-space: nowrap;
}
.xhs-select {
padding: 4px 8px;
border: 1px solid var(--c-border);
border-radius: 4px;
background: white;
font-size: 11px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.xhs-select:hover {
border-color: var(--c-primary);
}
.xhs-select:focus {
outline: none;
border-color: var(--c-primary);
}
.font-size-group {
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 6px;
background: white;
border: 1px solid var(--c-border);
border-radius: 4px;
padding: 2px;
}
.font-size-btn {
width: 24px;
height: 24px;
border: none;
background: transparent;
border-radius: 3px;
cursor: pointer;
font-size: 16px;
color: #5f6368;
transition: background 0.2s ease;
}
.font-size-btn:hover {
background: #f1f3f4;
}
.font-size-display {
min-width: 24px;
text-align: center;
font-size: 12px;
color: #202124;
font-weight: 500;
}
.xhs-page-navigation {
display: grid;
grid-auto-flow: column;
justify-content: center;
align-items: center;
gap: 16px;
padding: 12px;
background: white;
border-bottom: 1px solid #e8eaed;
}
.xhs-nav-btn {
width: 36px;
height: 36px;
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: var(--shadow-sm);
}
.xhs-nav-btn:hover {
background: var(--grad-primary);
color: white;
border-color: var(--c-primary);
}
.xhs-page-number {
font-size: 14px;
min-width: 50px;
text-align: center;
color: #202124;
font-weight: 500;
}
.xhs-bottom-toolbar {
display: grid;
grid-auto-flow: column;
justify-content: center;
gap: 12px;
padding: 12px 16px;
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: var(--grad-primary);
color: white;
font-size: 13px;
box-shadow: var(--shadow-primary-2);
}
.xhs-slice-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(30, 136, 229, 0.4);
}
.xhs-slice-btn.secondary {
background: var(--grad-blue);
box-shadow: 0 2px 6px rgba(66, 165, 245, 0.3);
}
.xhs-slice-btn.secondary:hover {
box-shadow: 0 4px 12px rgba(66, 165, 245, 0.4);
}
/* =========================================================== */
/* Xiaohongshu Login Modal 样式 */
/* =========================================================== */
.xiaohongshu-login-modal {
width: 400px;
padding: 20px;
}
.xhs-login-title {
text-align: center;
margin-bottom: 20px;
color: #ff4757;
}
.xhs-login-desc {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.xhs-code-container {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.xhs-code-label {
min-width: 80px;
}
.xhs-code-input-wrapper {
flex: 1;
}
.xhs-input-full {
width: 100%;
font-size: 16px;
}
.xhs-send-code-btn {
min-width: 120px;
margin-left: 10px;
}
.xhs-status-message {
min-height: 30px;
margin-bottom: 20px;
text-align: center;
font-size: 14px;
}
.xhs-status-message.success {
color: #27ae60;
}
.xhs-status-message.error {
color: #e74c3c;
}
.xhs-status-message.info {
color: #3498db;
}
.xhs-button-container {
display: grid;
grid-auto-flow: column;
justify-content: center;
gap: 15px;
margin-top: 20px;
}
.xhs-login-btn {
min-width: 100px;
}