| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- 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 { authFilesLookFu, FileListType } from '../Z3upFiles/data'
- import { forwardRef, useImperativeHandle } from 'react'
- // ----------------这个组件用于外面一级的附件上传
- 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 | ''
- ref: any
- }
- // 修复类型检查问题:统一使用dnd-kit管理所有拖拽
- function Z3upFilesRef(
- {
- isLook = false,
- fileCheck = false,
- dirCode,
- myUrl,
- fromData,
- tips = '单个附件不得超过500M',
- size = 500,
- maxCount = 999,
- oneIsCover,
- moduleId
- }: 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) {
- // 后端要求,这个模块不要传moduleId
- if (!['cms/order/register/son/upload'].includes(myUrl))
- fd.append('moduleId', moduleId + '')
- }
- 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 => [res.data, ...prev])
- fileDomInitialFu()
- } else {
- fileDomInitialFu()
- }
- } catch (error) {
- fileDomInitialFu()
- }
- }
- },
- [dirCode, fileList.length, 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 rev={undefined} />}
- >
- 上传文件
- </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)
|