优化模版
This commit is contained in:
parent
c17182527d
commit
a3c021561c
@ -1,79 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useEditor, EditorContent } from '@tiptap/react'
|
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
|
||||||
import Link from '@tiptap/extension-link'
|
|
||||||
import Image from '@tiptap/extension-image'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { TemplateSelector } from '../template/TemplateSelector'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const templates = [
|
|
||||||
{
|
|
||||||
id: 'wechat',
|
|
||||||
transform: (html: string) => html, // Add your transform logic here
|
|
||||||
styles: 'wechat-specific-styles'
|
|
||||||
}
|
|
||||||
// Add more templates as needed
|
|
||||||
]
|
|
||||||
|
|
||||||
const Editor = () => {
|
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
|
|
||||||
const [preview, setPreview] = useState(false)
|
|
||||||
|
|
||||||
const editor = useEditor({
|
|
||||||
extensions: [
|
|
||||||
StarterKit,
|
|
||||||
Link.configure({
|
|
||||||
openOnClick: false,
|
|
||||||
}),
|
|
||||||
Image,
|
|
||||||
],
|
|
||||||
content: '<p>开始编辑...</p>',
|
|
||||||
editorProps: {
|
|
||||||
attributes: {
|
|
||||||
class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none min-h-[500px]',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleTemplateSelect = (templateId: string) => {
|
|
||||||
setSelectedTemplate(templateId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPreviewContent = () => {
|
|
||||||
if (!editor || !selectedTemplate) return ''
|
|
||||||
const template = templates.find(t => t.id === selectedTemplate)
|
|
||||||
if (!template) return editor.getHTML()
|
|
||||||
return template.transform(editor.getHTML())
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<TemplateSelector onSelect={handleTemplateSelect} />
|
|
||||||
<button
|
|
||||||
onClick={() => setPreview(!preview)}
|
|
||||||
className="px-4 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90"
|
|
||||||
>
|
|
||||||
{preview ? '编辑' : '预览'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border rounded-lg p-4">
|
|
||||||
{preview ? (
|
|
||||||
<div className={cn(
|
|
||||||
"prose max-w-none",
|
|
||||||
selectedTemplate && templates.find(t => t.id === selectedTemplate)?.styles
|
|
||||||
)}>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: getPreviewContent() }} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<EditorContent editor={editor} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Editor
|
|
@ -1,132 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { Editor } from '@tiptap/react'
|
|
||||||
import {
|
|
||||||
Bold,
|
|
||||||
Italic,
|
|
||||||
List,
|
|
||||||
ListOrdered,
|
|
||||||
Quote,
|
|
||||||
Heading1,
|
|
||||||
Heading2,
|
|
||||||
Heading3,
|
|
||||||
Minus,
|
|
||||||
Link,
|
|
||||||
Image,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { Toggle } from '@/components/ui/toggle'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface EditorToolbarProps {
|
|
||||||
editor: Editor | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EditorToolbar({ editor }: EditorToolbarProps) {
|
|
||||||
if (!editor) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="border-b">
|
|
||||||
<div className="flex flex-wrap gap-1 p-1">
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('heading', { level: 1 })}
|
|
||||||
onPressedChange={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
|
||||||
>
|
|
||||||
<Heading1 className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('heading', { level: 2 })}
|
|
||||||
onPressedChange={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
||||||
>
|
|
||||||
<Heading2 className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('heading', { level: 3 })}
|
|
||||||
onPressedChange={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
|
||||||
>
|
|
||||||
<Heading3 className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
|
|
||||||
<Separator orientation="vertical" className="mx-1 h-6" />
|
|
||||||
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('bold')}
|
|
||||||
onPressedChange={() => editor.chain().focus().toggleBold().run()}
|
|
||||||
>
|
|
||||||
<Bold className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('italic')}
|
|
||||||
onPressedChange={() => editor.chain().focus().toggleItalic().run()}
|
|
||||||
>
|
|
||||||
<Italic className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
|
|
||||||
<Separator orientation="vertical" className="mx-1 h-6" />
|
|
||||||
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('bulletList')}
|
|
||||||
onPressedChange={() => editor.chain().focus().toggleBulletList().run()}
|
|
||||||
>
|
|
||||||
<List className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('orderedList')}
|
|
||||||
onPressedChange={() => editor.chain().focus().toggleOrderedList().run()}
|
|
||||||
>
|
|
||||||
<ListOrdered className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
|
|
||||||
<Separator orientation="vertical" className="mx-1 h-6" />
|
|
||||||
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('blockquote')}
|
|
||||||
onPressedChange={() => editor.chain().focus().toggleBlockquote().run()}
|
|
||||||
>
|
|
||||||
<Quote className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('horizontalRule')}
|
|
||||||
onPressedChange={() => editor.chain().focus().setHorizontalRule().run()}
|
|
||||||
>
|
|
||||||
<Minus className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
|
|
||||||
<Separator orientation="vertical" className="mx-1 h-6" />
|
|
||||||
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
pressed={editor.isActive('link')}
|
|
||||||
onPressedChange={() => {
|
|
||||||
const url = window.prompt('URL')
|
|
||||||
if (url) {
|
|
||||||
editor.chain().focus().setLink({ href: url }).run()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
<Toggle
|
|
||||||
size="sm"
|
|
||||||
onPressedChange={() => {
|
|
||||||
const url = window.prompt('Image URL')
|
|
||||||
if (url) {
|
|
||||||
editor.chain().focus().setImage({ src: url }).run()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image className="h-4 w-4" />
|
|
||||||
</Toggle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -219,7 +219,18 @@ export default function WechatEditor() {
|
|||||||
// 替换所有元素中的 CSS 变量
|
// 替换所有元素中的 CSS 变量
|
||||||
const replaceVariables = (element: HTMLElement) => {
|
const replaceVariables = (element: HTMLElement) => {
|
||||||
const style = window.getComputedStyle(element)
|
const style = window.getComputedStyle(element)
|
||||||
const properties = ['color', 'background-color', 'border-color', 'border-left-color']
|
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 => {
|
properties.forEach(prop => {
|
||||||
const value = style.getPropertyValue(prop)
|
const value = style.getPropertyValue(prop)
|
||||||
@ -229,6 +240,9 @@ export default function WechatEditor() {
|
|||||||
finalValue = finalValue.replace(`var(${variable})`, replacement)
|
finalValue = finalValue.replace(`var(${variable})`, replacement)
|
||||||
})
|
})
|
||||||
element.style[prop as any] = finalValue
|
element.style[prop as any] = finalValue
|
||||||
|
} else if (prop === 'font-size' && !element.style.fontSize) {
|
||||||
|
// 如果没有字体大小,从计算样式中获取
|
||||||
|
element.style.fontSize = computedStyle.fontSize
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -250,8 +264,8 @@ export default function WechatEditor() {
|
|||||||
// 创建并写入剪贴板
|
// 创建并写入剪贴板
|
||||||
await navigator.clipboard.write([
|
await navigator.clipboard.write([
|
||||||
new ClipboardItem({
|
new ClipboardItem({
|
||||||
'text/html': new Blob([tempDiv.innerHTML], { type: 'text/html' }),
|
'text/html': new Blob([previewContent.innerHTML], { type: 'text/html' }),
|
||||||
'text/plain': new Blob([tempDiv.innerText], { type: 'text/plain' })
|
'text/plain': new Blob([previewContent.innerText], { type: 'text/plain' })
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { Check, ChevronDown } from "lucide-react"
|
|
||||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { templates } from '@/config/templates'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
export function TemplateSelector({
|
|
||||||
onSelect
|
|
||||||
}: {
|
|
||||||
onSelect: (template: string, subTemplate?: string) => void
|
|
||||||
}) {
|
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<SelectPrimitive.Root
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setSelectedTemplate(value)
|
|
||||||
onSelect(value)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectPrimitive.Trigger className="inline-flex items-center justify-between rounded-md px-3 py-2 text-sm font-medium border border-input hover:bg-accent hover:text-accent-foreground min-w-[120px]">
|
|
||||||
<SelectPrimitive.Value placeholder="选择模板..." />
|
|
||||||
<SelectPrimitive.Icon>
|
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content className="overflow-hidden bg-popover rounded-md border shadow-md">
|
|
||||||
<SelectPrimitive.Viewport className="p-1">
|
|
||||||
{templates.map((template) => (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
key={template.id}
|
|
||||||
value={template.id}
|
|
||||||
className={cn(
|
|
||||||
"relative flex items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectPrimitive.ItemText>{template.name}</SelectPrimitive.ItemText>
|
|
||||||
<SelectPrimitive.ItemIndicator className="absolute left-2 inline-flex items-center">
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
))}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
</SelectPrimitive.Root>
|
|
||||||
|
|
||||||
{selectedTemplate && templates.find(t => t.id === selectedTemplate)?.subTemplates && (
|
|
||||||
<SelectPrimitive.Root onValueChange={(value) => onSelect(selectedTemplate, value)}>
|
|
||||||
<SelectPrimitive.Trigger className="inline-flex items-center justify-between rounded-md px-3 py-2 text-sm font-medium border border-input hover:bg-accent hover:text-accent-foreground min-w-[120px]">
|
|
||||||
<SelectPrimitive.Value placeholder="选择样式..." />
|
|
||||||
<SelectPrimitive.Icon>
|
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content className="overflow-hidden bg-popover rounded-md border shadow-md">
|
|
||||||
<SelectPrimitive.Viewport className="p-1">
|
|
||||||
{templates
|
|
||||||
.find(t => t.id === selectedTemplate)
|
|
||||||
?.subTemplates?.map((subTemplate) => (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
key={subTemplate.id}
|
|
||||||
value={subTemplate.id}
|
|
||||||
className={cn(
|
|
||||||
"relative flex items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectPrimitive.ItemText>{subTemplate.name}</SelectPrimitive.ItemText>
|
|
||||||
<SelectPrimitive.ItemIndicator className="absolute left-2 inline-flex items-center">
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
))}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
</SelectPrimitive.Root>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -53,16 +53,16 @@ export function WechatStylePicker({ value, onSelect }: WechatStylePickerProps) {
|
|||||||
<h1 style={{
|
<h1 style={{
|
||||||
...template.options?.block?.h1,
|
...template.options?.block?.h1,
|
||||||
fontSize: template.options?.block?.h1?.fontSize || '1.2em'
|
fontSize: template.options?.block?.h1?.fontSize || '1.2em'
|
||||||
}}>
|
} as React.CSSProperties}>
|
||||||
标题示例
|
标题示例
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{
|
<p style={{
|
||||||
...template.options?.block?.p,
|
...template.options?.block?.p,
|
||||||
fontSize: template.options?.block?.p?.fontSize || '1em'
|
fontSize: template.options?.block?.p?.fontSize || '1em'
|
||||||
}}>
|
} as React.CSSProperties}>
|
||||||
这是一段示例文本,展示不同样式模板的效果。
|
这是一段示例文本,展示不同样式模板的效果。
|
||||||
</p>
|
</p>
|
||||||
<blockquote style={template.options?.block?.blockquote}>
|
<blockquote style={template.options?.block?.blockquote as React.CSSProperties}>
|
||||||
引用文本示例
|
引用文本示例
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,7 +65,7 @@ export const templates: Template[] = [
|
|||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
color: 'rgba(0,0,0,0.5)',
|
color: 'rgba(0,0,0,0.5)',
|
||||||
background: 'var(--blockquote-background)',
|
background: 'var(--blockquote-background)',
|
||||||
marginBottom: '1em'
|
margin: '0 0 1em 0'
|
||||||
},
|
},
|
||||||
code_pre: {
|
code_pre: {
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
@ -201,7 +201,7 @@ export const templates: Template[] = [
|
|||||||
fontSize: '15px',
|
fontSize: '15px',
|
||||||
color: '#4a5568',
|
color: '#4a5568',
|
||||||
margin: '20px 0',
|
margin: '20px 0',
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.75,
|
||||||
borderLeft: '3px solid #4299e1',
|
borderLeft: '3px solid #4299e1',
|
||||||
paddingLeft: '1em'
|
paddingLeft: '1em'
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user