index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. import React, { useCallback, useMemo, useRef, useState } from 'react'
  2. import styles from './index.module.scss'
  3. // 引入编辑器组件
  4. // 安装---npm install braft-editor --save --force
  5. // npm install braft-utils --save --force
  6. import { ContentUtils } from 'braft-utils'
  7. import BraftEditor from 'braft-editor'
  8. // 引入编辑器样式
  9. import 'braft-editor/dist/index.css'
  10. import classNames from 'classnames'
  11. import { MessageFu } from '@/utils/message'
  12. import { fileDomInitialFu } from '@/utils/domShow'
  13. import { baseURL } from '@/utils/http'
  14. import { forwardRef, useImperativeHandle } from 'react'
  15. import { API_upFile } from '@/store/action/layout'
  16. import ZupAudio, { ZupAudioType } from '../ZupAudio'
  17. import { Button, Checkbox, Input } from 'antd'
  18. import { ArrowDownOutlined, DeleteOutlined, ArrowUpOutlined } from '@ant-design/icons'
  19. import MyPopconfirm from '../MyPopconfirm'
  20. export type SectionArrType = {
  21. id: number
  22. name: string
  23. txt: any
  24. fileInfo: ZupAudioType
  25. }
  26. type Props = {
  27. check: boolean //表单校验,为fasle表示不校验
  28. dirCode: string //文件的code码
  29. isLook: boolean //是否是查看进来
  30. ref: any //当前自己的ref,给父组件调用
  31. myUrl: string //上传的api地址
  32. isOne?: boolean //只显示单个富文本
  33. upAudioBtnNone?: boolean //是否能上传无障碍音频
  34. }
  35. function ZRichTexts(
  36. { check, dirCode, isLook, myUrl, isOne = false, upAudioBtnNone = false }: Props,
  37. ref: any
  38. ) {
  39. const [sectionArr, setSectionArr] = useState<SectionArrType[]>([
  40. {
  41. id: Date.now(),
  42. name: '',
  43. txt: BraftEditor.createEditorState(''),
  44. fileInfo: { fileName: '', filePath: '' }
  45. }
  46. ])
  47. // 是否按章节发布
  48. const [isSection, setIsSection] = useState(false)
  49. // 当前上传 图片 视频的索引
  50. const nowIndexRef = useRef(0)
  51. // 判断 富文本是否为空
  52. const isTxtFlag = useMemo(() => {
  53. let flag = false
  54. // 不是按章节发布,检查第一个富文本
  55. if (!isSection) {
  56. const txt = sectionArr[0].txt.toText()
  57. const txtHtml = sectionArr[0].txt.toHTML()
  58. const txtRes = txt.replaceAll('\n', '').replaceAll(' ', '')
  59. if (!txtRes && !txtHtml.includes('class="media-wrap')) flag = true
  60. } else {
  61. // 按章节发布 检查 所有的 标题 和富文本
  62. sectionArr.forEach(v => {
  63. if (!v.name) flag = true
  64. const txt = v.txt.toText()
  65. const txtHtml = sectionArr[0].txt.toHTML()
  66. const txtRes = txt.replaceAll('\n', '').replaceAll(' ', '')
  67. if (!txtRes && !txtHtml.includes('class="media-wrap')) flag = true
  68. })
  69. }
  70. return flag
  71. }, [isSection, sectionArr])
  72. const myInput = useRef<HTMLInputElement>(null)
  73. // 上传图片、视频
  74. const handeUpPhoto = useCallback(
  75. async (e: React.ChangeEvent<HTMLInputElement>) => {
  76. if (e.target.files) {
  77. // 拿到files信息
  78. const filesInfo = e.target.files[0]
  79. let type = ['image/jpeg', 'image/png', 'video/mp4']
  80. let size = 5
  81. let txt = '图片只支持png、jpg和jpeg格式!'
  82. let txt2 = '图片最大支持5M!'
  83. const isVideoFlag = filesInfo.name.endsWith('.mp4') || filesInfo.name.endsWith('.MP4')
  84. if (isVideoFlag) {
  85. // 上传视频
  86. size = 500
  87. txt = '视频只支持mp4格式!'
  88. txt2 = '视频最大支持500M!'
  89. }
  90. // 校验格式
  91. if (!type.includes(filesInfo.type)) {
  92. e.target.value = ''
  93. return MessageFu.warning(txt)
  94. }
  95. // 校验大小
  96. if (filesInfo.size > size * 1024 * 1024) {
  97. e.target.value = ''
  98. return MessageFu.warning(txt2)
  99. }
  100. // 创建FormData对象
  101. const fd = new FormData()
  102. // 把files添加进FormData对象(‘photo’为后端需要的字段)
  103. fd.append('type', isVideoFlag ? 'video' : 'img')
  104. fd.append('dirCode', dirCode)
  105. fd.append('file', filesInfo)
  106. e.target.value = ''
  107. try {
  108. const res = await API_upFile(fd, myUrl)
  109. if (res.code === 0) {
  110. MessageFu.success('上传成功!')
  111. // 在光标位置插入图片
  112. const newTxt = ContentUtils.insertMedias(sectionArr[nowIndexRef.current].txt, [
  113. {
  114. type: isVideoFlag ? 'VIDEO' : 'IMAGE',
  115. url: baseURL + res.data.filePath
  116. }
  117. ])
  118. const arr = [...sectionArr]
  119. arr[nowIndexRef.current].txt = newTxt
  120. setSectionArr(arr)
  121. }
  122. fileDomInitialFu()
  123. } catch (error) {
  124. fileDomInitialFu()
  125. }
  126. }
  127. },
  128. [dirCode, myUrl, sectionArr]
  129. )
  130. // 让父组件调用的 回显 富文本
  131. const ritxtShowFu = useCallback((val: any) => {
  132. if (val) {
  133. setIsSection(val.isSection || false)
  134. if (val.txtArr) {
  135. const arr = val.txtArr.map((v: any) => ({
  136. ...v,
  137. txt: BraftEditor.createEditorState(v.txt)
  138. }))
  139. setSectionArr(arr)
  140. }
  141. }
  142. }, [])
  143. // 让父组件调用的返回 富文本信息 和 表单校验 isTxtFlag为ture表示未通过校验
  144. const fatherBtnOkFu = useCallback(() => {
  145. const arr: any[] = []
  146. sectionArr.forEach((v, i) => {
  147. arr.push({
  148. ...v,
  149. txt: v.txt.toHTML()
  150. })
  151. })
  152. const obj = {
  153. isSection: isSection, //是否按章节发布
  154. txtArr: arr
  155. }
  156. return { val: obj, flag: isTxtFlag }
  157. }, [isSection, isTxtFlag, sectionArr])
  158. // 可以让父组件调用子组件的方法
  159. useImperativeHandle(ref, () => ({
  160. ritxtShowFu,
  161. fatherBtnOkFu
  162. }))
  163. // 点击新增章节
  164. const addSectionFu = useCallback(() => {
  165. if (sectionArr.length >= 20) return MessageFu.warning('最多存在20个章节')
  166. setSectionArr([
  167. ...sectionArr,
  168. {
  169. id: Date.now(),
  170. name: '',
  171. txt: BraftEditor.createEditorState(''),
  172. fileInfo: { fileName: '', filePath: '' }
  173. }
  174. ])
  175. }, [sectionArr])
  176. // 章节音频上传成功
  177. const upSectionFu = useCallback(
  178. (info: ZupAudioType, index: number) => {
  179. const arr = [...sectionArr]
  180. arr[index].fileInfo = info
  181. setSectionArr(arr)
  182. },
  183. [sectionArr]
  184. )
  185. // 章节音频删除
  186. const delSectionFu = useCallback(
  187. (index: number) => {
  188. // console.log("ppppppppp", index);
  189. const arr = [...sectionArr]
  190. arr[index].fileInfo = { fileName: '', filePath: '' }
  191. setSectionArr(arr)
  192. },
  193. [sectionArr]
  194. )
  195. // 整个章节的删除
  196. const delSectionAllFu = useCallback(
  197. (id: number) => {
  198. setSectionArr(sectionArr.filter(v => v.id !== id))
  199. },
  200. [sectionArr]
  201. )
  202. // 整个章节的位移
  203. const moveSectionFu = useCallback(
  204. (index: number, num: number) => {
  205. const arr = [...sectionArr]
  206. const temp = arr[index]
  207. arr[index] = arr[index + num]
  208. arr[index + num] = temp
  209. setSectionArr(arr)
  210. },
  211. [sectionArr]
  212. )
  213. // 单个富文本是否输入完整
  214. const isOneTxtFlag = useCallback(
  215. (name: string, txt: any) => {
  216. let flag = false
  217. if (!name && isSection) flag = true
  218. const txt2 = txt.toText()
  219. const txtHtml = txt.toHTML()
  220. const txtRes = txt2.replaceAll('\n', '').replaceAll(' ', '')
  221. if (!txtRes && !txtHtml.includes('class="media-wrap')) flag = true
  222. return flag
  223. },
  224. [isSection]
  225. )
  226. return (
  227. <div className={styles.ZRichTexts}>
  228. <input
  229. id='upInput'
  230. type='file'
  231. accept='.png,.jpg,.jpeg,.mp4'
  232. ref={myInput}
  233. onChange={e => handeUpPhoto(e)}
  234. />
  235. <div className={classNames('formRightZW', isLook ? 'formRightZWLook' : '')}>
  236. {isOne ? (
  237. <div></div>
  238. ) : (
  239. <Checkbox checked={isSection} onChange={e => setIsSection(e.target.checked)}>
  240. 按章节发布
  241. </Checkbox>
  242. )}
  243. {isSection ? (
  244. <Button hidden={isLook} type='primary' onClick={addSectionFu}>
  245. 新增章节
  246. </Button>
  247. ) : (
  248. <div className='formRightZWRR'>
  249. {upAudioBtnNone ? null : (
  250. <ZupAudio
  251. fileInfo={sectionArr[0].fileInfo}
  252. upDataFu={info => upSectionFu(info, 0)}
  253. delFu={() => delSectionFu(0)}
  254. dirCode={dirCode}
  255. myUrl={myUrl}
  256. isLook={isLook}
  257. />
  258. )}
  259. <div hidden={isLook} style={{ marginLeft: 20 }}>
  260. <Button
  261. onClick={() => {
  262. nowIndexRef.current = 0
  263. myInput.current?.click()
  264. }}
  265. >
  266. 上传图片/视频
  267. </Button>
  268. </div>
  269. </div>
  270. )}
  271. </div>
  272. <div className={classNames('txtBox', isLook ? 'txtBoxLook' : '')}>
  273. {sectionArr.map((item, index) => (
  274. <div
  275. className={classNames(
  276. 'zztxtRow',
  277. isOneTxtFlag(item.name, item.txt) && check ? 'zztxtRowErr' : ''
  278. )}
  279. key={item.id}
  280. hidden={!isSection && index > 0}
  281. >
  282. {/* 顶部 */}
  283. <div className='zztxtRow1' hidden={!isSection && index === 0}>
  284. <div className='zztxtRow1_1'>
  285. <div className='zztxtRow1_1_1'>章节 {index + 1}</div>
  286. <div className='zztxtRow1_1_2'>
  287. 标题:
  288. <Input
  289. readOnly={isLook}
  290. value={item.name}
  291. placeholder='请输入内容'
  292. maxLength={100}
  293. showCount
  294. style={{ width: 400 }}
  295. onChange={e => {
  296. const arr = [...sectionArr]
  297. arr[index].name = e.target.value.replace(/\s+/g, '')
  298. setSectionArr(arr)
  299. }}
  300. />
  301. &emsp;
  302. <Button
  303. hidden={isLook}
  304. onClick={() => {
  305. nowIndexRef.current = index
  306. myInput.current?.click()
  307. }}
  308. >
  309. 上传图片/视频
  310. </Button>
  311. </div>
  312. </div>
  313. <div className='zztxtRow1_2'>
  314. <ZupAudio
  315. fileInfo={item.fileInfo}
  316. upDataFu={info => upSectionFu(info, index)}
  317. delFu={() => delSectionFu(index)}
  318. dirCode={dirCode}
  319. myUrl={myUrl}
  320. isLook={isLook}
  321. />
  322. &emsp;
  323. <div
  324. hidden={isLook}
  325. className={classNames('zztxtRow1_2Icon', index === 0 ? 'zztxtRow1_2IconNo' : '')}
  326. onClick={() => moveSectionFu(index, -1)}
  327. >
  328. <ArrowUpOutlined title='上移' />
  329. </div>
  330. &emsp;
  331. <div
  332. hidden={isLook}
  333. className={classNames(
  334. 'zztxtRow1_2Icon',
  335. index === sectionArr.length - 1 ? 'zztxtRow1_2IconNo' : ''
  336. )}
  337. onClick={() => moveSectionFu(index, 1)}
  338. >
  339. <ArrowDownOutlined title='下移' />
  340. </div>
  341. &emsp;
  342. {isLook || sectionArr.length <= 1 ? null : (
  343. <MyPopconfirm
  344. txtK='删除'
  345. onConfirm={() => delSectionAllFu(item.id)}
  346. Dom={<DeleteOutlined title='删除' className='ZTbox2X' />}
  347. />
  348. )}
  349. </div>
  350. </div>
  351. {/* 主体 */}
  352. <BraftEditor
  353. readOnly={isLook}
  354. placeholder='请输入内容'
  355. value={item.txt}
  356. onChange={e => {
  357. const arr = [...sectionArr]
  358. arr[index].txt = e
  359. setSectionArr(arr)
  360. }}
  361. imageControls={['remove']}
  362. />
  363. </div>
  364. ))}
  365. </div>
  366. <div className={classNames('noUpThumb', check && isTxtFlag ? 'noUpThumbAc' : '')}>
  367. {`请完整输入${isSection ? '标题/' : ''}正文!`}
  368. </div>
  369. </div>
  370. )
  371. }
  372. export default forwardRef(ZRichTexts)