index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
  2. import styles from './index.module.scss'
  3. import { Button, Checkbox, Input, Modal } from 'antd'
  4. import { forwardRef, useImperativeHandle } from 'react'
  5. import { baseURL } from '@/utils/http'
  6. import {
  7. PlusOutlined,
  8. CloseCircleOutlined,
  9. UploadOutlined,
  10. CloseOutlined,
  11. DownloadOutlined,
  12. EyeOutlined
  13. } from '@ant-design/icons'
  14. import { MessageFu } from '@/utils/message'
  15. import { API_upFile } from '@/store/action/layout'
  16. import { fileDomInitialFu } from '@/utils/domShow'
  17. import store from '@/store'
  18. import ImageLazy from '../ImageLazy'
  19. import classNames from 'classnames'
  20. import MyPopconfirm from '../MyPopconfirm'
  21. // import { A2_APIchangeImgName } from "@/store/action/A2exhibition";
  22. export type FileListType = {
  23. fileName: string
  24. thumb?: string
  25. filePath: string
  26. id: number
  27. type: 'model' | 'img' | 'audio' | 'video'
  28. imgName: string
  29. }
  30. type Props = {
  31. ref: any //当前自己的ref,给父组件调用
  32. selecFlag: string //筛选的字符串 模型/图片/音频/视频
  33. fileCheck: boolean //有没有点击过确定
  34. dirCode: string //文件的code码
  35. myUrl: string //请求地址
  36. isLook?: boolean //是不是查看
  37. modelSize?: number //模型文件大小限制
  38. imgSize?: number //图片大小限制
  39. imgLength?: number //图片数量限制
  40. audioSize?: number //音频大小限制
  41. videoSize?: number //视频大小限制
  42. videoTit?: string //视频上传的提示语
  43. isTypeShow?: boolean //默认就选中(只有一个类型的时候)
  44. isUpName?: boolean //是否能修改图片名字
  45. lastImgTxt?: string //加载最后面的上传提示
  46. oneIsCover?: boolean //是否将第一张作为封面
  47. }
  48. function ZupTypes(
  49. {
  50. selecFlag,
  51. fileCheck,
  52. dirCode,
  53. myUrl,
  54. isLook = false,
  55. modelSize = 500,
  56. imgSize = 5,
  57. imgLength = 9,
  58. audioSize = 10,
  59. videoSize = 500,
  60. videoTit = '',
  61. isTypeShow = false,
  62. isUpName = false,
  63. lastImgTxt = '',
  64. oneIsCover = false
  65. }: Props,
  66. ref: any
  67. ) {
  68. // 筛选
  69. const [typeCheck, setTypeCheck] = useState<string[]>([])
  70. // 筛选数组
  71. const typeCheckArr = useMemo(() => {
  72. const arr = [
  73. { label: '模型', value: 'model' },
  74. { label: '图片', value: 'img' },
  75. { label: '音频', value: 'audio' },
  76. { label: '视频', value: 'video' }
  77. ]
  78. const arrRes = arr.filter(v => selecFlag.includes(v.label))
  79. if (arrRes.length <= 1 && isTypeShow) {
  80. setTypeCheck([arrRes[0].value])
  81. // 默认就选中(只有一个类型的时候)
  82. }
  83. return arrRes
  84. }, [isTypeShow, selecFlag])
  85. // 上传附件的信息
  86. const [fileList, setFileList] = useState({
  87. model: {} as FileListType,
  88. img: [] as FileListType[],
  89. audio: {} as FileListType,
  90. video: {} as FileListType
  91. })
  92. // 附件信息的校验,不满足返回 true
  93. const fileCheckFu = useMemo(() => {
  94. let flag = false
  95. if (typeCheck.length === 0) flag = true
  96. if (typeCheck.includes('model') && !fileList.model.id) flag = true
  97. if (typeCheck.includes('img') && fileList.img.length === 0) flag = true
  98. if (typeCheck.includes('audio') && !fileList.audio.id) flag = true
  99. if (typeCheck.includes('video') && !fileList.video.id) flag = true
  100. return flag
  101. }, [fileList, typeCheck])
  102. // 点击上传附件按钮
  103. const myInput = useRef<HTMLInputElement>(null)
  104. const [fileOneType, setFileOneType] = useState('')
  105. useEffect(() => {
  106. if (fileOneType) myInput.current?.click()
  107. }, [fileOneType])
  108. const upFileFu = useCallback((type: string) => {
  109. setFileOneType('')
  110. window.setTimeout(() => {
  111. setFileOneType(type)
  112. }, 100)
  113. }, [])
  114. // 上传附件的处理函数
  115. const handeUpPhoto2 = useCallback(
  116. async (e: React.ChangeEvent<HTMLInputElement>) => {
  117. if (e.target.files) {
  118. // 拿到files信息
  119. const filesInfo = e.target.files[0]
  120. let anType = ['image/jpeg', 'image/png']
  121. let anTit1 = '只支持png、jpg格式!'
  122. let anTit2 = `最大支持${imgSize}M!`
  123. let anSize = imgSize * 1024 * 1024
  124. if (fileOneType === 'audio') {
  125. anType = ['audio/mpeg']
  126. anTit1 = '只支持mp3格式!'
  127. anTit2 = `最大支持${audioSize}M!`
  128. anSize = audioSize * 1024 * 1024
  129. } else if (fileOneType === 'video') {
  130. anType = ['video/mp4']
  131. anTit1 = '只支持mp4格式!'
  132. anTit2 = `最大支持${videoSize}M!`
  133. anSize = videoSize * 1024 * 1024
  134. } else if (fileOneType === 'model') {
  135. anType = ['']
  136. anTit1 = '只支持4dage格式!'
  137. anTit2 = `最大支持${modelSize}M!`
  138. anSize = modelSize * 1024 * 1024
  139. }
  140. // 校验格式
  141. if (fileOneType !== 'model') {
  142. if (!anType.includes(filesInfo.type)) {
  143. e.target.value = ''
  144. return MessageFu.warning(anTit1)
  145. }
  146. } else {
  147. if (!filesInfo.name.includes('.4dage')) {
  148. e.target.value = ''
  149. return MessageFu.warning(anTit1)
  150. }
  151. }
  152. // 校验大小
  153. if (filesInfo.size > anSize) {
  154. e.target.value = ''
  155. return MessageFu.warning(anTit2)
  156. }
  157. // 创建FormData对象
  158. const fd = new FormData()
  159. // 把files添加进FormData对象(‘photo’为后端需要的字段)
  160. fd.append('type', fileOneType)
  161. fd.append('dirCode', dirCode)
  162. fd.append('isDb', 'true')
  163. //初始图片 fileName为:未命名
  164. if (isUpName) {
  165. fd.append('isDefaultName', 'false')
  166. }
  167. fd.append('file', filesInfo)
  168. if (fileOneType === 'img' && filesInfo.size > 1 * 1024 * 1024) {
  169. // 开启压缩图片
  170. fd.append('isCompress', 'true')
  171. }
  172. e.target.value = ''
  173. const res = await API_upFile(fd, myUrl)
  174. try {
  175. if (res.code === 0) {
  176. MessageFu.success('上传成功!')
  177. if (fileOneType === 'img')
  178. setFileList({
  179. ...fileList,
  180. img: [...fileList.img, { ...res.data, imgName: '未命名' }]
  181. })
  182. else setFileList({ ...fileList, [fileOneType]: res.data })
  183. }
  184. fileDomInitialFu()
  185. } catch (error) {
  186. fileDomInitialFu()
  187. }
  188. }
  189. },
  190. [audioSize, dirCode, fileList, fileOneType, imgSize, isUpName, modelSize, myUrl, videoSize]
  191. )
  192. // 附件图片的拖动
  193. const [dragImg, setDragImg] = useState<any>(null)
  194. const handleDragOver = useCallback(
  195. (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
  196. if (isLook) return
  197. e.dataTransfer.dropEffect = 'move'
  198. },
  199. [isLook]
  200. )
  201. const handleDragEnter = useCallback(
  202. (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
  203. if (isLook) return
  204. e.dataTransfer.effectAllowed = 'move'
  205. if (item === dragImg) return
  206. const newItems = [...fileList.img] //拷贝一份数据进行交换操作。
  207. const src = newItems.indexOf(dragImg) //获取数组下标
  208. const dst = newItems.indexOf(item)
  209. newItems.splice(dst, 0, ...newItems.splice(src, 1)) //交换位置
  210. setFileList({ ...fileList, img: newItems })
  211. },
  212. [dragImg, fileList, isLook]
  213. )
  214. // 删除某一张图片
  215. const delImgListFu = useCallback(
  216. (id: number) => {
  217. const newItems = fileList.img.filter(v => v.id !== id)
  218. setFileList({ ...fileList, img: newItems })
  219. },
  220. [fileList]
  221. )
  222. // 模型 音频 视频 的 dom
  223. const resOneDivDom = useCallback(
  224. (type: 'model' | 'audio' | 'video') => {
  225. const dom = (
  226. <div className='ZTbox' hidden={!typeCheck.includes(type)}>
  227. <div className='ZTbox1'>
  228. <span> </span>
  229. {type === 'model' ? '模型' : type === 'audio' ? '音频' : '视频'}:
  230. </div>
  231. {fileList[type].id ? (
  232. <div className='ZTbox2'>
  233. <div className='ZTbox2Name'>{fileList[type].fileName}</div>
  234. <div
  235. className='ZTbox2Look'
  236. onClick={() =>
  237. store.dispatch({
  238. type: 'layout/lookDom',
  239. payload: { src: fileList[type].filePath, type }
  240. })
  241. }
  242. >
  243. <EyeOutlined rev={undefined} />
  244. </div>
  245. <a
  246. href={baseURL + fileList[type].filePath}
  247. download
  248. target='_blank'
  249. className='ZTbox2Down'
  250. rel='noreferrer'
  251. >
  252. <DownloadOutlined rev={undefined} />
  253. </a>
  254. <MyPopconfirm
  255. txtK='删除'
  256. onConfirm={() => setFileList({ ...fileList, [type]: {} as FileListType })}
  257. Dom={<CloseCircleOutlined className='ZTbox2X' rev={undefined} />}
  258. />
  259. </div>
  260. ) : (
  261. <>
  262. <Button onClick={() => upFileFu(type)} icon={<UploadOutlined rev={undefined} />}>
  263. 上传
  264. </Button>
  265. <div className='ZTboxTit'>
  266. {type === 'model'
  267. ? `仅支持4dage格式的模型文件,大小不能超过${modelSize}M。`
  268. : type === 'audio'
  269. ? `仅支持mp3格式的音频文件,大小不得超过${audioSize}MB。`
  270. : `仅支持mp4格式的视频文件,大小不得超过${videoSize}MB。${videoTit}`}
  271. </div>
  272. </>
  273. )}
  274. </div>
  275. )
  276. return dom
  277. },
  278. [audioSize, fileList, modelSize, typeCheck, upFileFu, videoSize, videoTit]
  279. )
  280. // ------------让父组件调用的 回显
  281. const setFileComFileFu = useCallback((info: any) => {
  282. if (info.type) setTypeCheck(info.type.split(','))
  283. if (info.fileList && info.fileList.length) {
  284. const data: FileListType[] = info.fileList
  285. const obj = {
  286. model: {} as FileListType,
  287. img: [] as FileListType[],
  288. audio: {} as FileListType,
  289. video: {} as FileListType
  290. }
  291. data.forEach(v => {
  292. if (v.type === 'img') {
  293. obj.img.push({ ...v, imgName: v.fileName })
  294. } else obj[v.type!] = v
  295. })
  296. setFileList(obj)
  297. }
  298. }, [])
  299. // --------------让父组件调用的返回 附件 信息
  300. const fileComFileResFu = useCallback(() => {
  301. let coverUrl = ''
  302. const fileIds = []
  303. if (fileList.model.id && typeCheck.includes('model')) fileIds.push(fileList.model.id)
  304. if (fileList.audio.id && typeCheck.includes('audio')) fileIds.push(fileList.audio.id)
  305. if (fileList.video.id && typeCheck.includes('video')) fileIds.push(fileList.video.id)
  306. if (typeCheck.includes('img')) {
  307. fileList.img.forEach((v, i) => {
  308. if (v.id) {
  309. fileIds.push(v.id)
  310. if (oneIsCover && i === 0) {
  311. // 返回 第一张图的url 作为封面
  312. coverUrl = v.thumb || v.filePath
  313. }
  314. }
  315. })
  316. }
  317. return {
  318. sonType: typeCheck,
  319. sonFileIds: fileIds,
  320. sonIsOk: fileCheckFu,
  321. coverUrl
  322. }
  323. }, [
  324. fileCheckFu,
  325. fileList.audio.id,
  326. fileList.img,
  327. fileList.model.id,
  328. fileList.video.id,
  329. oneIsCover,
  330. typeCheck
  331. ])
  332. // 可以让父组件调用子组件的方法
  333. useImperativeHandle(ref, () => ({
  334. setFileComFileFu,
  335. fileComFileResFu
  336. }))
  337. // 修改图片名称
  338. const [isNameChange, setIsNameChange] = useState({
  339. id: 0,
  340. oldName: '',
  341. newName: ''
  342. })
  343. // 关闭弹窗
  344. const isNameChangeXFu = useCallback(() => {
  345. setIsNameChange({ id: 0, oldName: '', newName: '' })
  346. }, [])
  347. // 点击图片名字-出来弹窗
  348. const isNameChangeFu = useCallback(
  349. (item: FileListType) => {
  350. if (isLook) return
  351. setIsNameChange({ id: item.id, oldName: item.imgName, newName: '' })
  352. },
  353. [isLook]
  354. )
  355. // 修改完这点击 确定修改
  356. const isNameChangeOkFu = useCallback(async () => {
  357. // if (!isNameChange.newName) return MessageFu.warning("图片名不能为空!");
  358. // const res = await A2_APIchangeImgName({
  359. // id: isNameChange.id,
  360. // fileName: isNameChange.newName,
  361. // });
  362. // if (res.code === 0) {
  363. // MessageFu.success("修改图片名成功!");
  364. // setFileList({
  365. // ...fileList,
  366. // img: fileList.img.map((v) => ({
  367. // ...v,
  368. // imgName: v.id === isNameChange.id ? isNameChange.newName : v.imgName,
  369. // })),
  370. // });
  371. // isNameChangeXFu();
  372. // }
  373. }, [])
  374. //
  375. return (
  376. <div className={classNames(styles.ZupTypes, isLook ? styles.ZupTypesLook : '')}>
  377. <input
  378. id='upInput'
  379. type='file'
  380. accept={
  381. fileOneType === 'img'
  382. ? '.png,.jpg,.jpeg'
  383. : fileOneType === 'audio'
  384. ? '.mp3'
  385. : fileOneType === 'model'
  386. ? '.4dage'
  387. : '.mp4'
  388. }
  389. ref={myInput}
  390. onChange={e => handeUpPhoto2(e)}
  391. />
  392. <div hidden={isTypeShow}>
  393. <Checkbox.Group
  394. options={typeCheckArr}
  395. value={typeCheck}
  396. onChange={e => setTypeCheck(e as string[])}
  397. />
  398. </div>
  399. {/* -----------模型 */}
  400. {resOneDivDom('model')}
  401. {/* -----------图片 */}
  402. <div className='ZTboxImgMain' hidden={!typeCheck.includes('img')}>
  403. <div className='ZTboxImgBox'>
  404. <div className='ZTbox1' hidden={isTypeShow}>
  405. <span> </span> 图片:
  406. </div>
  407. <div className='ZTbox1Img' style={{ width: isTypeShow ? '100%' : '' }}>
  408. <div
  409. hidden={(!!fileList.img.length && fileList.img.length >= imgLength) || isLook}
  410. className='ZTbox1ImgIcon'
  411. onClick={() => upFileFu('img')}
  412. >
  413. <PlusOutlined rev={undefined} />
  414. </div>
  415. {fileList.img.map((v, i) => (
  416. <div
  417. className='ZTbox1ImgRow'
  418. key={v.id}
  419. draggable='true'
  420. onDragStart={() => setDragImg(v)}
  421. onDragOver={e => handleDragOver(e, v)}
  422. onDragEnter={e => handleDragEnter(e, v)}
  423. onDragEnd={() => setDragImg(null)}
  424. >
  425. {v.thumb || v.filePath ? (
  426. <ImageLazy noLook={true} width={100} height={100} src={v.thumb || v.filePath} />
  427. ) : null}
  428. {oneIsCover && i === 0 ? <div className='ZTbox1ImgRowCover'>封面</div> : null}
  429. {/* 修改图片名字 */}
  430. {isUpName ? (
  431. <div
  432. title={v.imgName}
  433. className='ZTbox1ImgRowName'
  434. onClick={() => isNameChangeFu(v)}
  435. >
  436. {v.imgName}
  437. </div>
  438. ) : null}
  439. <div className='ZTbox1ImgRowIcon'>
  440. <EyeOutlined
  441. onClick={() =>
  442. store.dispatch({
  443. type: 'layout/lookBigImg',
  444. payload: {
  445. url: baseURL + v.filePath,
  446. show: true
  447. }
  448. })
  449. }
  450. rev={undefined}
  451. />
  452. <a href={baseURL + v.filePath} download target='_blank' rel='noreferrer'>
  453. <DownloadOutlined rev={undefined} />
  454. </a>
  455. </div>
  456. <MyPopconfirm
  457. txtK='删除'
  458. onConfirm={() => delImgListFu(v.id!)}
  459. Dom={<CloseOutlined className='ZTbox1ImgRowX' rev={undefined} />}
  460. />
  461. </div>
  462. ))}
  463. </div>
  464. </div>
  465. <div className='ZTboxTit' hidden={isLook}>
  466. {fileList.img.length && fileList.img.length >= 2 ? (
  467. <>
  468. 按住鼠标可拖动图片调整顺序。
  469. <br />
  470. </>
  471. ) : null}
  472. 支持png、jpg的图片格式;最大支持5M;最多支持{imgLength}张。
  473. {lastImgTxt}
  474. </div>
  475. </div>
  476. {/* -----------音频 */}
  477. {resOneDivDom('audio')}
  478. {/* -----------视频 */}
  479. {resOneDivDom('video')}
  480. {/* 最后的提示 */}
  481. <div className={classNames('ZcheckTxt', fileCheck && fileCheckFu ? 'ZcheckTxtAc' : '')}>
  482. 请最少上传一张图片!
  483. </div>
  484. {/* 点击修改名字出来的弹窗 */}
  485. {isNameChange.id ? (
  486. <Modal
  487. wrapClassName={styles.ZupTypesMo}
  488. open={true}
  489. title='修改展品图片名称'
  490. footer={
  491. [] // 设置footer为空,去掉 取消 确定默认按钮
  492. }
  493. >
  494. <br />
  495. <div className='ZupTypesMoRow'>
  496. <strong>当前名:</strong>
  497. {isNameChange.oldName}
  498. </div>
  499. <div className='ZupTypesMoRow'>
  500. <br />
  501. <strong>修改为:</strong>
  502. <Input
  503. style={{ width: 400 }}
  504. placeholder='请输入图片名'
  505. maxLength={50}
  506. showCount
  507. value={isNameChange.newName}
  508. onChange={e => {
  509. setIsNameChange({
  510. ...isNameChange,
  511. newName: e.target.value.replace(/\s+/g, '')
  512. })
  513. }}
  514. />
  515. </div>
  516. <div className='ZupTypesMoBtn'>
  517. <Button onClick={isNameChangeXFu}>取消</Button>
  518. &emsp;
  519. <Button type='primary' onClick={isNameChangeOkFu}>
  520. 修改
  521. </Button>
  522. </div>
  523. </Modal>
  524. ) : null}
  525. </div>
  526. )
  527. }
  528. export default forwardRef(ZupTypes)