msgWxGraphicListModal.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. import { PlusOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
  2. import { Button, Col, Input, InputNumber, message, Modal, Radio, Row, Space, Tag, Tooltip } from 'antd'
  3. import { RadioChangeEvent } from 'antd/lib/radio'
  4. import React, { useCallback, useEffect, useState } from 'react'
  5. import MsgWxLeft from './msgWxLeft'
  6. import style from './graphic.less'
  7. import { useModel } from 'umi'
  8. import WxMpnews from '@/components/MaterialModal/wxMpnews'
  9. import WxHistoryBD from '@/components/MaterialModal/wxHistorBD'
  10. import { isArray } from 'lodash'
  11. import { exportMediaByUrl1 } from '@/services/operating/material'
  12. import useIndexDB from '@/Hook/useIndexDB'
  13. import SmMaterialModal from '@/components/MaterialModal/smModalBox'
  14. import filePng from '../../../public/file.png'
  15. import WxSelect from '../WxSelect'
  16. /**客服,延迟,立即回复,公众号菜单使用中 */
  17. /**
  18. * @param isProhibitMs 是否禁止多图文按钮 默认 false 不禁用
  19. * @param tooltipTxt 多图文单图文按钮提示文字配置
  20. * @param ImportBtShow 导入按钮控制 {1: 历史图文, 2: 素材图文,3: 全部展示} 默认值 3
  21. * @param historyType //历史图文type 默认 0: 智能互动消息历史 1:客服消息历史
  22. */
  23. type Props = {
  24. visible: boolean,
  25. onCancel: () => void,
  26. onOK: (props: any, type?: number) => void,
  27. defaultData: any,
  28. msgType?: number,
  29. isProhibitMs?: boolean, // 是否禁止多图文按钮
  30. tooltipTxt?: string, // 提示信息
  31. ImportBtShow?: number, // {1: 历史图文, 2: 素材图文,3: 全部展示} 默认值 3
  32. historyType?: number,
  33. title?: string,//弹窗标题
  34. isKnews?: boolean, // 是否k图文新建
  35. }
  36. const MsgWxGraphicListModal: React.FC<Props> = (props) => {
  37. const { drawerState: { dataArr, actionId, AllData }, dispatchMate } = useModel('useOperating.useMsgMaterialDrawer', model => ({ drawerState: model.state, dispatchMate: model.dispatch }))
  38. const { visible, onCancel, onOK, defaultData, msgType, isProhibitMs = false, tooltipTxt = "", ImportBtShow = 3, historyType = 0, title, isKnews = false } = props
  39. const [isShow, setIsShow] = useState<boolean>(false)//图片弹窗
  40. const [ref, setRef] = useState<any>(null)//存放实例
  41. const [range, setRange] = useState<Range>()//存放光标丢失位置
  42. const { state: { actionWX, selectWx }, initSelectWx } = useModel('useOperating.useWxGroupList')
  43. const [it, setIt] = useState<number>(0)//单图文多图文切换
  44. const [indexId, setIndexId] = useState<number>(0) //前面列表id
  45. const [mpVisible, setMpVisible] = useState<boolean>(false) // 素材弹窗
  46. const [showNews, setShowNews] = useState<boolean>(false)//导入文章
  47. const [newsUrl, setNewsUrl] = useState<string>('')//文章地址
  48. const [showBD, setShowBD] = useState<boolean>(false)//BD导入弹窗
  49. const [titleNum, setTitleNum] = useState<number>(0)
  50. const [v, setV] = useState<number>(1)//图篇或k图文 filebox使用
  51. const [sort, setSort] = useState<number>(0)
  52. const [noFile, setNoFile] = useState<boolean>(false)
  53. //DB
  54. const { add } = useIndexDB()
  55. //导入文章
  56. const handleImportNews = useCallback(() => {
  57. let str = newsUrl
  58. if (str.indexOf(',') === -1) {
  59. str = str.replace(/\s/ig, ',')
  60. }
  61. let arr = str.split(/[,,]/)
  62. let newArr: string[] = []
  63. arr.forEach((url: string) => {
  64. url = url.replace(/\s*/g, '')
  65. if (url !== "" && url) {
  66. newArr.push(url)
  67. }
  68. })
  69. let isOk = newArr.every((url: string, index: number) => {
  70. if (url.search(/http[s]?:\/\/mp.weixin.qq.com/ig) === 0) {
  71. return true
  72. } else {
  73. message.error(`第${index + 1}的地址错误,请输入正确的文章地址!`)
  74. return false
  75. }
  76. })
  77. if (newArr.length > (9 - actionId)) {
  78. message.error('你输入的链接条数超过了当前剩余可新增的条数')
  79. return
  80. }
  81. if (isOk) {
  82. let promiseAll: any[] = []
  83. newArr.forEach((url: string, index: number) => {
  84. // promiseAll.push(exportMediaByUrl1(url).then((res) => { return res.json() }))
  85. promiseAll.push(exportMediaByUrl1(encodeURIComponent(url)).then((res) => { return res.json() }))
  86. })
  87. Promise.all(promiseAll).then((res: any) => {
  88. let arr: any[] = []
  89. if (res) {
  90. res.forEach((r: { data: any, code: 200 }, index: number) => {
  91. if (r.code === 200) {
  92. let { content, contentSourceUrl, digest, thumbUrl, title, thumbMediaUrl } = r.data
  93. contentSourceUrl = contentSourceUrl.replace(/['"]*/g, '')
  94. content = content.replace('data-src', 'src').replace(/\<mpvoice[\s\S]*\<\/mpvoice\>/ig, '')
  95. arr.push({ menuId: actionId + index, content, contentSourceUrl, digest, thumbUrl: thumbUrl || thumbMediaUrl, title })
  96. }
  97. })
  98. arr = arr.sort((a: { menuId: number }, b: { menuId: number }) => { return a.menuId - b.menuId })
  99. dispatchMate({ type: 'importNews', params: { data: { dataArrs: arr, actionId } } })
  100. }
  101. }).catch(err => { console.log(err) })
  102. setShowNews(false)
  103. }
  104. }, [newsUrl, actionId])
  105. useEffect(() => {
  106. if (it === 1) {
  107. dispatchMate({ type: 'action', params: { menuId: dataArr[0].menuId } })
  108. }
  109. }, [it]) // 单图文默认1
  110. useEffect(() => { // 禁止设置选中单图文链接按钮
  111. if (isProhibitMs) {
  112. setIt(1)
  113. }
  114. }, [isProhibitMs])
  115. useEffect(() => {
  116. if (msgType === 1) {
  117. setIt(1)
  118. }
  119. }, [msgType])
  120. //确定
  121. const callback = useCallback((isBD?: boolean) => {
  122. let oldarr: any = dataArr
  123. if (it === 1) {
  124. let ac = oldarr[actionId - 1]
  125. oldarr = [ac]
  126. }
  127. let arr = oldarr.filter((item: any) => JSON.stringify(item) !== '{}')//筛除空对象||
  128. arr = arr.filter((item: any) => {
  129. return item.title
  130. })
  131. arr = arr.map((item: any) => {
  132. console.log(item)
  133. let obj: any = {
  134. knewsThumbUrl: item.thumbUrl, // 图片
  135. title: item.title.replace(/<[/]?span[^>]*>/ig, '#'), // 图文标题
  136. description: item.digest, // 描述内容
  137. knewsLink: item.contentSourceUrl, // 链接设置
  138. knewsThumbInfo: item?.knewsThumbInfo
  139. }
  140. if (item?.knewsThumbId) {
  141. obj['knewsThumbId'] = item?.knewsThumbId
  142. }
  143. return obj
  144. })
  145. console.log(arr)
  146. let isOk: boolean = arr.length > 0 && arr.every((item: any) => (item.knewsThumbUrl || item?.knewsThumbId || item?.knewsThumbInfo) && item.description && item.knewsLink && item.title)//检测全部必填项
  147. if (isBD) {
  148. add({ data: arr })//保存进DB
  149. return
  150. }
  151. if (isOk) {
  152. let params: any = { newsList: arr, indexId: indexId, mpIds: selectWx }
  153. if (isKnews) {
  154. params.sort = sort
  155. }
  156. onOK(params, 5)
  157. initSelectWx()
  158. dispatchMate({ type: 'initData' })
  159. } else {
  160. message.error('请检测标题,图片,描述,链接是否填写完整!不需要的篇章请全部留空')
  161. }
  162. }, [dataArr, it, AllData, actionId, add, selectWx, sort])
  163. const handelRadioIt = useCallback((e: RadioChangeEvent) => {
  164. let v = e.target.value
  165. setIt(v)
  166. }, [])
  167. //开关图片弹窗
  168. const closeModal = useCallback((v?: any) => {
  169. console.log(v)
  170. if (v) {
  171. setV(v)
  172. }
  173. setIsShow(!isShow)
  174. }, [isShow])
  175. /**选中图片,单独选择图片在传id 选择k图文直接映射*/
  176. const handleOk = (data: any) => {
  177. let obj: any = { menuId: actionId, thumbUrl: data.url }
  178. if (data?.id && !data?.mediaId) {//存在ID不存在媒体ID证明是本地素材才添加
  179. obj['knewsThumbId'] = data?.id
  180. }
  181. obj['knewsThumbInfo'] = {
  182. folder: data?.folder,
  183. title: data?.title,
  184. number: data?.number
  185. }
  186. if (v === 6) {
  187. obj['title'] = data?.title
  188. obj['contentSourceUrl'] = data?.knewsLink
  189. obj['digest'] = data?.description
  190. obj['thumbUrl'] = data?.knewsThumbUrl
  191. obj['knewsThumbId'] = data?.knewsThumbId
  192. obj['knewsThumbInfo'] = { folder: data?.folder }
  193. }
  194. dispatchMate({ type: 'pushData', params: obj })
  195. closeModal()
  196. }
  197. //插入粉丝昵称
  198. const pushName = useCallback(() => {
  199. let span = document.createElement('span')
  200. span.className = style.nickName
  201. span.setAttribute('contentEditable', 'false')
  202. span.innerText = `粉丝昵称`
  203. range?.insertNode(span)
  204. dispatchMate({ type: 'pushData', params: { menuId: actionId, title: ref.innerHTML } })
  205. }, [range, actionId])
  206. //光标丢失记录位置
  207. const onBlur = useCallback(() => {
  208. let selection = window.getSelection()
  209. let range = selection?.getRangeAt(0)
  210. setRange(range)
  211. if (ref.innerHTML) {
  212. dispatchMate({ type: 'pushData', params: { menuId: actionId, title: ref.innerHTML.replace(/&quot;/ig, '"').replace(/&amp;/ig, '&').replace(/&lt;/ig, '<').replace(/&gt;/ig, '>').replace(/<\s+/g, '<') } })
  213. }
  214. }, [ref, actionId])
  215. useEffect(() => {
  216. ref?.focus()
  217. }, [ref])
  218. //连接输入
  219. const handelLink = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
  220. let v = e.target.value
  221. dispatchMate({ type: 'pushData', params: { menuId: actionId, contentSourceUrl: v } })
  222. }, [actionId])
  223. //摘要输入
  224. const handelText = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
  225. let v = e.target.value
  226. dispatchMate({ type: 'pushData', params: { menuId: actionId, digest: v } })
  227. }, [actionId])
  228. //复制只取纯文本
  229. function textPaste(event: any) {
  230. event.preventDefault();
  231. let text;
  232. let clp = (event.originalEvent || event).clipboardData;
  233. // 兼容chorme或hotfire
  234. text = clp.getData('text')
  235. if (text !== "") {
  236. document.execCommand('insertHtml', false, text);
  237. }
  238. }
  239. /**编辑回填 */
  240. useEffect(() => {
  241. if (defaultData?.newsList) {
  242. let arr = defaultData?.newsList.map((item: any) => {
  243. let obj = {
  244. title: item.title.replace('#粉丝昵称#', `<span class=${style.nickName} contenteditable="false">粉丝昵称</span>`),
  245. thumbUrl: item.knewsThumbUrl,
  246. contentSourceUrl: item.knewsLink,
  247. digest: item.description,
  248. knewsThumbInfo: item?.knewsThumbInfo,
  249. knewsThumbId: item?.knewsThumbId
  250. }//转义
  251. setSort(item?.sort || 0)
  252. return obj
  253. })
  254. // arr = [...arr, ...Object.values(Array(8 - arr.length).fill({})).map(n => n)] //不足8个填充
  255. arr = arr.map((item: any, i: number) => {
  256. return { menuId: i + 1, ...item }
  257. })
  258. setIndexId(defaultData?.indexId)
  259. dispatchMate({ type: 'initData', params: { data: { dataArr: arr } } })
  260. if (arr.length === 1) {
  261. setIt(1)
  262. dispatchMate({ type: 'action', params: { menuId: dataArr[0].menuId } })
  263. }
  264. }
  265. return () => {
  266. dispatchMate({ type: 'initData' })
  267. }
  268. }, [defaultData])
  269. return <Modal
  270. title={title || '编辑图文内容'}
  271. width={1100}
  272. open={visible}
  273. onCancel={() => {
  274. onCancel()
  275. dispatchMate({ type: 'initData' })
  276. }}
  277. footer={<div>
  278. <Button onClick={() => {
  279. onCancel()
  280. dispatchMate({ type: 'initData' })
  281. }}>取消</Button>
  282. <Button onClick={() => callback()} type='primary'>确定</Button>
  283. <Button onClick={() => callback(true)}>保存到浏览器本地</Button>
  284. </div>}
  285. destroyOnClose
  286. >
  287. <div>
  288. <div style={{ marginBottom: '10px' }}>
  289. <Space>
  290. <Radio.Group value={it} onChange={handelRadioIt}>
  291. <Radio.Button value={0} disabled={msgType === 1}>多图文链接</Radio.Button>
  292. <Radio.Button value={1}>单图文链接</Radio.Button>
  293. </Radio.Group>
  294. <Tooltip title={tooltipTxt !== "" ? tooltipTxt : "多图文链接只有立即回复(关注与扫码)可设置且只能放在第一条,延迟消息只支持单图文链接"}>
  295. <ExclamationCircleOutlined />
  296. </Tooltip>
  297. </Space>
  298. </div>
  299. <div className={style.header}>
  300. <label>导入方式:</label>
  301. <Space>
  302. {
  303. ImportBtShow === 1 ?
  304. null
  305. : ImportBtShow === 2 ?
  306. <Button type="primary" size='small' onClick={() => setMpVisible(true)}>素材库导入</Button>
  307. : <Button type="primary" size='small' onClick={() => setMpVisible(true)}>素材库导入</Button>
  308. }
  309. <Button type='primary' size="small" onClick={() => setShowNews(true)}>链接导入</Button>
  310. <Button type='primary' size="small" onClick={() => setShowBD(true)}>浏览器本地导入</Button>
  311. <Button type='primary' size="small" onClick={() => closeModal(6)}>K-图文</Button>
  312. </Space>
  313. </div>
  314. {
  315. // 自定义设置
  316. <div className={style.centent}>
  317. <div >
  318. <MsgWxLeft it={it} dataArr={dataArr}></MsgWxLeft>
  319. </div>
  320. <div className={style.graphic_list}>
  321. {
  322. dataArr?.map((list: any, index: number) => {
  323. return actionId === list?.menuId && <div key={index}>
  324. <Row style={{ width: '80%' }}>
  325. <Col span={24}>
  326. <label >图文标题 :<Space><span>(必填)</span><span>(<span style={{ color: titleNum > 64 ? 'red' : '#000' }}>{titleNum}</span>/64)</span></Space></label>
  327. <div
  328. contentEditable="true"
  329. className={style.editor}
  330. ref={(ref) => setRef(ref)}
  331. onBlur={onBlur}
  332. dangerouslySetInnerHTML={{ __html: list?.title }}
  333. onPaste={textPaste}
  334. onKeyUp={() => {
  335. setTitleNum(ref?.innerHTML?.length || 0)
  336. }}
  337. />
  338. {/* <label >点击插入:<Tag color='orange' onClick={pushName}>粉丝昵称</Tag></label> */}
  339. </Col>
  340. <Col span={24}>
  341. <label >描述内容 :</label>
  342. <Input.TextArea rows={5} placeholder='填写图文描述' onChange={handelText} value={list?.digest} />
  343. </Col>
  344. <Col span={24}>
  345. <label >链接设置 :<span>(必填)</span></label>
  346. <Input placeholder='请输入跳转链接,且必须以http://或https://开头' onChange={handelLink} value={list?.contentSourceUrl} />
  347. </Col>
  348. {isKnews && <Col span={24}>
  349. <label >排序 :<span>(必填)</span><Tag color='warning'>数值越大越靠前</Tag></label>
  350. <InputNumber
  351. style={{ width: '100%' }}
  352. value={sort}
  353. placeholder='请输入序号, 数值越大越靠前'
  354. onChange={(e) => {
  355. setSort(e || 0)
  356. }}
  357. />
  358. </Col>}
  359. <WxSelect />
  360. </Row>
  361. <div>
  362. <label >图文封面:</label>
  363. <span className={list?.thumbUrl ? style.imgupload : ''} onClick={() => { closeModal(1); setNoFile(true) }}>
  364. {
  365. // (list?.thumbUrl || list?.knewsThumbInfo) ? <img src={list?.knewsThumbInfo?.folder ? filePng : list?.knewsThumbInfo?.id ? list?.knewsThumbInfo?.url : list?.thumbUrl} /> : <PlusOutlined style={{ fontSize: 30 }} />
  366. (list?.thumbUrl || list?.knewsThumbId) ? <img src={list?.thumbUrl || (list.knewsThumbId ? filePng : 'https://s.weituibao.com/static/1552098829922/bigfm.png')} /> : <PlusOutlined style={{ fontSize: 30 }} />
  367. // list?.knewsThumbInfo?.folder ? <img src={filePng} /> : (list?.thumbUrl) ? <img src={list?.thumbUrl} /> : <PlusOutlined style={{ fontSize: 30 }} />
  368. }
  369. <div><PlusOutlined style={{ fontSize: 30, color: '#fff' }} /> </div>
  370. </span>
  371. <div style={{ display: 'flex', flexFlow: 'column', width: 100 }}>
  372. <span>名称:{list?.knewsThumbInfo?.title || '网图'} </span>
  373. <span>编号:{list?.knewsThumbInfo?.number || '网图'}</span>
  374. </div>
  375. </div>
  376. </div>
  377. })
  378. }
  379. </div>
  380. {isShow && <SmMaterialModal
  381. visible={visible}
  382. onCancel={() => { closeModal(); setNoFile(false) }}
  383. title={`选择图片`}
  384. onOk={(props: any) => {
  385. handleOk(props)
  386. setNoFile(false)
  387. }}
  388. mediaType={v}
  389. isShowWx={v === 1 ? true : false}
  390. isAllData
  391. noFile={noFile}
  392. />}
  393. </div>
  394. }
  395. {mpVisible && <WxMpnews
  396. visible={mpVisible}
  397. onCancel={() => setMpVisible(false)}
  398. onOK={(data: any) => {
  399. if (data) {
  400. let articles: any[] = data?.news
  401. if (isArray(articles) && articles.length > 0) {
  402. dispatchMate({ type: 'initData', params: { data: { dataArr: articles.map((ar: any, index: number) => { return { menuId: index + 1, digest: ar.digest, contentSourceUrl: ar.contentSourceUrl, title: ar.title, thumbUrl: ar.thumbUrl || ar.thumbMediaUrl } }) } } })
  403. setMpVisible(false)
  404. }
  405. }
  406. }}
  407. defaultData=''
  408. wx={actionWX}
  409. />}
  410. <WxHistoryBD
  411. visible={showBD}
  412. onCancel={() => setShowBD(false)}
  413. onOK={(data: any) => {
  414. if (data) {
  415. dispatchMate({
  416. type: 'initData', params: {
  417. data: {
  418. dataArr: data.map((arr: any, index: number) => {
  419. return { menuId: index + 1, digest: arr.description || arr.newsDescription, contentSourceUrl: arr.knewsLink || arr.newsUrl, title: arr.title || arr.newsTitle, thumbUrl: arr.knewsThumbUrl || arr.newsPicUrl, knewsThumbInfo: arr?.knewsThumbInfo, knewsThumbId: arr?.knewsThumbId }
  420. })
  421. }
  422. }
  423. })
  424. setShowBD(false)
  425. }
  426. }}
  427. />
  428. {/* 链接导入弹窗 */}
  429. <Modal
  430. open={showNews}
  431. title='导入文章'
  432. onCancel={() => { setShowNews(false) }}
  433. onOk={handleImportNews}
  434. destroyOnClose
  435. >
  436. <Input.TextArea
  437. placeholder={
  438. ` 请填写文章url地址,支持批量,批量传入需要在每个链接后面加上逗号或者换行,从你当前选中的篇数开始导入替换,一次只能导入8篇,输入的链接地址不能超过可新建的篇数,假如你从第5篇开始导入那只能导入4篇
  439. 方式1:支持末尾加,号分割导入 https://mp.weixin.qq.com/s/C4qcDnPwfAlf4Y2P9qeThA,https://mp.weixin.qq.com/s/tVGyg6rxR6BoppjqIbKoGg
  440. 方式2:支持换行分割导入
  441. https://mp.weixin.qq.com/s/C4qcDnPwfAlf4Y2P9qeThA
  442. https://mp.weixin.qq.com/s/tVGyg6rxR6BoppjqIbKoGg
  443. `
  444. }
  445. rows={10}
  446. onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
  447. let url = e.target.value
  448. if (url) {
  449. setNewsUrl(url)
  450. }
  451. }}
  452. />
  453. </Modal>
  454. </div>
  455. </Modal>
  456. }
  457. export default React.memo(MsgWxGraphicListModal, (a, b) => {
  458. if (JSON.stringify(a) === JSON.stringify(b)) {
  459. return true
  460. } else {
  461. return false
  462. }
  463. })