|
|
@@ -0,0 +1,464 @@
|
|
|
+import React, { useCallback, useMemo, useRef, useState } from 'react'
|
|
|
+import styles from './index.module.scss'
|
|
|
+import { API_upFile } from '@/store/action/layout'
|
|
|
+import { MessageFu } from '@/utils/message'
|
|
|
+import { fileDomInitialFu } from '@/utils/domShow'
|
|
|
+import { Button, Popconfirm } from 'antd'
|
|
|
+import {
|
|
|
+ UploadOutlined,
|
|
|
+ CloseOutlined,
|
|
|
+ DownloadOutlined,
|
|
|
+ EyeOutlined,
|
|
|
+ MenuOutlined
|
|
|
+} from '@ant-design/icons'
|
|
|
+import classNames from 'classnames'
|
|
|
+import { baseURL } from '@/utils/http'
|
|
|
+import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, closestCenter } from '@dnd-kit/core'
|
|
|
+import {
|
|
|
+ SortableContext,
|
|
|
+ useSortable,
|
|
|
+ verticalListSortingStrategy,
|
|
|
+ arrayMove
|
|
|
+} from '@dnd-kit/sortable'
|
|
|
+import { CSS } from '@dnd-kit/utilities'
|
|
|
+import { fileTypeRes } from '@/utils'
|
|
|
+import ImageLazy from '../ImageLazy'
|
|
|
+import store from '@/store'
|
|
|
+import MyPopconfirm from '../MyPopconfirm'
|
|
|
+import { forwardRef, useImperativeHandle } from 'react'
|
|
|
+import { authFilesLookFu, FileListType } from './data'
|
|
|
+
|
|
|
+// ----------------这个组件用于外面一级的附件上传
|
|
|
+
|
|
|
+interface SortableFileItemProps {
|
|
|
+ file: FileListType
|
|
|
+ onDelete: (id: number) => void
|
|
|
+ isLook: boolean
|
|
|
+ index: number
|
|
|
+ disabled?: boolean
|
|
|
+ oneIsCover?: boolean
|
|
|
+}
|
|
|
+// 修复性能问题:移除不必要的状态和事件处理
|
|
|
+const SortableFileItem = React.memo(
|
|
|
+ ({ file, onDelete, isLook, index, disabled, oneIsCover }: SortableFileItemProps) => {
|
|
|
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
|
+ id: file.id,
|
|
|
+ disabled: disabled || isLook // 修复拖动禁用问题
|
|
|
+ })
|
|
|
+
|
|
|
+ const style = {
|
|
|
+ transform: CSS.Transform.toString(transform),
|
|
|
+ transition,
|
|
|
+ opacity: isDragging ? 0.5 : 1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 修复按钮点击问题:阻止事件冒泡
|
|
|
+ const handleButtonClick = useCallback((e: React.MouseEvent, callback: () => void) => {
|
|
|
+ e.stopPropagation() // 修复按钮点击无效问题:阻止事件冒泡到拖拽元素
|
|
|
+ callback()
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ if (file.type === 'img') {
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ ref={setNodeRef}
|
|
|
+ style={style}
|
|
|
+ className={classNames('ZTbox1ImgRow', isDragging ? 'dragging' : '')}
|
|
|
+ >
|
|
|
+ {/* 修复按钮点击无效问题:将拖拽手柄单独放置,不覆盖操作按钮 */}
|
|
|
+ <div
|
|
|
+ className='ZTbox1ImgRowDragHandle'
|
|
|
+ {...attributes}
|
|
|
+ {...listeners}
|
|
|
+ style={{ cursor: isLook ? 'default' : 'grab' }}
|
|
|
+ >
|
|
|
+ {file.thumb || file.filePath ? (
|
|
|
+ <ImageLazy noLook={true} width={100} height={100} src={file.thumb || file.filePath} />
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {oneIsCover && index === 0 ? <div className='ZTbox1ImgRowCover'>封面</div> : null}
|
|
|
+
|
|
|
+ <div className='ZTbox1ImgRowIcon'>
|
|
|
+ <EyeOutlined
|
|
|
+ onClick={e =>
|
|
|
+ handleButtonClick(e, () =>
|
|
|
+ store.dispatch({
|
|
|
+ type: 'layout/lookBigImg',
|
|
|
+ payload: {
|
|
|
+ url: baseURL + file.filePath,
|
|
|
+ show: true
|
|
|
+ }
|
|
|
+ })
|
|
|
+ )
|
|
|
+ }
|
|
|
+ rev={undefined}
|
|
|
+ />
|
|
|
+ <a
|
|
|
+ href={baseURL + file.filePath}
|
|
|
+ download
|
|
|
+ target='_blank'
|
|
|
+ rel='noreferrer'
|
|
|
+ onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
|
|
|
+ >
|
|
|
+ <DownloadOutlined rev={undefined} />
|
|
|
+ </a>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {!isLook && (
|
|
|
+ <MyPopconfirm
|
|
|
+ txtK='删除'
|
|
|
+ onConfirm={() => onDelete(file.id)}
|
|
|
+ Dom={
|
|
|
+ <CloseOutlined
|
|
|
+ className='ZTbox1ImgRowX'
|
|
|
+ rev={undefined}
|
|
|
+ onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
|
|
|
+ />
|
|
|
+ }
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ ref={setNodeRef}
|
|
|
+ style={style}
|
|
|
+ className={classNames('Z3filesRow', isDragging ? 'dragging' : '')}
|
|
|
+ >
|
|
|
+ {!isLook && (
|
|
|
+ <div className='dragHandle' {...attributes} {...listeners}>
|
|
|
+ <MenuOutlined rev={undefined} />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div className='Z3files1' title={file.fileName}>
|
|
|
+ {file.fileName}
|
|
|
+ </div>
|
|
|
+ <div className='Z3files2'>
|
|
|
+ {authFilesLookFu(file.fileName, '') ? (
|
|
|
+ <>
|
|
|
+ <EyeOutlined
|
|
|
+ rev={undefined}
|
|
|
+ title='查看'
|
|
|
+ onClick={e =>
|
|
|
+ handleButtonClick(e, () => authFilesLookFu(file.fileName, file.filePath))
|
|
|
+ }
|
|
|
+ />
|
|
|
+  
|
|
|
+ </>
|
|
|
+ ) : null}
|
|
|
+ <a
|
|
|
+ title='下载'
|
|
|
+ href={baseURL + file.filePath}
|
|
|
+ download={file.fileName}
|
|
|
+ target='_blank'
|
|
|
+ rel='noreferrer'
|
|
|
+ onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
|
|
|
+ >
|
|
|
+ <DownloadOutlined rev={undefined} />
|
|
|
+ </a>
|
|
|
+  
|
|
|
+ {!isLook && (
|
|
|
+ <Popconfirm
|
|
|
+ title='删除后无法恢复,是否删除?'
|
|
|
+ okText='删除'
|
|
|
+ cancelText='取消'
|
|
|
+ onConfirm={() => onDelete(file.id)}
|
|
|
+ okButtonProps={{ loading: false }}
|
|
|
+ >
|
|
|
+ <CloseOutlined
|
|
|
+ rev={undefined}
|
|
|
+ title='删除'
|
|
|
+ onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
|
|
|
+ />
|
|
|
+ </Popconfirm>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+// ------------------------------------------
|
|
|
+type Props = {
|
|
|
+ dirCode: string
|
|
|
+ myUrl: string
|
|
|
+ isLook?: boolean
|
|
|
+ fileCheck?: boolean
|
|
|
+ fromData?: any
|
|
|
+ tips?: string
|
|
|
+ size?: number
|
|
|
+ maxCount?: number
|
|
|
+ oneIsCover?: boolean
|
|
|
+ moduleId: number | ''
|
|
|
+ formDefId: number
|
|
|
+ ref: any
|
|
|
+}
|
|
|
+
|
|
|
+// 修复类型检查问题:统一使用dnd-kit管理所有拖拽
|
|
|
+function Z3upFilesRef(
|
|
|
+ {
|
|
|
+ isLook = false,
|
|
|
+ fileCheck = false,
|
|
|
+ dirCode,
|
|
|
+ myUrl,
|
|
|
+ fromData,
|
|
|
+ tips = '单个附件不得超过500M',
|
|
|
+ size = 500,
|
|
|
+ maxCount = 999,
|
|
|
+ oneIsCover,
|
|
|
+ moduleId,
|
|
|
+ formDefId
|
|
|
+ }: Props,
|
|
|
+ ref: any
|
|
|
+) {
|
|
|
+ const [list, setList] = useState<FileListType[]>([])
|
|
|
+
|
|
|
+ // 修复图片闪动问题:分别管理图片和非图片的排序
|
|
|
+ const { imageFiles, otherFiles } = useMemo(() => {
|
|
|
+ const arr = list || []
|
|
|
+ const imgArr = arr.filter(v => v.type === 'img')
|
|
|
+ const otherArr = arr.filter(v => v.type !== 'img')
|
|
|
+ return { imageFiles: imgArr, otherFiles: otherArr }
|
|
|
+ }, [list])
|
|
|
+
|
|
|
+ const fileList = useMemo(() => {
|
|
|
+ return [...imageFiles, ...otherFiles]
|
|
|
+ }, [imageFiles, otherFiles])
|
|
|
+
|
|
|
+ const [activeId, setActiveId] = useState<number | null>(null)
|
|
|
+ const myInput = useRef<HTMLInputElement>(null)
|
|
|
+
|
|
|
+ // 修复性能问题:使用useMemo缓存文件ID数组
|
|
|
+ const fileIds = useMemo(() => fileList.map(f => f.id), [fileList])
|
|
|
+
|
|
|
+ // 上传多个文件
|
|
|
+ const handeUpPhoto = useCallback(
|
|
|
+ async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
+ if (!e.target.files || e.target.files.length === 0) return
|
|
|
+
|
|
|
+ const files = Array.from(e.target.files)
|
|
|
+
|
|
|
+ if (files.length + fileList.length > maxCount)
|
|
|
+ return MessageFu.warning(
|
|
|
+ `最多可上传${maxCount}个文件,当前选中${files.length}个文件,还可上传${maxCount - fileList.length}个文件`
|
|
|
+ )
|
|
|
+
|
|
|
+ // 逐个上传文件
|
|
|
+ for (const file of files) {
|
|
|
+ if (size && file.size > size * 1024 * 1024) {
|
|
|
+ MessageFu.warning(`文件"${file.name}"超过${size}M限制!`)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ const typeRes = fileTypeRes(file.name)
|
|
|
+ const fd = new FormData()
|
|
|
+ fd.append('type', typeRes)
|
|
|
+ fd.append('dirCode', dirCode)
|
|
|
+ fd.append('isCompress', 'true')
|
|
|
+ fd.append('isDb', 'true')
|
|
|
+ if (moduleId) fd.append('moduleId', moduleId + '')
|
|
|
+ if (formDefId) fd.append('formDefId', formDefId + '')
|
|
|
+ fd.append('file', file)
|
|
|
+
|
|
|
+ if (fromData) {
|
|
|
+ for (const k in fromData) {
|
|
|
+ if (fromData[k]) fd.append(k, fromData[k])
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ e.target.value = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await API_upFile(fd, myUrl)
|
|
|
+ if (res.code === 0) {
|
|
|
+ MessageFu.success(
|
|
|
+ `上传成功${files.length > 1 ? `(${files.indexOf(file) + 1}/${files.length})` : ''}`
|
|
|
+ )
|
|
|
+ setList(prev => [...prev, res.data])
|
|
|
+ fileDomInitialFu()
|
|
|
+ } else {
|
|
|
+ fileDomInitialFu()
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ fileDomInitialFu()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [dirCode, fileList.length, formDefId, fromData, maxCount, moduleId, myUrl, size]
|
|
|
+ )
|
|
|
+
|
|
|
+ // 修复拖拽逻辑问题:统一使用dnd-kit管理拖拽
|
|
|
+ const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
|
+ setActiveId(Number(event.active.id))
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 修复拖拽交换问题:增加类型检查,只有同类型文件才能交换
|
|
|
+ const handleDragEnd = useCallback(
|
|
|
+ (event: DragEndEvent) => {
|
|
|
+ const { active, over } = event
|
|
|
+ setActiveId(null)
|
|
|
+
|
|
|
+ if (over && active.id !== over.id) {
|
|
|
+ const activeFile = fileList.find(item => item.id === active.id)
|
|
|
+ const overFile = fileList.find(item => item.id === over.id)
|
|
|
+
|
|
|
+ // 修复类型检查问题:只有同类型文件才能交换位置
|
|
|
+ if (activeFile && overFile && activeFile.type === overFile.type) {
|
|
|
+ setList(items => {
|
|
|
+ const oldIndex = items.findIndex(item => item.id === active.id)
|
|
|
+ const newIndex = items.findIndex(item => item.id === over.id)
|
|
|
+ return arrayMove(items, oldIndex, newIndex)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [fileList]
|
|
|
+ )
|
|
|
+
|
|
|
+ // 列表删除某一个文件
|
|
|
+ const delImgListFu = useCallback(
|
|
|
+ (id: number) => {
|
|
|
+ setList(prev => prev.filter(v => v.id !== id))
|
|
|
+ },
|
|
|
+ [setList]
|
|
|
+ )
|
|
|
+
|
|
|
+ // 获取当前拖拽的文件
|
|
|
+ const activeFile = useMemo(() => {
|
|
|
+ return activeId ? fileList.find(file => file.id === activeId) : null
|
|
|
+ }, [activeId, fileList])
|
|
|
+
|
|
|
+ // 修复拖拽覆盖层显示问题
|
|
|
+ const renderDragOverlay = useCallback(() => {
|
|
|
+ if (!activeFile) return null
|
|
|
+
|
|
|
+ if (activeFile.type === 'img') {
|
|
|
+ return (
|
|
|
+ <div className={classNames('ZTbox1ImgRow', 'dragOverlay')}>
|
|
|
+ {activeFile.thumb || activeFile.filePath ? (
|
|
|
+ <ImageLazy
|
|
|
+ noLook={true}
|
|
|
+ width={100}
|
|
|
+ height={100}
|
|
|
+ src={activeFile.thumb || activeFile.filePath}
|
|
|
+ />
|
|
|
+ ) : null}
|
|
|
+ <div className='ZTbox1ImgRowIcon' style={{ opacity: 0.5 }}>
|
|
|
+ <EyeOutlined rev={undefined} />
|
|
|
+ <DownloadOutlined rev={undefined} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ return (
|
|
|
+ <div className={classNames('Z3filesRow', 'dragOverlay')}>
|
|
|
+ <div className='dragHandle'>
|
|
|
+ <MenuOutlined rev={undefined} />
|
|
|
+ </div>
|
|
|
+ <div className='Z3files1' title={activeFile.fileName}>
|
|
|
+ {activeFile.fileName}
|
|
|
+ </div>
|
|
|
+ <div className='Z3files2'>{/* 拖拽时隐藏操作按钮 */}</div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }, [activeFile])
|
|
|
+
|
|
|
+ // 设置数据
|
|
|
+ const sonSetListFu = useCallback((list: FileListType[]) => {
|
|
|
+ setList(list)
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 返回数据
|
|
|
+ const sonResListFu = useCallback(() => {
|
|
|
+ const obj = {
|
|
|
+ list: fileList || [],
|
|
|
+ thumb: '',
|
|
|
+ thumbPc: ''
|
|
|
+ }
|
|
|
+ if (oneIsCover && fileList.length) {
|
|
|
+ const findObj = fileList.find(v => v.type === 'img')
|
|
|
+
|
|
|
+ if (findObj) {
|
|
|
+ obj.thumb = findObj.thumb
|
|
|
+ obj.thumbPc = findObj.filePath
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return obj
|
|
|
+ }, [fileList, oneIsCover])
|
|
|
+
|
|
|
+ // 可以让父组件调用子组件的方法
|
|
|
+ useImperativeHandle(ref, () => ({
|
|
|
+ sonSetListFu,
|
|
|
+ sonResListFu
|
|
|
+ }))
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className={styles.Z3upFilesRef}>
|
|
|
+ <input
|
|
|
+ id='upInput'
|
|
|
+ type='file'
|
|
|
+ ref={myInput}
|
|
|
+ onChange={handeUpPhoto}
|
|
|
+ multiple
|
|
|
+ style={{ display: 'none' }}
|
|
|
+ />
|
|
|
+ <div className='Z3Btn'>
|
|
|
+ {!isLook && (
|
|
|
+ <>
|
|
|
+ <Button
|
|
|
+ onClick={() => myInput.current?.click()}
|
|
|
+ icon={<UploadOutlined />}
|
|
|
+ type='primary'
|
|
|
+ >
|
|
|
+ 上传附件
|
|
|
+ </Button>
|
|
|
+  {oneIsCover ? <span>第一张图片将用作封面</span> : ''}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div className='Z3files'>
|
|
|
+ <DndContext
|
|
|
+ collisionDetection={closestCenter}
|
|
|
+ onDragStart={handleDragStart}
|
|
|
+ onDragEnd={handleDragEnd}
|
|
|
+ >
|
|
|
+ <SortableContext items={fileIds} strategy={verticalListSortingStrategy}>
|
|
|
+ {fileList.map((file, index) => (
|
|
|
+ <SortableFileItem
|
|
|
+ key={file.id}
|
|
|
+ file={file}
|
|
|
+ onDelete={delImgListFu}
|
|
|
+ isLook={isLook}
|
|
|
+ index={index}
|
|
|
+ oneIsCover={oneIsCover}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </SortableContext>
|
|
|
+ <DragOverlay>{renderDragOverlay()}</DragOverlay>
|
|
|
+ </DndContext>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className='fileTit' hidden={isLook}>
|
|
|
+ {tips}
|
|
|
+ ;支持按住Ctrl键选择多个文件上传;按住鼠标拖动图片 / 拖动附件左侧图标 可调整顺序
|
|
|
+ <div
|
|
|
+ className={classNames(
|
|
|
+ 'noUpThumb',
|
|
|
+ fileList.length <= 0 && fileCheck ? 'noUpThumbAc' : ''
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ 请上传文件
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {isLook && fileList.length <= 0 ? (
|
|
|
+ <div style={{ height: 32, lineHeight: '32px' }}>(空)</div>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export default forwardRef(Z3upFilesRef)
|