import { Button, Col, Row, Space } from 'antd'; import { Alert, Mentions } from 'antd5'; import type { MentionsRef } from 'antd5/es/mentions'; import _ from 'lodash'; import type { CSSProperties } from 'react'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useIntl } from 'umi'; import { AuditContainerContext } from '.'; import { queryUser } from '../server'; import { formatUserRender } from '../utils'; import { replyMessage } from './api'; import lessStyle from './index.less'; const styles: Record< 'root' | 'input' | 'mention' | 'button' | 'alert' | 'insert' | 'alertBody', CSSProperties > = { root: { width: '100%', display: 'flex', alignItems: 'start', justifyContent: 'space-between', }, input: { width: 'calc(100% - 100px)', }, mention: { height: 100, }, button: { marginTop: (100 - 32) / 2, }, alert: { height: '100%', }, insert: { float: 'right', margin: '4px 0 4px 4px', clear: 'both', }, alertBody: { overflow: 'hidden', fontSize: 14, }, }; const formatValue = (nameZh: string, account: string) => { return `${nameZh}(${account})`; }; type SelectedType = { id: number; nameZh: string; account: string; }; export interface ReplyProps { chainId?: string; readOnly?: boolean; auditDetail?: { presetReplyMsg?: string[] }; style?: CSSProperties; onReplied?: () => void; } const Reply = (props: ReplyProps) => { const { chainId, readOnly, auditDetail, style, onReplied } = props; const [replyText, setReplyText] = useState(''); const { formatMessage } = useIntl(); const [loading, setLoading] = useState(false); const [users, setUsers] = useState([]); const [selected, setSelected] = useState([]); const searchRef = useRef(); const [replying, setReplying] = useState(false); const { wrapperRef } = useContext(AuditContainerContext); const scrollBottomRef = useRef(false); const onReply = useCallback(async () => { try { if (_.isEmpty(replyText) || !chainId) return; setReplying(true); const options = Mentions.getMentions(replyText); await replyMessage({ chainId, message: replyText, mentions: options .map((v) => { return selected.find((sv) => { return v.value === formatValue(sv.nameZh, sv.account); }); }) .filter((v) => v), }); onReplied?.(); setReplyText(''); setSelected([]); scrollBottomRef.current = true; } catch (e) { } finally { setReplying(false); } }, [chainId, onReplied, replyText, selected]); const scrollTimeoutRef = useRef(); // 在回复成功后,滚动到底部,保持最新消息可见 useEffect(() => { const ele = wrapperRef?.current; if (scrollBottomRef.current && ele) { clearTimeout(scrollTimeoutRef.current); scrollTimeoutRef.current = setTimeout(() => { ele.scrollTo({ behavior: 'smooth', top: ele.scrollHeight }); }, 300); } scrollBottomRef.current = false; }, [auditDetail, wrapperRef]); const searchUser = useMemo<(key: string) => void>(() => { return _.debounce((key: string) => { if (_.isEmpty(key)) { setUsers([]); return; } queryUser(key, 1) .then(({ data }) => { if (searchRef.current !== key) return; setUsers(data?.list ?? []); }) .finally(() => { if (searchRef.current !== key) return; setLoading(false); }); }, 800); }, []); const onSearch = useCallback( (search: string) => { searchRef.current = search; setLoading(!!search); setUsers([]); searchUser(search); }, [searchUser], ); const onSelect = useCallback( (o: any) => { const { data } = o; if (!selected.find((v) => v.id === data.id)) { setSelected((old) => [...old, data as SelectedType]); } }, [selected], ); const options = useMemo(() => { return users.map((v) => { const { userName, nameEn, nameZh, cnName, id } = v; const nz = nameZh || cnName; const ac = userName || nameEn; const value = formatValue(nz, ac); const label = formatUserRender(v, false, false); const data = { id, nameZh: nz, account: ac }; return { value, label, data }; }); }, [users]); const mentionsRef = useRef(null); const cursorTimeoutRef = useRef(); const onInsert = useCallback( (v: string) => { const text = `${replyText ?? ''}${v}`; setReplyText(text); // 将光标移至末尾 clearTimeout(cursorTimeoutRef.current); cursorTimeoutRef.current = setTimeout(() => { mentionsRef.current?.focus(); const input = mentionsRef.current?.nativeElement.querySelector('textarea'); if (input instanceof HTMLTextAreaElement) { input.setSelectionRange(text.length, text.length); } }, 400); }, [replyText], ); const { presetReplyMsg } = auditDetail ?? {}; const presetReply = useMemo( () => presetReplyMsg?.filter((t) => t?.trim()) ?? [], [presetReplyMsg], ); return (
setSelected([])} ref={mentionsRef} /> {presetReply.length > 0 && ( {presetReply.map((text, i) => ( // eslint-disable-next-line react/no-array-index-key {text}
} type={'success'} style={styles.alert} /> ))} )} ); }; export default Reply;