优化模版样式

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: {
...(template?.options?.block?.p || {}),
...(styleOptions.block?.p || {}),
fontSize: styleOptions.block?.p?.fontSize || template?.options?.block?.p?.fontSize || '15px',
lineHeight: styleOptions.block?.p?.lineHeight || template?.options?.block?.p?.lineHeight || 2,
letterSpacing: styleOptions.block?.p?.letterSpacing || template?.options?.block?.p?.letterSpacing || '0.1em'
fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px',
lineHeight: styleOptions.base?.lineHeight || template?.options?.base?.lineHeight || 2
},
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: {
...(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)
if (!template?.transform) return html
try {
@ -202,84 +217,28 @@ export default function WechatEditor() {
}
}, [toast])
// 渲染预览内容
const renderPreview = useCallback(() => {
const content = getPreviewContent()
return (
<div
className="preview-content"
dangerouslySetInnerHTML={{ __html: content }}
/>
)
}, [getPreviewContent])
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 {
// 创建临时容器来处理样式
const htmlContent = getPreviewContent()
const tempDiv = document.createElement('div')
tempDiv.innerHTML = previewContent.innerHTML
// 获取当前模板
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)'
}
tempDiv.innerHTML = htmlContent
const plainText = tempDiv.textContent || tempDiv.innerText
// 替换所有元素中的 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([
new ClipboardItem({
'text/html': new Blob([previewContent.innerHTML], { type: 'text/html' }),
'text/plain': new Blob([previewContent.innerText], { type: 'text/plain' })
'text/html': new Blob([htmlContent], { type: 'text/html' }),
'text/plain': new Blob([plainText], { type: 'text/plain' })
})
])
@ -291,7 +250,7 @@ export default function WechatEditor() {
} catch (err) {
console.error('Copy error:', err)
try {
await navigator.clipboard.writeText(previewContent.innerText)
await navigator.clipboard.writeText(previewContent)
toast({
title: "复制成功",
description: "已复制预览内容(仅文本)",
@ -306,7 +265,7 @@ export default function WechatEditor() {
})
}
}
}, [previewRef, selectedTemplate, styleOptions, toast])
}, [previewRef, toast, getPreviewContent])
// 检测是否为移动设备
const isMobile = useCallback(() => {

View File

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

View File

@ -17,10 +17,7 @@ function cssPropertiesToString(style: StyleOptions = {}): string {
// 将基础样式选项转换为 CSS 字符串
function baseStylesToString(base: RendererOptions['base'] = {}): string {
const styles: string[] = []
if (base.primaryColor) {
styles.push(`--md-primary-color: ${base.primaryColor}`)
}
if (base.lineHeight) {
styles.push(`line-height: ${base.lineHeight}`)
}
@ -84,11 +81,10 @@ const defaultOptions: RendererOptions = {
}
export function convertToWechat(markdown: string, options: RendererOptions = defaultOptions): string {
// 创建渲染器
const customRenderer = new marked.Renderer()
const renderer = new marked.Renderer()
// 继承基础渲染器
Object.setPrototypeOf(customRenderer, baseRenderer)
Object.setPrototypeOf(renderer, baseRenderer)
// 合并选项
const mergedOptions = {
@ -97,7 +93,7 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
inline: { ...defaultOptions.inline, ...options.inline }
}
customRenderer.heading = function({ text, depth }: Tokens.Heading) {
renderer.heading = function({ text, depth }: Tokens.Heading) {
const style = {
...mergedOptions.block?.[`h${depth}`],
color: mergedOptions.base?.themeColor, // 使用主题颜色
@ -105,78 +101,77 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
}
const styleStr = cssPropertiesToString(style)
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}>`
}
customRenderer.paragraph = function({ text }: Tokens.Paragraph) {
const style = mergedOptions.block?.p
renderer.paragraph = function({ text }: Tokens.Paragraph) {
const style = mergedOptions.block?.p || {}
const styleStr = cssPropertiesToString(style)
const tokens = marked.Lexer.lexInline(text)
const content = marked.Parser.parseInline(tokens, { renderer: customRenderer })
return `<p${styleStr ? ` style="${styleStr}"` : ''}>${content}</p>`
const content = marked.Parser.parseInline(tokens, { renderer })
return `<p style="${styleStr}">${content}</p>`
}
customRenderer.blockquote = function({ text }: Tokens.Blockquote) {
renderer.blockquote = function({ text }: Tokens.Blockquote) {
const style = mergedOptions.block?.blockquote
const styleStr = cssPropertiesToString(style)
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 styleStr = cssPropertiesToString(style)
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 styleStr = cssPropertiesToString(style)
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 styleStr = cssPropertiesToString(style)
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 styleStr = cssPropertiesToString(style)
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 styleStr = cssPropertiesToString(style)
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 styleStr = cssPropertiesToString(style)
return `<img src="${href}"${title ? ` title="${title}"` : ''} alt="${text}"${styleStr ? ` style="${styleStr}"` : ''} />`
}
// 重写 list 方法
customRenderer.list = function(token: Tokens.List): string {
renderer.list = function(token: Tokens.List): string {
const tag = token.ordered ? 'ol' : 'ul'
try {
const style = mergedOptions.block?.[token.ordered ? 'ol' : 'ul']
const styleStr = cssPropertiesToString(style)
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) {
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 方法
customRenderer.listitem = function(item: Tokens.ListItem) {
renderer.listitem = function(item: Tokens.ListItem) {
try {
const style = mergedOptions.inline?.listitem
const styleStr = cssPropertiesToString(style)
@ -192,7 +187,7 @@ customRenderer.listitem = function(item: Tokens.ListItem) {
// 使用 Lexer 和 Parser 处理剩余的内联标记
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>`
} catch (error) {
@ -201,15 +196,12 @@ customRenderer.listitem = function(item: Tokens.ListItem) {
}
}
// 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
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 {