123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- import { Button, Input, message, Modal, Popover, Radio, Space } from 'antd'
- import React, { useCallback, useEffect, useRef, useState } from 'react'
- import Expression from '@/components/Expression'
- import style from './text.less'
- import sanitizeHtml from 'sanitize-html';
- import { RadioChangeEvent } from 'antd/lib/radio';
- type Props = {
- visible: boolean,
- onCancel: () => void,
- onOK: (props: any, type?: number) => void
- defaultData?: any,
- hdLink?: boolean,
- fansName?: boolean
- }
- const sanitizeConf: any = {
- allowedTags: ['br', 'a'],
- allowedAttributes: { a: ["href", "_href", "data-miniprogram-appid", "data-miniprogram-path"] },
- allowedSchemes: ['http', 'https', 'weixin']
- };
- /**文本弹窗 */
- const WxTextModal = React.memo((props: Props) => {
- const { visible, onCancel, onOK, fansName = true, hdLink = true } = props
- const [value, setValue] = useState<string>('')//存放连接文字
- const [content, setContent] = useState<string>('')//存放发送文字
- const [isShow, setIsShow] = useState<boolean>(false)//互动弹窗
- const [range, setRange] = useState<Range>()//丢失焦点存放焦点位置
- const [ref, setRef] = useState<any>(null)//存放编辑框的实例
- const [aLink, setAlink] = useState<string>('')//存放页面链接
- const [appID, setAppID] = useState<string>('')//存放小程序APPID
- const [path, setPath] = useState<string>('')//存放小程序路径
- const [userId, setUserId] = useState<any>(2)//小程序路径插入userID
- const [textData, setTextData] = useState<any>({ range: null, left: -100, top: -100, text: '' })
- const [isLink, setIsLink] = useState<boolean>(false)
- const [isWxLink, setIsWxLink] = useState<boolean>(false)
- const [isHtml, setIsHtml] = useState<boolean>(false)
- const text = useRef('');
- const [phoneType, setPhoneType] = useState<1 | 2 | 3|4>(3)
- /**
- * 发送处理
- */
- const callback = useCallback(() => {
- if (ref.innerHTML) {
- console.log(ref.innerHTML)
- let textContent: any = sanitizeHtml(ref.innerHTML, sanitizeConf);
- console.log(textContent)
- textContent = textContent.replace(/"/ig, '"')
- .replace(/&/ig, '&')
- .replace(/</ig, '<')
- .replace(/>/ig, '>')
- .replace(/<\s+/g, '<')
- .replace(/[\f\n\r\t\v]/g, '<br/>')//将回车变成br
- .replace(/\ (<br>)?/g, '')
- .replace(/<[/]?myspan[^>]*>/ig, '#')
- .replace(/#<br>/g, '#')
- .replace(/<([a-z]+?)(?:\s+?[^>]*?)?>[\s(<br>)]*?<\/\1>/ig, '<br/>')//替换所有空标签或空标签带<br>的标签为<br>
- .replace(/(\b<div>(<br>)?)|((<br>)?<div>)/ig, '<br/>')//<div>or<br><div>or<div><br>转br
- .replace(/<\/div>/ig, '')//</div>转‘’
- .replace(/_href="\s+/ig, '_href="')//清除href头部空格
- .replace(/href=weixin/ig,'href="weixin')
- .replace(/msgmenuid="/ig,'msgmenuid= "')
- .replace(/"=""/ig,'')
- // .replace(/\s+"/ig, '"')//清除href尾部空格
- .split('<br/>')
- onOK({
- textContent: textContent, indexId: props.defaultData?.indexId //textContent.filter((str)=> str!=='') 注释保留空格和换行
- }, 4)
- } else {
- alert('元素获取失败请复制内容刷新页面')
- message.error('请输入文字')
- }
- handleClose()
- }, [ref])
- /**
- * 互动连接文字
- */
- const textChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
- let v = e.target.value
- setValue(v)
- }, [])
- /**
- * 互动发送文本
- */
- const contentChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
- let v = e.target.value
- setContent(v)
- }, [])
- /**
- * 网页小程序连接地址
- */
- const aLinkChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
- let v = e.target.value
- setAlink(v)
- }, [])
- /**
- * 小程序路径
- */
- const pathChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
- let v = e.target.value
- setPath(v)
- }, [])
- /**
- * 小程序ID
- */
- const appIDChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
- let v = e.target.value
- setAppID(v)
- }, [])
- /**
- * 插入表情
- */
- const getStr = useCallback((str: string, range: Range) => {
- if (range) {
- let selection = window.getSelection()
- selection?.empty()//清空选择range
- selection?.addRange(range)//插入新的range
- document.execCommand('insertHtml', false, str + ' ');
- }
- }, [])
- /**
- * 光标丢失记录位置
- */
- const onBlur = useCallback(() => {
- try {
- let selection = window.getSelection()
- let range = selection?.getRangeAt(0)
- setRange(range)
- } catch (err) {
- }
- }, [text])
- /**
- * 插入粉丝昵称
- */
- const pushName = useCallback(() => {
- let selection = window.getSelection()
- selection?.empty()//清空选择range
- selection?.addRange(range as Range)//插入新的range
- document.execCommand('insertHtml', false, `#粉丝昵称#`);
- selection?.collapseToEnd()//光标插入到末尾
- }, [range])
- /**
- * 插入互动链
- */
- const pushLink = useCallback(() => {
- if (!value || !content) {
- setIsShow(false)
- return
- }
- let selection = window.getSelection()
- selection?.empty()//清空选择range
- selection?.addRange(range as Range)//插入新的range
- if(phoneType === 4){
- document.execCommand('insertHtml', false, `<a href="a href=weixin://bizmsgmenu?msgmenucontent=${content}&msgmenuid=+++">${value}</a>`);
- }else{
- document.execCommand('insertHtml', false, `<a href='weixin://${phoneType === 1 ? 'kefumenu' : 'bizmsgmenu'}?${phoneType === 1 ? `kefumenucontent=${content}` : `msgmenucontent=${content}`}&${phoneType === 1 ? 'kefumenuid=0' : phoneType === 2 ? `msgmenuid= ` : 'msgmenuid=0'}' >${value}</a>`);
- }
- selection?.collapseToEnd()//光标插入到末尾
- setValue('')
- setContent('')
- setIsShow(false)
- }, [range, value, content, phoneType])
- /**
- * 初始光标选中
- */
- useEffect(() => {
- console.log('ref')
- if (ref) {
- ref?.focus()
- }
- }, [ref])
- //复制只取纯文本
- function textPaste(event: any) {
- event.preventDefault();
- let text;
- let clp = (event.originalEvent || event).clipboardData;
- // 兼容chorme或hotfire
- text = (clp.getData('text/plain') || clp.getData('text'))
- .replace(/"/ig, '"')
- .replace(/&/ig, '&')
- .replace(/</ig, '<')
- .replace(/>/ig, '>')
- .replace(/<\s+/g, '<')
- .replace(/href="weixin/ig,'href=weixin')
- text = sanitizeHtml(text, sanitizeConf) || "";
- if (text !== "") {
- document.execCommand('insertHtml', false, text);
- }
- }
- /**
- * 编辑默认内容写入
- */
- useEffect(() => {
- console.log('编辑默认内容写入', props?.defaultData?.textContent)
- if (props?.defaultData?.textContent && ref) {
- let text = '';
- if (Array.isArray(props?.defaultData?.textContent)) {
- props?.defaultData?.textContent?.map((key: any, index: number) => {
- key = key.replace(/href="weixin/ig,'href=weixin')
- if (key.indexOf('<') !== -1) {//假如存在就是原文
- setIsHtml(true)
- }
- text += index !== props?.defaultData?.textContent?.length - 1 ? `${key}<br/>` : key
- })
- } else {
- text = `${props?.defaultData?.textContent}`
- }
- document.execCommand('insertHtml', true, text);
- }
- }, [ref])
- /**切换文本原文 */
- const onHtml = useCallback(() => {
- let str: string = ref.innerHTML
- str = str.replace(/"/ig, '"').replace(/&/ig, '&').replace(/</ig, '<').replace(/>/ig, '>').replace(/<\s+/g, '<').replace(/""/ig,'"').replace(/"="/ig,'')
- if (isHtml) {
- console.log('str1',str)
- setIsHtml(false)
- ref.innerHTML = ''
- ref.innerHTML = str
- } else {
- console.log('str2',str)
- setIsHtml(true)
- ref.innerHTML = ''
- ref.innerText = str.replace(/<br[\/]?>/ig, '\n').replace(/ /ig, ' ')
- }
- }, [ref, text, isHtml])
- //文本选中
- let handleSelectText = useCallback((event: React.SyntheticEvent<HTMLPreElement, Event>) => {
- let selection = window.getSelection ? window.getSelection() : (document.getSelection ? document.getSelection() : ((document as any)?.selection ? (document as any)?.selection.createRange().text : ""))
- let text = selection.toString() || selection.text
- if (text) {//存在文本弹窗
- let range = selection?.getRangeAt(0)
- let { top, left } = range?.getBoundingClientRect()
- setTextData({ range, top, left, text })
- } else {
- handleClose()//关闭弹窗
- }
- }, [])
- //点击设置连接转换输入,清空连接处理
- let handleLink = useCallback((type: number) => {
- console.log('点击设置连接转换输入,清空连接处理')
- switch (type) {
- case 1:
- setIsLink(true)
- break;
- case 2:
- setIsWxLink(true)
- break;
- default:
- let selection = window.getSelection()
- selection?.empty()//清空选择range
- selection?.addRange(textData?.range)//插入新的range
- document.execCommand('unlink')//清除连接
- selection?.collapseToEnd()//光标插入到末尾
- handleClose()//关闭弹窗
- break
- }
- }, [textData])
- //清空数据并关闭弹窗
- let handleClose = useCallback(() => {
- console.log('清空数据并关闭弹窗')
- setTextData({ range: null, left: -100, top: -100, text: '' })
- setIsWxLink(false)
- setIsLink(false)
- setAppID('')
- setAlink('')
- setPath('')
- }, [])
- //ok插入连接数据
- let handleOk = useCallback(() => {
- let selection = window.getSelection()
- selection?.empty()//清空选择range
- selection?.addRange(textData?.range)//插入新的range
- if (isWxLink) {
- if (aLink && appID && path) {
- let Apath = path
- if (userId) {
- if (userId === 1 && path && !path.includes('#USER_ID#')) {
- Apath = Apath + '#USER_ID#'
- } else if (userId === 2 && path && !path.includes('#QC_USER_ID#')) {
- Apath = Apath + '#QC_USER_ID#'
- }
- }
- document.execCommand('insertHTML', true, `<a href='${aLink}' data-miniprogram-appid='${appID}' data-miniprogram-path='${Apath}' >${textData?.text}</a>`);//插入连接
- selection?.collapseToEnd()
- handleClose()
- } else {
- message.error('请填写完整')
- }
- } else {
- if (aLink && aLink.search(/http[s]?:\/\//ig) !== -1) {//
- document.execCommand('createLink', false, aLink);//插入连接
- selection?.collapseToEnd()
- handleClose()
- } else {
- message.error('请填入正确的连接')
- }
- }
- }, [textData, aLink, appID, path, isWxLink, range, userId])
- //处理选中的文本
- return <Modal
- title='编辑文字内容'
- open={visible}
- width={1100}
- onCancel={() => {
- handleClose()
- text.current = ''
- onCancel()
- }}
- onOk={callback}
- destroyOnClose
- >
- <div className={style.box}>
- <div className={style.header}>
- <Space>
- <Expression getStr={getStr} range={range as Range} />
- {
- // fansName && <Button size='small' onClick={pushName}>粉丝昵称</Button>
- }
- {
- hdLink && <Popover
- placement="right"
- title={'新建互动链'}
- open={isShow}
- onOpenChange={(visible) => { setIsShow(visible) }}
- content={<div className={style.popover}>
- <div>
- <label>连接文字:</label>
- <Input.TextArea rows={2} placeholder='互动链显示的文字' onChange={textChange} value={value} />
- </div>
- <div>
- <label>点击发送:</label>
- <Input placeholder='粉丝点击互动链后,自动向公众号发送的消息' onChange={contentChange} value={content} />
- </div>
- <div>
- <label>系 统:</label>
- <Radio.Group
- onChange={(e: RadioChangeEvent) => {
- setPhoneType(e.target.value)
- }}
- value={phoneType}
- >
- <Radio value={3}>通用</Radio>
- <Radio value={1}>安卓1</Radio>
- <Radio value={4}>安卓2</Radio>
- <Radio value={2}>苹果</Radio>
- </Radio.Group>
- </div>
- <div>
- <label>提 示:</label>
- {
- phoneType === 1 ?
- <strong style={{ color: 'red' }}>
- 8.0.9——8.0.11(最新版本),共3个版本均可激活48小时互动!<br />
- 8.0.7及以下版本,点击新蓝链会跳转空页面,无法激活!<br />
- iOS用户点击新蓝链,系统无法模拟用户回复消息!!!
- </strong>
- :
- phoneType === 3 ?
- <strong>
- 原来的篮字方式无法激活48小时互动但所有机型通用
- </strong>
- :phoneType === 4 ?<strong>
- 安卓用户可以看到,ios看不到
- </strong>:<strong>
- 只对IOS有效
- </strong>
- }
- </div>
- <Button onClick={pushLink} type='primary'>确定</Button>
- </div>}
- trigger="click"
- >
- <Button size='small' >互动链</Button>
- </Popover>
- }
- <Button size='small' onClick={onHtml}>文本转译</Button>
- </Space>
- </div>
- <pre
- className={style.editable} //样式
- onBlur={onBlur}//焦点丢失
- contentEditable="true"
- dangerouslySetInnerHTML={{ __html: text.current }}
- onSelect={handleSelectText}//选中事件
- ref={(ref: any) => { setRef(ref) }}
- onKeyDown={(e: React.KeyboardEvent<HTMLPreElement>) => {
- if (e.key === 'Enter') {
- document.execCommand('insertHTML', false, '\n')
- e.preventDefault()
- }
- }}
- onKeyUp={(e: React.KeyboardEvent<HTMLPreElement>) => {
- if (e.key === 'Enter') {
- e.preventDefault()
- }
- }}
- onPaste={textPaste}
- />
- </div>
- <div
- className={style.fixed_pop}
- style={{ left: textData?.left, top: textData?.top + 30, display: textData.left > 0 ? 'flex' : '' }}
- >
- <>
- {isLink && <div className={style.fixed_pop_link}>
- <Input placeholder='输入链接,以http://或https://开头' onChange={aLinkChange} />
- <a onClick={handleOk}>✓</a>
- <a onClick={handleClose}>×</a>
- </div>}
- {
- isWxLink && <div className={style.fixed_pop_wx}>
- <span>
- <Input placeholder='填写小程序AppID,跳转小程序需与当前公众号绑定关联关系' onChange={appIDChange} />
- <Input placeholder='填写小程序路径,例如:pages/index' onChange={pathChange} />
- <Input placeholder='备用网页,以http://或https://开头' onChange={aLinkChange} />
- <div className={style.fixed_pop_wx_radio}>
- <span>路径插入用户:</span>
- <Radio.Group onChange={(e: any) => { setUserId(Number(e.target.value)) }} value={userId} >
- <Radio value={1}>普通用户</Radio>
- <Radio value={2}>趣程用户</Radio>
- <Radio value={0}>否</Radio>
- </Radio.Group>
- </div>
- </span>
- <span>
- <a onClick={handleOk}>✓</a>
- <a onClick={handleClose}>×</a>
- </span>
- </div>
- }
- {!isLink && !isWxLink && <>
- <span onClick={() => { handleLink(1) }}>设置连接</span>
- <span onClick={() => { handleLink(2) }}>设置小程序</span>
- <span onClick={() => { handleLink(3) }}>清空连接</span>
- </>
- }
- </>
- </div>
- </Modal >
- })
- export default WxTextModal
|