index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. import React, { useCallback, useMemo, useRef, useState } from 'react'
  2. import styles from './index.module.scss'
  3. import { API_upFile } from '@/store/action/layout'
  4. import { MessageFu } from '@/utils/message'
  5. import { fileDomInitialFu } from '@/utils/domShow'
  6. import { Button, Popconfirm } from 'antd'
  7. import {
  8. UploadOutlined,
  9. CloseOutlined,
  10. DownloadOutlined,
  11. EyeOutlined,
  12. MenuOutlined
  13. } from '@ant-design/icons'
  14. import classNames from 'classnames'
  15. import { baseURL } from '@/utils/http'
  16. import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, closestCenter } from '@dnd-kit/core'
  17. import {
  18. SortableContext,
  19. useSortable,
  20. verticalListSortingStrategy,
  21. arrayMove
  22. } from '@dnd-kit/sortable'
  23. import { CSS } from '@dnd-kit/utilities'
  24. import { fileTypeRes } from '@/utils'
  25. import ImageLazy from '../ImageLazy'
  26. import store from '@/store'
  27. import MyPopconfirm from '../MyPopconfirm'
  28. import { authFilesLookFu, FileListType } from '../Z3upFiles/data'
  29. import { forwardRef, useImperativeHandle } from 'react'
  30. // ----------------这个组件用于外面一级的附件上传
  31. interface SortableFileItemProps {
  32. file: FileListType
  33. onDelete: (id: number) => void
  34. isLook: boolean
  35. index: number
  36. disabled?: boolean
  37. oneIsCover?: boolean
  38. }
  39. // 修复性能问题:移除不必要的状态和事件处理
  40. const SortableFileItem = React.memo(
  41. ({ file, onDelete, isLook, index, disabled, oneIsCover }: SortableFileItemProps) => {
  42. const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
  43. id: file.id,
  44. disabled: disabled || isLook // 修复拖动禁用问题
  45. })
  46. const style = {
  47. transform: CSS.Transform.toString(transform),
  48. transition,
  49. opacity: isDragging ? 0.5 : 1
  50. }
  51. // 修复按钮点击问题:阻止事件冒泡
  52. const handleButtonClick = useCallback((e: React.MouseEvent, callback: () => void) => {
  53. e.stopPropagation() // 修复按钮点击无效问题:阻止事件冒泡到拖拽元素
  54. callback()
  55. }, [])
  56. if (file.type === 'img') {
  57. return (
  58. <div
  59. ref={setNodeRef}
  60. style={style}
  61. className={classNames('ZTbox1ImgRow', isDragging ? 'dragging' : '')}
  62. >
  63. {/* 修复按钮点击无效问题:将拖拽手柄单独放置,不覆盖操作按钮 */}
  64. <div
  65. className='ZTbox1ImgRowDragHandle'
  66. {...attributes}
  67. {...listeners}
  68. style={{ cursor: isLook ? 'default' : 'grab' }}
  69. >
  70. {file.thumb || file.filePath ? (
  71. <ImageLazy noLook={true} width={100} height={100} src={file.thumb || file.filePath} />
  72. ) : null}
  73. </div>
  74. {oneIsCover && index === 0 ? <div className='ZTbox1ImgRowCover'>封面</div> : null}
  75. <div className='ZTbox1ImgRowIcon'>
  76. <EyeOutlined
  77. onClick={e =>
  78. handleButtonClick(e, () =>
  79. store.dispatch({
  80. type: 'layout/lookBigImg',
  81. payload: {
  82. url: baseURL + file.filePath,
  83. show: true
  84. }
  85. })
  86. )
  87. }
  88. rev={undefined}
  89. />
  90. <a
  91. href={baseURL + file.filePath}
  92. download
  93. target='_blank'
  94. rel='noreferrer'
  95. onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
  96. >
  97. <DownloadOutlined rev={undefined} />
  98. </a>
  99. </div>
  100. {!isLook && (
  101. <MyPopconfirm
  102. txtK='删除'
  103. onConfirm={() => onDelete(file.id)}
  104. Dom={
  105. <CloseOutlined
  106. className='ZTbox1ImgRowX'
  107. rev={undefined}
  108. onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
  109. />
  110. }
  111. />
  112. )}
  113. </div>
  114. )
  115. } else {
  116. return (
  117. <div
  118. ref={setNodeRef}
  119. style={style}
  120. className={classNames('Z3filesRow', isDragging ? 'dragging' : '')}
  121. >
  122. {!isLook && (
  123. <div className='dragHandle' {...attributes} {...listeners}>
  124. <MenuOutlined rev={undefined} />
  125. </div>
  126. )}
  127. <div className='Z3files1' title={file.fileName}>
  128. {file.fileName}
  129. </div>
  130. <div className='Z3files2'>
  131. {authFilesLookFu(file.fileName, '') ? (
  132. <>
  133. <EyeOutlined
  134. rev={undefined}
  135. title='查看'
  136. onClick={e =>
  137. handleButtonClick(e, () => authFilesLookFu(file.fileName, file.filePath))
  138. }
  139. />
  140. &emsp;
  141. </>
  142. ) : null}
  143. <a
  144. title='下载'
  145. href={baseURL + file.filePath}
  146. download={file.fileName}
  147. target='_blank'
  148. rel='noreferrer'
  149. onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
  150. >
  151. <DownloadOutlined rev={undefined} />
  152. </a>
  153. &emsp;
  154. {!isLook && (
  155. <Popconfirm
  156. title='删除后无法恢复,是否删除?'
  157. okText='删除'
  158. cancelText='取消'
  159. onConfirm={() => onDelete(file.id)}
  160. okButtonProps={{ loading: false }}
  161. >
  162. <CloseOutlined
  163. rev={undefined}
  164. title='删除'
  165. onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
  166. />
  167. </Popconfirm>
  168. )}
  169. </div>
  170. </div>
  171. )
  172. }
  173. }
  174. )
  175. // ------------------------------------------
  176. type Props = {
  177. dirCode: string
  178. myUrl: string
  179. isLook?: boolean
  180. fileCheck?: boolean
  181. fromData?: any
  182. tips?: string
  183. size?: number
  184. maxCount?: number
  185. oneIsCover?: boolean
  186. moduleId: number | ''
  187. ref: any
  188. }
  189. // 修复类型检查问题:统一使用dnd-kit管理所有拖拽
  190. function Z3upFilesRef(
  191. {
  192. isLook = false,
  193. fileCheck = false,
  194. dirCode,
  195. myUrl,
  196. fromData,
  197. tips = '单个附件不得超过500M',
  198. size = 500,
  199. maxCount = 999,
  200. oneIsCover,
  201. moduleId
  202. }: Props,
  203. ref: any
  204. ) {
  205. const [list, setList] = useState<FileListType[]>([])
  206. // 修复图片闪动问题:分别管理图片和非图片的排序
  207. const { imageFiles, otherFiles } = useMemo(() => {
  208. const arr = list || []
  209. const imgArr = arr.filter(v => v.type === 'img')
  210. const otherArr = arr.filter(v => v.type !== 'img')
  211. return { imageFiles: imgArr, otherFiles: otherArr }
  212. }, [list])
  213. const fileList = useMemo(() => {
  214. return [...imageFiles, ...otherFiles]
  215. }, [imageFiles, otherFiles])
  216. const [activeId, setActiveId] = useState<number | null>(null)
  217. const myInput = useRef<HTMLInputElement>(null)
  218. // 修复性能问题:使用useMemo缓存文件ID数组
  219. const fileIds = useMemo(() => fileList.map(f => f.id), [fileList])
  220. // 上传多个文件
  221. const handeUpPhoto = useCallback(
  222. async (e: React.ChangeEvent<HTMLInputElement>) => {
  223. if (!e.target.files || e.target.files.length === 0) return
  224. const files = Array.from(e.target.files)
  225. if (files.length + fileList.length > maxCount)
  226. return MessageFu.warning(
  227. `最多可上传${maxCount}个文件,当前选中${files.length}个文件,还可上传${maxCount - fileList.length}个文件`
  228. )
  229. // 逐个上传文件
  230. for (const file of files) {
  231. if (size && file.size > size * 1024 * 1024) {
  232. MessageFu.warning(`文件"${file.name}"超过${size}M限制!`)
  233. continue
  234. }
  235. const typeRes = fileTypeRes(file.name)
  236. const fd = new FormData()
  237. fd.append('type', typeRes)
  238. fd.append('dirCode', dirCode)
  239. fd.append('isCompress', 'true')
  240. fd.append('isDb', 'true')
  241. if (moduleId) {
  242. // 后端要求,这个模块不要传moduleId
  243. if (!['cms/order/register/son/upload'].includes(myUrl))
  244. fd.append('moduleId', moduleId + '')
  245. }
  246. fd.append('file', file)
  247. if (fromData) {
  248. for (const k in fromData) {
  249. if (fromData[k]) fd.append(k, fromData[k])
  250. }
  251. }
  252. e.target.value = ''
  253. try {
  254. const res = await API_upFile(fd, myUrl)
  255. if (res.code === 0) {
  256. MessageFu.success(
  257. `上传成功${files.length > 1 ? `(${files.indexOf(file) + 1}/${files.length})` : ''}`
  258. )
  259. setList(prev => [res.data, ...prev])
  260. fileDomInitialFu()
  261. } else {
  262. fileDomInitialFu()
  263. }
  264. } catch (error) {
  265. fileDomInitialFu()
  266. }
  267. }
  268. },
  269. [dirCode, fileList.length, fromData, maxCount, moduleId, myUrl, size]
  270. )
  271. // 修复拖拽逻辑问题:统一使用dnd-kit管理拖拽
  272. const handleDragStart = useCallback((event: DragStartEvent) => {
  273. setActiveId(Number(event.active.id))
  274. }, [])
  275. // 修复拖拽交换问题:增加类型检查,只有同类型文件才能交换
  276. const handleDragEnd = useCallback(
  277. (event: DragEndEvent) => {
  278. const { active, over } = event
  279. setActiveId(null)
  280. if (over && active.id !== over.id) {
  281. const activeFile = fileList.find(item => item.id === active.id)
  282. const overFile = fileList.find(item => item.id === over.id)
  283. // 修复类型检查问题:只有同类型文件才能交换位置
  284. if (activeFile && overFile && activeFile.type === overFile.type) {
  285. setList(items => {
  286. const oldIndex = items.findIndex(item => item.id === active.id)
  287. const newIndex = items.findIndex(item => item.id === over.id)
  288. return arrayMove(items, oldIndex, newIndex)
  289. })
  290. }
  291. }
  292. },
  293. [fileList]
  294. )
  295. // 列表删除某一个文件
  296. const delImgListFu = useCallback(
  297. (id: number) => {
  298. setList(prev => prev.filter(v => v.id !== id))
  299. },
  300. [setList]
  301. )
  302. // 获取当前拖拽的文件
  303. const activeFile = useMemo(() => {
  304. return activeId ? fileList.find(file => file.id === activeId) : null
  305. }, [activeId, fileList])
  306. // 修复拖拽覆盖层显示问题
  307. const renderDragOverlay = useCallback(() => {
  308. if (!activeFile) return null
  309. if (activeFile.type === 'img') {
  310. return (
  311. <div className={classNames('ZTbox1ImgRow', 'dragOverlay')}>
  312. {activeFile.thumb || activeFile.filePath ? (
  313. <ImageLazy
  314. noLook={true}
  315. width={100}
  316. height={100}
  317. src={activeFile.thumb || activeFile.filePath}
  318. />
  319. ) : null}
  320. <div className='ZTbox1ImgRowIcon' style={{ opacity: 0.5 }}>
  321. <EyeOutlined rev={undefined} />
  322. <DownloadOutlined rev={undefined} />
  323. </div>
  324. </div>
  325. )
  326. } else {
  327. return (
  328. <div className={classNames('Z3filesRow', 'dragOverlay')}>
  329. <div className='dragHandle'>
  330. <MenuOutlined rev={undefined} />
  331. </div>
  332. <div className='Z3files1' title={activeFile.fileName}>
  333. {activeFile.fileName}
  334. </div>
  335. <div className='Z3files2'>{/* 拖拽时隐藏操作按钮 */}</div>
  336. </div>
  337. )
  338. }
  339. }, [activeFile])
  340. // 设置数据
  341. const sonSetListFu = useCallback((list: FileListType[]) => {
  342. setList(list)
  343. }, [])
  344. // 返回数据
  345. const sonResListFu = useCallback(() => {
  346. const obj = {
  347. list: fileList || [],
  348. thumb: '',
  349. thumbPc: ''
  350. }
  351. if (oneIsCover && fileList.length) {
  352. const findObj = fileList.find(v => v.type === 'img')
  353. if (findObj) {
  354. obj.thumb = findObj.thumb
  355. obj.thumbPc = findObj.filePath
  356. }
  357. }
  358. return obj
  359. }, [fileList, oneIsCover])
  360. // 可以让父组件调用子组件的方法
  361. useImperativeHandle(ref, () => ({
  362. sonSetListFu,
  363. sonResListFu
  364. }))
  365. return (
  366. <div className={styles.Z3upFilesRef}>
  367. <input
  368. id='upInput'
  369. type='file'
  370. ref={myInput}
  371. onChange={handeUpPhoto}
  372. multiple
  373. style={{ display: 'none' }}
  374. />
  375. <div className='Z3Btn'>
  376. {!isLook && (
  377. <>
  378. <Button
  379. onClick={() => myInput.current?.click()}
  380. icon={<UploadOutlined rev={undefined} />}
  381. >
  382. 上传文件
  383. </Button>
  384. &emsp;{oneIsCover ? <span>第一张图片将用作封面</span> : ''}
  385. </>
  386. )}
  387. <div className='Z3files'>
  388. <DndContext
  389. collisionDetection={closestCenter}
  390. onDragStart={handleDragStart}
  391. onDragEnd={handleDragEnd}
  392. >
  393. <SortableContext items={fileIds} strategy={verticalListSortingStrategy}>
  394. {fileList.map((file, index) => (
  395. <SortableFileItem
  396. key={file.id}
  397. file={file}
  398. onDelete={delImgListFu}
  399. isLook={isLook}
  400. index={index}
  401. oneIsCover={oneIsCover}
  402. />
  403. ))}
  404. </SortableContext>
  405. <DragOverlay>{renderDragOverlay()}</DragOverlay>
  406. </DndContext>
  407. </div>
  408. <div className='fileTit' hidden={isLook}>
  409. {tips}
  410. ;支持按住Ctrl键选择多个文件上传;按住鼠标拖动图片 / 拖动附件左侧图标 可调整顺序
  411. <div
  412. className={classNames(
  413. 'noUpThumb',
  414. fileList.length <= 0 && fileCheck ? 'noUpThumbAc' : ''
  415. )}
  416. >
  417. 请上传文件!
  418. </div>
  419. </div>
  420. </div>
  421. {isLook && fileList.length <= 0 ? (
  422. <div style={{ height: 32, lineHeight: '32px' }}>(空)</div>
  423. ) : null}
  424. </div>
  425. )
  426. }
  427. export default forwardRef(Z3upFilesRef)