优化模版样式
This commit is contained in:
parent
a82941ceb1
commit
dd2753a65c
@ -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(() => {
|
||||
|
@ -89,6 +89,7 @@ export const templates: Template[] = [
|
||||
|
||||
// 段落
|
||||
p: {
|
||||
'fontSize': `var(--fontSize)`,
|
||||
'margin': `1.5em 8px`,
|
||||
'letterSpacing': `0.1em`,
|
||||
'color': `hsl(var(--foreground))`,
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user