优化模版样式

This commit is contained in:
tianyaxiang 2025-02-01 14:10:15 +08:00
parent a82941ceb1
commit dd2753a65c
3 changed files with 62 additions and 110 deletions

View File

@ -106,18 +106,33 @@ export default function WechatEditor() {
p: { p: {
...(template?.options?.block?.p || {}), ...(template?.options?.block?.p || {}),
...(styleOptions.block?.p || {}), ...(styleOptions.block?.p || {}),
fontSize: styleOptions.block?.p?.fontSize || template?.options?.block?.p?.fontSize || '15px', fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px',
lineHeight: styleOptions.block?.p?.lineHeight || template?.options?.block?.p?.lineHeight || 2, lineHeight: styleOptions.base?.lineHeight || template?.options?.base?.lineHeight || 2
letterSpacing: styleOptions.block?.p?.letterSpacing || template?.options?.block?.p?.letterSpacing || '0.1em' },
ol: {
...(template?.options?.block?.ol || {}),
...(styleOptions.block?.ol || {}),
fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px',
},
ul: {
...(template?.options?.block?.ul || {}),
...(styleOptions.block?.ul || {}),
fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px',
} }
}, },
inline: { inline: {
...(template?.options?.inline || {}), ...(template?.options?.inline || {}),
...(styleOptions.inline || {}) ...(styleOptions.inline || {}),
listitem: {
...(template?.options?.inline?.listitem || {}),
...(styleOptions.inline?.listitem || {}),
fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px',
}
} }
} }
const html = convertToWechat(value, mergedOptions) const html = convertToWechat(value, mergedOptions)
if (!template?.transform) return html if (!template?.transform) return html
try { try {
@ -202,84 +217,28 @@ export default function WechatEditor() {
} }
}, [toast]) }, [toast])
// 渲染预览内容
const renderPreview = useCallback(() => {
const content = getPreviewContent()
return (
<div
className="preview-content"
dangerouslySetInnerHTML={{ __html: content }}
/>
)
}, [getPreviewContent])
const handleCopy = useCallback(async () => { const handleCopy = useCallback(async () => {
const previewContent = previewRef.current?.querySelector('.preview-content') as HTMLElement | null
if (!previewContent) {
toast({
variant: "destructive",
title: "复制失败",
description: "未找到预览内容",
duration: 2000
})
return
}
try { try {
// 创建临时容器来处理样式 const htmlContent = getPreviewContent()
const tempDiv = document.createElement('div') const tempDiv = document.createElement('div')
tempDiv.innerHTML = previewContent.innerHTML tempDiv.innerHTML = htmlContent
// 获取当前模板 const plainText = tempDiv.textContent || tempDiv.innerText
const template = templates.find(t => t.id === selectedTemplate)
// 处理 CSS 变量
const cssVariables = {
'--foreground': styleOptions.base?.themeColor || template?.options.base?.themeColor || '#1a1a1a',
'--background': '#ffffff',
'--muted': '#f1f5f9',
'--muted-foreground': '#64748b',
'--blockquote-background': 'rgba(0, 0, 0, 0.05)'
}
// 替换所有元素中的 CSS 变量
const replaceVariables = (element: HTMLElement) => {
const style = window.getComputedStyle(element)
const properties = ['color', 'background-color', 'border-color', 'border-left-color', 'font-size']
// 获取元素的计算样式
const computedStyle = window.getComputedStyle(element)
// 保留原始样式
const originalStyle = element.getAttribute('style') || ''
// 如果是段落元素,确保应用字体大小
if (element.tagName.toLowerCase() === 'p') {
// alert(element.style.fontSize )
element.style.fontSize = '15px'
}
properties.forEach(prop => {
const value = style.getPropertyValue(prop)
if (value.includes('var(')) {
let finalValue = value
Object.entries(cssVariables).forEach(([variable, replacement]) => {
finalValue = finalValue.replace(`var(${variable})`, replacement)
})
element.style[prop as any] = finalValue
} else if (prop === 'font-size' && !element.style.fontSize) {
// 如果没有字体大小,从计算样式中获取
element.style.fontSize = computedStyle.fontSize
}
})
// 递归处理子元素
Array.from(element.children).forEach(child => {
if (child instanceof HTMLElement) {
replaceVariables(child)
}
})
}
// 处理所有元素
Array.from(tempDiv.children).forEach(child => {
if (child instanceof HTMLElement) {
replaceVariables(child)
}
})
// 创建并写入剪贴板
await navigator.clipboard.write([ await navigator.clipboard.write([
new ClipboardItem({ new ClipboardItem({
'text/html': new Blob([previewContent.innerHTML], { type: 'text/html' }), 'text/html': new Blob([htmlContent], { type: 'text/html' }),
'text/plain': new Blob([previewContent.innerText], { type: 'text/plain' }) 'text/plain': new Blob([plainText], { type: 'text/plain' })
}) })
]) ])
@ -291,7 +250,7 @@ export default function WechatEditor() {
} catch (err) { } catch (err) {
console.error('Copy error:', err) console.error('Copy error:', err)
try { try {
await navigator.clipboard.writeText(previewContent.innerText) await navigator.clipboard.writeText(previewContent)
toast({ toast({
title: "复制成功", title: "复制成功",
description: "已复制预览内容(仅文本)", description: "已复制预览内容(仅文本)",
@ -306,7 +265,7 @@ export default function WechatEditor() {
}) })
} }
} }
}, [previewRef, selectedTemplate, styleOptions, toast]) }, [previewRef, toast, getPreviewContent])
// 检测是否为移动设备 // 检测是否为移动设备
const isMobile = useCallback(() => { const isMobile = useCallback(() => {

View File

@ -89,6 +89,7 @@ export const templates: Template[] = [
// 段落 // 段落
p: { p: {
'fontSize': `var(--fontSize)`,
'margin': `1.5em 8px`, 'margin': `1.5em 8px`,
'letterSpacing': `0.1em`, 'letterSpacing': `0.1em`,
'color': `hsl(var(--foreground))`, 'color': `hsl(var(--foreground))`,

View File

@ -18,9 +18,6 @@ function cssPropertiesToString(style: StyleOptions = {}): string {
function baseStylesToString(base: RendererOptions['base'] = {}): string { function baseStylesToString(base: RendererOptions['base'] = {}): string {
const styles: string[] = [] const styles: string[] = []
if (base.primaryColor) {
styles.push(`--md-primary-color: ${base.primaryColor}`)
}
if (base.lineHeight) { if (base.lineHeight) {
styles.push(`line-height: ${base.lineHeight}`) styles.push(`line-height: ${base.lineHeight}`)
} }
@ -84,11 +81,10 @@ const defaultOptions: RendererOptions = {
} }
export function convertToWechat(markdown: string, options: RendererOptions = defaultOptions): string { export function convertToWechat(markdown: string, options: RendererOptions = defaultOptions): string {
// 创建渲染器 const renderer = new marked.Renderer()
const customRenderer = new marked.Renderer()
// 继承基础渲染器 // 继承基础渲染器
Object.setPrototypeOf(customRenderer, baseRenderer) Object.setPrototypeOf(renderer, baseRenderer)
// 合并选项 // 合并选项
const mergedOptions = { const mergedOptions = {
@ -97,7 +93,7 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
inline: { ...defaultOptions.inline, ...options.inline } inline: { ...defaultOptions.inline, ...options.inline }
} }
customRenderer.heading = function({ text, depth }: Tokens.Heading) { renderer.heading = function({ text, depth }: Tokens.Heading) {
const style = { const style = {
...mergedOptions.block?.[`h${depth}`], ...mergedOptions.block?.[`h${depth}`],
color: mergedOptions.base?.themeColor, // 使用主题颜色 color: mergedOptions.base?.themeColor, // 使用主题颜色
@ -105,78 +101,77 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
} }
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
const tokens = marked.Lexer.lexInline(text) const tokens = marked.Lexer.lexInline(text)
const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) const content = marked.Parser.parseInline(tokens, { renderer })
return `<h${depth}${styleStr ? ` style="${styleStr}"` : ''}>${content}</h${depth}>` return `<h${depth}${styleStr ? ` style="${styleStr}"` : ''}>${content}</h${depth}>`
} }
customRenderer.paragraph = function({ text }: Tokens.Paragraph) { renderer.paragraph = function({ text }: Tokens.Paragraph) {
const style = mergedOptions.block?.p const style = mergedOptions.block?.p || {}
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
const tokens = marked.Lexer.lexInline(text) const tokens = marked.Lexer.lexInline(text)
const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) const content = marked.Parser.parseInline(tokens, { renderer })
return `<p${styleStr ? ` style="${styleStr}"` : ''}>${content}</p>` return `<p style="${styleStr}">${content}</p>`
} }
customRenderer.blockquote = function({ text }: Tokens.Blockquote) { renderer.blockquote = function({ text }: Tokens.Blockquote) {
const style = mergedOptions.block?.blockquote const style = mergedOptions.block?.blockquote
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
return `<blockquote${styleStr ? ` style="${styleStr}"` : ''}>${text}</blockquote>` return `<blockquote${styleStr ? ` style="${styleStr}"` : ''}>${text}</blockquote>`
} }
customRenderer.code = function({ text, lang }: Tokens.Code) { renderer.code = function({ text, lang }: Tokens.Code) {
const style = mergedOptions.block?.code_pre const style = mergedOptions.block?.code_pre
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
return `<pre${styleStr ? ` style="${styleStr}"` : ''}><code class="language-${lang || ''}">${text}</code></pre>` return `<pre${styleStr ? ` style="${styleStr}"` : ''}><code class="language-${lang || ''}">${text}</code></pre>`
} }
customRenderer.codespan = function({ text }: Tokens.Codespan) { renderer.codespan = function({ text }: Tokens.Codespan) {
const style = mergedOptions.inline?.codespan const style = mergedOptions.inline?.codespan
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
return `<code${styleStr ? ` style="${styleStr}"` : ''}>${text}</code>` return `<code${styleStr ? ` style="${styleStr}"` : ''}>${text}</code>`
} }
customRenderer.em = function({ text }: Tokens.Em) { renderer.em = function({ text }: Tokens.Em) {
const style = mergedOptions.inline?.em const style = mergedOptions.inline?.em
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
return `<em${styleStr ? ` style="${styleStr}"` : ''}>${text}</em>` return `<em${styleStr ? ` style="${styleStr}"` : ''}>${text}</em>`
} }
customRenderer.strong = function({ text }: Tokens.Strong) { renderer.strong = function({ text }: Tokens.Strong) {
const style = mergedOptions.inline?.strong const style = mergedOptions.inline?.strong
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
return `<strong${styleStr ? ` style="${styleStr}"` : ''}>${text}</strong>` return `<strong${styleStr ? ` style="${styleStr}"` : ''}>${text}</strong>`
} }
customRenderer.link = function({ href, title, text }: Tokens.Link) { renderer.link = function({ href, title, text }: Tokens.Link) {
const style = mergedOptions.inline?.link const style = mergedOptions.inline?.link
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
return `<a href="${href}"${title ? ` title="${title}"` : ''}${styleStr ? ` style="${styleStr}"` : ''}>${text}</a>` return `<a href="${href}"${title ? ` title="${title}"` : ''}${styleStr ? ` style="${styleStr}"` : ''}>${text}</a>`
} }
customRenderer.image = function({ href, title, text }: Tokens.Image) { renderer.image = function({ href, title, text }: Tokens.Image) {
const style = mergedOptions.block?.image const style = mergedOptions.block?.image
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
return `<img src="${href}"${title ? ` title="${title}"` : ''} alt="${text}"${styleStr ? ` style="${styleStr}"` : ''} />` return `<img src="${href}"${title ? ` title="${title}"` : ''} alt="${text}"${styleStr ? ` style="${styleStr}"` : ''} />`
} }
// 重写 list 方法 // 重写 list 方法
customRenderer.list = function(token: Tokens.List): string { renderer.list = function(token: Tokens.List): string {
const tag = token.ordered ? 'ol' : 'ul' const tag = token.ordered ? 'ol' : 'ul'
try { try {
const style = mergedOptions.block?.[token.ordered ? 'ol' : 'ul'] const style = mergedOptions.block?.[token.ordered ? 'ol' : 'ul']
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
const startAttr = token.ordered && token.start !== 1 ? ` start="${token.start}"` : '' const startAttr = token.ordered && token.start !== 1 ? ` start="${token.start}"` : ''
return `<${tag}${startAttr}${styleStr ? ` style="${styleStr}"` : ''}>${token.items.map(item => customRenderer.listitem(item)).join('')}</${tag}>` return `<${tag}${startAttr}${styleStr ? ` style="${styleStr}"` : ''}>${token.items.map(item => renderer.listitem(item)).join('')}</${tag}>`
} catch (error) { } catch (error) {
console.error(`Error rendering list: ${error}`) console.error(`Error rendering list: ${error}`)
return `<${tag}>${token.items.map(item => customRenderer.listitem(item)).join('')}</${tag}>` return `<${tag}>${token.items.map(item => renderer.listitem(item)).join('')}</${tag}>`
} }
} }
// 重写 listitem 方法 // 重写 listitem 方法
customRenderer.listitem = function(item: Tokens.ListItem) { renderer.listitem = function(item: Tokens.ListItem) {
try { try {
const style = mergedOptions.inline?.listitem const style = mergedOptions.inline?.listitem
const styleStr = cssPropertiesToString(style) const styleStr = cssPropertiesToString(style)
@ -192,7 +187,7 @@ customRenderer.listitem = function(item: Tokens.ListItem) {
// 使用 Lexer 和 Parser 处理剩余的内联标记 // 使用 Lexer 和 Parser 处理剩余的内联标记
const tokens = marked.Lexer.lexInline(itemText) const tokens = marked.Lexer.lexInline(itemText)
const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) const content = marked.Parser.parseInline(tokens, { renderer })
return `<li${styleStr ? ` style="${styleStr}"` : ''}>${content}</li>` return `<li${styleStr ? ` style="${styleStr}"` : ''}>${content}</li>`
} catch (error) { } catch (error) {
@ -201,15 +196,12 @@ customRenderer.listitem = function(item: Tokens.ListItem) {
} }
} }
// Convert Markdown to HTML using the custom renderer // Convert Markdown to HTML using the custom renderer
const html = marked.parse(markdown, { renderer: customRenderer }) as string const html = marked.parse(markdown, { renderer }) as string
// Apply base styles // Apply base styles
const baseStyles = baseStylesToString(mergedOptions.base) const baseStyles = baseStylesToString(mergedOptions.base)
return baseStyles ? `<div style="${baseStyles}">${html}</div>` : html return baseStyles ? `<section style="${baseStyles}">${html}</section>` : html
} }
export function convertToXiaohongshu(markdown: string): string { export function convertToXiaohongshu(markdown: string): string {