优化模版样式
This commit is contained in:
parent
a82941ceb1
commit
dd2753a65c
@ -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(() => {
|
||||||
|
@ -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))`,
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user