支持消息 Markdown 与欢迎语富文本
聊天/离线提示使用 Markdown 安全渲染;欢迎语改为 TipTap 富文本(颜色字号),DOMPurify 白名单清洗后展示。
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
type AvailableAgent, type Channel, type Message, type Session, type SessionEvent,
|
||||
} from '@/services/api'
|
||||
import { ChatImage } from '@/components/common/ImagePreview'
|
||||
import MarkdownBody from '@/components/common/MarkdownBody'
|
||||
import { useAuth } from '@/stores/auth'
|
||||
|
||||
const { RangePicker } = DatePicker
|
||||
@@ -672,7 +673,7 @@ const ChatHistory = () => {
|
||||
{msg.type === 'image' ? (
|
||||
<ChatImage src={msg.content} alt="图片" className="max-w-48 max-h-48" />
|
||||
) : (
|
||||
<p className="text-sm text-neutral-800 m-0 whitespace-pre-wrap break-words">{msg.content}</p>
|
||||
<MarkdownBody className="text-sm text-neutral-800">{msg.content}</MarkdownBody>
|
||||
)}
|
||||
<span className="text-xs text-neutral-400 mt-0.5 block">{formatTime(msg.sent_at)}</span>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import EmojiPicker, { insertAtCursor } from '@/components/common/EmojiPicker'
|
||||
import { ChatImage } from '@/components/common/ImagePreview'
|
||||
import MarkdownBody, { stripMarkdown } from '@/components/common/MarkdownBody'
|
||||
import { useAuth } from '@/stores/auth'
|
||||
import {
|
||||
addSessionNote, claimSession, endSession, getAvailableAgents, getCustomers, getKnowledgeEntries,
|
||||
@@ -327,7 +328,7 @@ const Dashboard = () => {
|
||||
cursor = lastBatchSeq
|
||||
}
|
||||
if (latest && selectedIdRef.current === id) {
|
||||
const preview = latest.type === 'image' ? '[图片]' : latest.content
|
||||
const preview = latest.type === 'image' ? '[图片]' : stripMarkdown(latest.content)
|
||||
setSessions(previous => previous.map(session =>
|
||||
session.id === id
|
||||
? {
|
||||
@@ -360,7 +361,7 @@ const Dashboard = () => {
|
||||
return true
|
||||
}
|
||||
|
||||
const preview = msg.type === 'image' ? '[图片]' : msg.content
|
||||
const preview = msg.type === 'image' ? '[图片]' : stripMarkdown(msg.content)
|
||||
let appended = false
|
||||
setDetail(previous => {
|
||||
if (selectedIdRef.current !== sessionId) return previous
|
||||
@@ -1183,7 +1184,12 @@ const Dashboard = () => {
|
||||
{message.type === 'image' ? (
|
||||
<ChatImage src={message.content} alt="聊天图片" className="max-w-64 max-h-64" />
|
||||
) : (
|
||||
<p className="text-sm leading-normal whitespace-pre-wrap break-words m-0">{message.content}</p>
|
||||
<MarkdownBody
|
||||
tone={isAgent ? 'inverse' : 'default'}
|
||||
className="text-sm"
|
||||
>
|
||||
{message.content}
|
||||
</MarkdownBody>
|
||||
)}
|
||||
</div>
|
||||
<div className={`mt-1 text-xs text-neutral-400 ${isAgent ? 'text-right' : ''}`}>
|
||||
@@ -1289,7 +1295,7 @@ const Dashboard = () => {
|
||||
ref={messageInputRef}
|
||||
rows={3}
|
||||
className="flex-1 min-w-0 min-h-[88px] max-h-[160px] rounded-xl px-3.5 py-3 bg-neutral-50 border border-neutral-200 text-sm leading-6 text-neutral-800 placeholder:text-neutral-400 outline-none resize-y focus:border-[#2563eb] transition-colors"
|
||||
placeholder="输入回复内容… / 调话术,Enter 发送,Shift+Enter 换行"
|
||||
placeholder="支持 Markdown(**加粗** *斜体* 列表 链接)… / 调话术,Enter 发送"
|
||||
value={messageInput}
|
||||
onChange={event => {
|
||||
const v = event.target.value
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
type Channel, type CustomerTag, type StaffUser, type TenantSettings, type WorkHours, type WelcomeSegment,
|
||||
} from '@/services/api'
|
||||
import { useAuth } from '@/stores/auth'
|
||||
import MarkdownEditor from '@/components/common/MarkdownEditor'
|
||||
import RichTextEditor from '@/components/common/RichTextEditor'
|
||||
|
||||
const tagColorPresets: { value: string; label: string; bg: string; color: string }[] = [
|
||||
{ value: 'amber', label: '琥珀', bg: '#fef3c7', color: '#92400e' },
|
||||
@@ -834,9 +836,12 @@ const Settings = () => {
|
||||
}
|
||||
for (let i = 0; i < welcomeSegments.length; i++) {
|
||||
const seg = welcomeSegments[i]
|
||||
if (seg.type === 'text' && !seg.content.trim()) {
|
||||
message.error(`第 ${i + 1} 段文本不能为空`)
|
||||
return
|
||||
if (seg.type === 'text') {
|
||||
const plain = seg.content.replace(/<[^>]+>/g, '').replace(/ /g, ' ').trim()
|
||||
if (!plain) {
|
||||
message.error(`第 ${i + 1} 段文本不能为空`)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (seg.type === 'image' && !seg.content.trim()) {
|
||||
message.error(`第 ${i + 1} 段请上传图片`)
|
||||
@@ -857,7 +862,8 @@ const Settings = () => {
|
||||
<div>
|
||||
<div className="text-sm font-medium text-neutral-800">欢迎语(多段)</div>
|
||||
<p className="text-xs text-neutral-500 m-0 mt-0.5">
|
||||
访客打开窗口且尚无消息时按顺序展示;可混排文本与图片,最多 {MAX_WELCOME_SEGMENTS} 段。
|
||||
访客打开窗口且尚无消息时按顺序展示;可混排富文本与图片,最多 {MAX_WELCOME_SEGMENTS} 段。
|
||||
选中文字可设置<strong className="font-medium text-neutral-600">颜色、字号、加粗</strong>等(仅欢迎语;正常聊天仍为 Markdown)。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -913,14 +919,10 @@ const Settings = () => {
|
||||
</div>
|
||||
</div>
|
||||
{seg.type === 'text' ? (
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
maxLength={500}
|
||||
showCount
|
||||
<RichTextEditor
|
||||
value={seg.content}
|
||||
placeholder="访客打开窗口时展示的文案"
|
||||
onChange={e => {
|
||||
const v = e.target.value
|
||||
placeholder="输入欢迎语,选中文字后设置颜色、字号…"
|
||||
onChange={v => {
|
||||
setWelcomeSegments(prev =>
|
||||
prev.map((s, i) => (i === idx ? { ...s, content: v } : s)),
|
||||
)
|
||||
@@ -1006,8 +1008,18 @@ const Settings = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item name="offline_prompt" label="离线留言提示" rules={[{ max: 500 }]}>
|
||||
<Input.TextArea rows={3} maxLength={500} showCount placeholder="无客服在线时展示" />
|
||||
<Form.Item
|
||||
name="offline_prompt"
|
||||
label="离线留言提示"
|
||||
rules={[{ max: 500 }]}
|
||||
extra="点工具栏设置加粗、列表、链接、表情;可点「预览」查看效果"
|
||||
>
|
||||
<MarkdownEditor
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
showCount
|
||||
placeholder="无客服在线时展示…"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item className="!mb-0">
|
||||
<Button type="primary" htmlType="submit" loading={saving}>保存</Button>
|
||||
|
||||
Reference in New Issue
Block a user