优化工作台输入与表情,并补充访客 IP 地区
- 输入框改为单层大尺寸 textarea,修复双层边框 - 新增表情选择器(工作台/Widget),插入 Unicode 表情 - 会话记录访客 IP/地区/UA,工作台顶栏与侧栏展示 - 去掉表情面板无用横向滚动条
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { useState } from 'react'
|
||||
import { Popover } from 'antd'
|
||||
import { SmileOutlined } from '@ant-design/icons'
|
||||
|
||||
/** 常用 Unicode 表情(无需额外依赖,文本消息可直接发送) */
|
||||
const EMOJI_GROUPS: { key: string; label: string; emojis: string[] }[] = [
|
||||
{
|
||||
key: 'face',
|
||||
label: '表情',
|
||||
emojis: [
|
||||
'😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊',
|
||||
'😋', '😎', '😍', '😘', '😗', '😙', '😚', '🙂', '🤗', '🤩',
|
||||
'🤔', '🤨', '😐', '😑', '😶', '🙄', '😏', '😣', '😥', '😮',
|
||||
'🤐', '😯', '😪', '😫', '🥱', '😴', '😌', '😛', '😜', '😝',
|
||||
'🤤', '😒', '😓', '😔', '😕', '🙃', '🤑', '😲', '☹️', '🙁',
|
||||
'😖', '😞', '😟', '😤', '😢', '😭', '😦', '😧', '😨', '😩',
|
||||
'🤯', '😬', '😰', '😱', '🥵', '🥶', '😳', '🤪', '😵', '🥴',
|
||||
'😠', '😡', '🤬', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '😇',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'gesture',
|
||||
label: '手势',
|
||||
emojis: [
|
||||
'👍', '👎', '👌', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉',
|
||||
'👆', '👇', '☝️', '✋', '🤚', '🖐', '🖖', '👋', '🤝', '👏',
|
||||
'🙌', '👐', '🤲', '🙏', '💪', '🦾', '✍️', '💅', '🤳', '💃',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'heart',
|
||||
label: '符号',
|
||||
emojis: [
|
||||
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔',
|
||||
'❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️',
|
||||
'✅', '❌', '⭐', '🌟', '💫', '✨', '🔥', '💯', '🎉', '🎊',
|
||||
'💐', '🌹', '🌺', '🌸', '🌼', '🌻', '🍀', '🌈', '☀️', '🌙',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'work',
|
||||
label: '工作',
|
||||
emojis: [
|
||||
'📦', '📋', '📌', '📍', '📎', '🔗', '📝', '✏️', '📂', '📁',
|
||||
'💼', '💻', '🖥️', '📱', '☎️', '📞', '📧', '📨', '📩', '🕐',
|
||||
'⏰', '📅', '📊', '📈', '📉', '💡', '🔍', '🛠️', '⚙️', '🛒',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export interface EmojiPickerProps {
|
||||
onSelect: (emoji: string) => void
|
||||
disabled?: boolean
|
||||
/** 触发按钮 className */
|
||||
className?: string
|
||||
title?: string
|
||||
/** 弹出方向 */
|
||||
placement?: 'topLeft' | 'top' | 'topRight' | 'bottomLeft' | 'bottom' | 'bottomRight'
|
||||
}
|
||||
|
||||
/**
|
||||
* 轻量表情选择器:点击插入 Unicode 表情到输入框,无需后端改造。
|
||||
*/
|
||||
const EmojiPicker = ({
|
||||
onSelect,
|
||||
disabled = false,
|
||||
className = 'w-8 h-8 rounded-md flex items-center justify-center text-neutral-500 hover:bg-neutral-100 disabled:opacity-40',
|
||||
title = '表情',
|
||||
placement = 'topLeft',
|
||||
}: EmojiPickerProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [activeGroup, setActiveGroup] = useState(EMOJI_GROUPS[0].key)
|
||||
|
||||
const group = EMOJI_GROUPS.find(g => g.key === activeGroup) || EMOJI_GROUPS[0]
|
||||
|
||||
const panel = (
|
||||
<div className="w-[272px] max-w-[calc(100vw-32px)] overflow-hidden">
|
||||
<div className="flex gap-1 mb-2 border-b border-neutral-100 pb-2">
|
||||
{EMOJI_GROUPS.map(g => (
|
||||
<button
|
||||
key={g.key}
|
||||
type="button"
|
||||
onClick={() => setActiveGroup(g.key)}
|
||||
className={`flex-1 min-w-0 text-xs py-1 rounded-md transition-colors ${
|
||||
activeGroup === g.key
|
||||
? 'bg-blue-50 text-blue-600 font-medium'
|
||||
: 'text-neutral-500 hover:bg-neutral-50'
|
||||
}`}
|
||||
>
|
||||
{g.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* 仅纵向滚动,禁止横向进度条 */}
|
||||
<div className="grid grid-cols-8 gap-0.5 max-h-[200px] overflow-y-auto overflow-x-hidden overscroll-contain">
|
||||
{group.emojis.map((emoji, i) => (
|
||||
<button
|
||||
key={`${group.key}-${i}`}
|
||||
type="button"
|
||||
className="aspect-square w-full min-w-0 text-base leading-none rounded hover:bg-neutral-100 flex items-center justify-center p-0"
|
||||
onClick={() => {
|
||||
onSelect(emoji)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open && !disabled}
|
||||
onOpenChange={v => { if (!disabled) setOpen(v) }}
|
||||
content={panel}
|
||||
trigger="click"
|
||||
placement={placement}
|
||||
arrow={false}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SmileOutlined />
|
||||
</button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmojiPicker
|
||||
|
||||
/** 在 textarea/input 光标处插入文本,并返回新值与光标位置 */
|
||||
export function insertAtCursor(
|
||||
el: HTMLTextAreaElement | HTMLInputElement | null,
|
||||
value: string,
|
||||
insert: string,
|
||||
): { next: string; cursor: number } {
|
||||
if (!el) {
|
||||
return { next: value + insert, cursor: value.length + insert.length }
|
||||
}
|
||||
const start = el.selectionStart ?? value.length
|
||||
const end = el.selectionEnd ?? value.length
|
||||
const next = value.slice(0, start) + insert + value.slice(end)
|
||||
const cursor = start + insert.length
|
||||
return { next, cursor }
|
||||
}
|
||||
Reference in New Issue
Block a user