shaogen1995 il y a 3 semaines
Parent
commit
962124e848

+ 12 - 5
src/components/Z3upFiles/index.module.scss

@@ -4,18 +4,25 @@
       color: black;
     }
     .Z3files {
-      display: flex;
-      flex-wrap: wrap;
-
       .ZTbox1ImgRow {
-        // content-visibility:auto;
+        display: inline-block;
         margin: 15px 20px 10px 0;
         width: 100px;
         height: 125px;
         position: relative;
-        cursor: move;
         position: relative;
 
+        // 修复图片闪动问题:确保拖拽手柄有固定尺寸
+        .ZTbox1ImgRowDragHandle {
+          width: 100px;
+          height: 100px;
+          cursor: grab;
+
+          &:active {
+            cursor: grabbing;
+          }
+        }
+
         // 第一张作为封面
         .ZTbox1ImgRowCover {
           font-size: 12px;

+ 211 - 165
src/components/Z3upFiles/index.tsx

@@ -29,60 +29,204 @@ import ImageLazy from '../ImageLazy'
 import store from '@/store'
 import MyPopconfirm from '../MyPopconfirm'
 
-// 可排序的文件项组件
+// ----------------这个组件用于外面一级的附件上传
+
 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}>
+            {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))
+                  }
+                />
+                &emsp;
+              </>
+            ) : null}
+            <a
+              title='下载'
+              href={baseURL + file.filePath}
+              download={file.fileName}
+              target='_blank'
+              rel='noreferrer'
+              onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+            >
+              <DownloadOutlined rev={undefined} />
+            </a>
+            &emsp;
+            {!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 = {
-  isLook: boolean // 是否是查看
-  fileCheck: boolean
-  dirCode: string // 文件的code码
+  dirCode: string
   myUrl: string
+  isLook?: boolean
+  fileCheck?: boolean
   fromData?: any
   tips?: string
-  // 文件大小
   size?: number
-  // 最大文件数量
   maxCount?: number
-  // 订单id
-  moduleId: number
-  oneIsCover?: boolean //是否将第一张作为封面
+  oneIsCover?: boolean
 }
 
+// 修复类型检查问题:统一使用dnd-kit管理所有拖拽
 function Z3upFiles({
-  isLook,
-  fileCheck,
+  isLook = false,
+  fileCheck = false,
   dirCode,
   myUrl,
   fromData,
   tips = '单个附件不得超过500M',
   size = 500,
-  moduleId,
   maxCount = 999,
   oneIsCover
 }: Props) {
   const { info, setInfoFu } = useInfo()
 
-  const fileList = useMemo(() => {
-    return info.files || []
+  // 修复图片闪动问题:分别管理图片和非图片的排序
+  const { imageFiles, otherFiles } = useMemo(() => {
+    const arr = info.files || []
+    const imgArr = arr.filter(v => v.type === 'img')
+    const otherArr = arr.filter(v => v.type !== 'img')
+    return { imageFiles: imgArr, otherFiles: otherArr }
   }, [info.files])
 
+  const fileList = useMemo(() => {
+    return [...imageFiles, ...otherFiles]
+  }, [imageFiles, otherFiles])
+
+  // 修复性能问题:使用useCallback优化函数记忆
   const setFileList = useCallback(
     (newFiles: FileListType[] | ((prev: FileListType[]) => FileListType[])) => {
       setInfoFu((prevInfo: Typetable) => {
         const currentFiles = prevInfo.files || []
         const updatedFiles = typeof newFiles === 'function' ? newFiles(currentFiles) : newFiles
 
-        // 明确指定返回类型
         const updatedInfo: Typetable = {
           ...prevInfo,
           files: updatedFiles
         }
-
         return updatedInfo
       })
     },
@@ -92,6 +236,9 @@ function Z3upFiles({
   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>) => {
@@ -106,19 +253,17 @@ function Z3upFiles({
 
       // 逐个上传文件
       for (const file of files) {
-        // 校验大小
         if (size && file.size > size * 1024 * 1024) {
           MessageFu.warning(`文件"${file.name}"超过${size}M限制!`)
           continue
         }
         const typeRes = fileTypeRes(file.name)
-        // 创建FormData对象
         const fd = new FormData()
         fd.append('type', typeRes)
         fd.append('dirCode', dirCode)
         fd.append('isCompress', 'true')
         fd.append('isDb', 'true')
-        fd.append('moduleId', moduleId + '')
+        fd.append('moduleId', info.id + '')
         fd.append('file', file)
 
         if (fromData) {
@@ -126,7 +271,7 @@ function Z3upFiles({
             if (fromData[k]) fd.append(k, fromData[k])
           }
         }
-        // 清空input值
+
         e.target.value = ''
 
         try {
@@ -135,7 +280,7 @@ function Z3upFiles({
             MessageFu.success(
               `上传成功${files.length > 1 ? `(${files.indexOf(file) + 1}/${files.length})` : ''}`
             )
-            setFileList(prev => [...prev, res.data])
+            setFileList(prev => [res.data, ...prev])
             fileDomInitialFu()
           } else {
             fileDomInitialFu()
@@ -145,30 +290,35 @@ function Z3upFiles({
         }
       }
     },
-    [dirCode, fileList.length, fromData, maxCount, moduleId, myUrl, setFileList, size]
+    [dirCode, fileList.length, fromData, info.id, maxCount, myUrl, setFileList, 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) {
-        setFileList(items => {
-          const oldIndex = items.findIndex(item => item.id === active.id)
-          const newIndex = items.findIndex(item => item.id === over.id)
-
-          return arrayMove(items, oldIndex, newIndex)
-        })
+        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) {
+          setFileList(items => {
+            const oldIndex = items.findIndex(item => item.id === active.id)
+            const newIndex = items.findIndex(item => item.id === over.id)
+            return arrayMove(items, oldIndex, newIndex)
+          })
+        }
       }
     },
-    [setFileList]
+    [fileList, setFileList]
   )
 
   // 列表删除某一个文件
@@ -182,136 +332,41 @@ function Z3upFiles({
   // 获取当前拖拽的文件
   const activeFile = activeId ? fileList.find(file => file.id === activeId) : null
 
-  // 附件图片的拖动
-  const [dragImg, setDragImg] = useState<any>(null)
+  // 修复拖拽覆盖层显示问题
+  const renderDragOverlay = useCallback(() => {
+    if (!activeFile) return null
 
-  const handleDragOver = useCallback(
-    (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
-      if (isLook) return
-      e.dataTransfer.dropEffect = 'move'
-    },
-    [isLook]
-  )
-
-  const handleDragEnter = useCallback(
-    (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
-      if (isLook) return
-
-      e.dataTransfer.effectAllowed = 'move'
-      if (item === dragImg) return
-      const newItems = [...fileList] //拷贝一份数据进行交换操作。
-      const src = newItems.indexOf(dragImg) //获取数组下标
-      const dst = newItems.indexOf(item)
-      newItems.splice(dst, 0, ...newItems.splice(src, 1)) //交换位置
-      setFileList({ ...newItems })
-    },
-    [dragImg, fileList, isLook, setFileList]
-  )
-
-  // 详情子函数--------------------
-  const SortableFileItem = ({ file, onDelete, isLook, index }: SortableFileItemProps) => {
-    const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
-      id: file.id
-    })
-
-    const style = {
-      transform: CSS.Transform.toString(transform),
-      transition,
-      opacity: isDragging ? 0.5 : 1
-    }
-    if (file.type === 'img') {
+    if (activeFile.type === 'img') {
       return (
-        <div
-          className='ZTbox1ImgRow'
-          draggable='true'
-          onDragStart={() => setDragImg(file)}
-          onDragOver={e => handleDragOver(e, file)}
-          onDragEnter={e => handleDragEnter(e, file)}
-          onDragEnd={() => setDragImg(null)}
-        >
-          {file.thumb || file.filePath ? (
-            <ImageLazy noLook={true} width={100} height={100} src={file.thumb || file.filePath} />
-          ) : null}
-
-          {oneIsCover && index === 0 ? <div className='ZTbox1ImgRowCover'>封面</div> : null}
-
-          <div className='ZTbox1ImgRowIcon'>
-            <EyeOutlined
-              onClick={() =>
-                store.dispatch({
-                  type: 'layout/lookBigImg',
-                  payload: {
-                    url: baseURL + file.filePath,
-                    show: true
-                  }
-                })
-              }
-              rev={undefined}
+        <div className={classNames('ZTbox1ImgRow', 'dragOverlay')}>
+          {activeFile.thumb || activeFile.filePath ? (
+            <ImageLazy
+              noLook={true}
+              width={100}
+              height={100}
+              src={activeFile.thumb || activeFile.filePath}
             />
-            <a href={baseURL + file.filePath} download target='_blank' rel='noreferrer'>
-              <DownloadOutlined rev={undefined} />
-            </a>
+          ) : null}
+          <div className='ZTbox1ImgRowIcon' style={{ opacity: 0.5 }}>
+            <EyeOutlined rev={undefined} />
+            <DownloadOutlined rev={undefined} />
           </div>
-
-          <MyPopconfirm
-            txtK='删除'
-            onConfirm={() => delImgListFu(file.id)}
-            Dom={<CloseOutlined className='ZTbox1ImgRowX' rev={undefined} />}
-          />
         </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 className={classNames('Z3filesRow', 'dragOverlay')}>
+          <div className='dragHandle'>
+            <MenuOutlined rev={undefined} />
           </div>
-          <div className='Z3files2'>
-            {authFilesLookFu(file.fileName, '') ? (
-              <>
-                <EyeOutlined
-                  rev={undefined}
-                  title='查看'
-                  onClick={() => authFilesLookFu(file.fileName, file.filePath)}
-                />
-                &emsp;
-              </>
-            ) : null}
-            <a
-              title='下载'
-              href={baseURL + file.filePath}
-              download={file.fileName}
-              target='_blank'
-              rel='noreferrer'
-            >
-              <DownloadOutlined rev={undefined} />
-            </a>
-            &emsp;
-            {!isLook && (
-              <Popconfirm
-                title='删除后无法恢复,是否删除?'
-                okText='删除'
-                cancelText='取消'
-                onConfirm={() => onDelete(file.id)}
-                okButtonProps={{ loading: false }}
-              >
-                <CloseOutlined rev={undefined} title='删除' />
-              </Popconfirm>
-            )}
+          <div className='Z3files1' title={activeFile.fileName}>
+            {activeFile.fileName}
           </div>
+          <div className='Z3files2'>{/* 拖拽时隐藏操作按钮 */}</div>
         </div>
       )
     }
-  }
+  }, [activeFile])
 
   return (
     <div className={styles.Z3upFiles}>
@@ -320,7 +375,8 @@ function Z3upFiles({
         type='file'
         ref={myInput}
         onChange={handeUpPhoto}
-        multiple // 支持多选
+        multiple
+        style={{ display: 'none' }}
       />
       <div className='Z3Btn'>
         {!isLook && (
@@ -338,7 +394,7 @@ function Z3upFiles({
             onDragStart={handleDragStart}
             onDragEnd={handleDragEnd}
           >
-            <SortableContext items={fileList.map(f => f.id)} strategy={verticalListSortingStrategy}>
+            <SortableContext items={fileIds} strategy={verticalListSortingStrategy}>
               {fileList.map((file, index) => (
                 <SortableFileItem
                   key={file.id}
@@ -346,27 +402,17 @@ function Z3upFiles({
                   onDelete={delImgListFu}
                   isLook={isLook}
                   index={index}
+                  oneIsCover={oneIsCover}
                 />
               ))}
             </SortableContext>
-            <DragOverlay>
-              {activeFile ? (
-                <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>
-              ) : null}
-            </DragOverlay>
+            <DragOverlay>{renderDragOverlay()}</DragOverlay>
           </DndContext>
         </div>
 
         <div className='fileTit' hidden={isLook}>
-          {tips};支持按住Ctrl键选择多个文件;按住鼠标拖动图片/拖动附件左侧图标 可调整顺序
+          {tips}
+          ;支持按住Ctrl键选择多个文件上传;按住鼠标拖动图片 / 拖动附件左侧图标 可调整顺序
           <div
             className={classNames(
               'noUpThumb',
@@ -384,4 +430,4 @@ function Z3upFiles({
   )
 }
 
-export default Z3upFiles
+export default React.memo(Z3upFiles)

+ 151 - 0
src/components/Z3upFilesRef/index.module.scss

@@ -0,0 +1,151 @@
+.Z3upFilesRef {
+  :global {
+    a {
+      color: black;
+    }
+    .Z3files {
+      .ZTbox1ImgRow {
+        display: inline-block;
+        margin: 15px 20px 10px 0;
+        width: 100px;
+        height: 125px;
+        position: relative;
+        position: relative;
+
+        // 修复图片闪动问题:确保拖拽手柄有固定尺寸
+        .ZTbox1ImgRowDragHandle {
+          width: 100px;
+          height: 100px;
+          cursor: grab;
+
+          &:active {
+            cursor: grabbing;
+          }
+        }
+
+        // 第一张作为封面
+        .ZTbox1ImgRowCover {
+          font-size: 12px;
+          line-height: 22px;
+          position: absolute;
+          left: 0;
+          top: 0;
+          width: 100%;
+          height: 24px;
+          background-color: rgba(0, 0, 0, 0.8);
+          color: #fff;
+          text-align: center;
+          pointer-events: none;
+        }
+
+        .ZTbox1ImgRowIcon {
+          width: 100%;
+          background-color: rgba(0, 0, 0, 0.6);
+          color: #fff;
+          display: flex;
+          justify-content: space-around;
+          font-size: 16px;
+
+          a {
+            color: #fff !important;
+          }
+        }
+
+        .ZTbox1ImgRowX {
+          cursor: pointer;
+          position: absolute;
+          right: -10px;
+          top: -10px;
+          z-index: 99;
+          background-color: rgba(0, 0, 0, 0.8);
+          width: 20px;
+          height: 20px;
+          border-radius: 50%;
+          font-size: 16px;
+          color: #fff;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+        }
+      }
+
+      .Z3filesRow {
+        width: 100%;
+        display: flex;
+        align-items: center;
+        padding: 0px 12px;
+        border: 1px solid #d9d9d9;
+        border-radius: 6px;
+        margin: 2px;
+        background: #fff;
+        transition: all 0.3s;
+
+        &:hover {
+          border-color: #40a9ff;
+          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        }
+
+        &.dragging {
+          opacity: 0.5;
+          background: #f0f0f0;
+        }
+
+        &.dragOverlay {
+          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+          transform: rotate(5deg);
+        }
+
+        .dragHandle {
+          cursor: grab;
+          margin-right: 12px;
+          color: #999;
+          padding: 4px;
+
+          &:active {
+            cursor: grabbing;
+          }
+
+          &:hover {
+            color: #40a9ff;
+          }
+        }
+
+        .Z3files1 {
+          flex: 1;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        .Z3files2 {
+          display: flex;
+          align-items: center;
+
+          .anticon {
+            cursor: pointer;
+            color: #666;
+
+            &:hover {
+              color: #40a9ff;
+            }
+          }
+        }
+      }
+    }
+
+    .fileTit {
+      margin-top: 5px;
+      color: #666;
+      font-size: 12px;
+
+      .noUpThumb {
+        display: none;
+
+        &.noUpThumbAc {
+          display: block;
+          color: #ff4d4f;
+        }
+      }
+    }
+  }
+}

+ 446 - 0
src/components/Z3upFilesRef/index.tsx

@@ -0,0 +1,446 @@
+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}>
+            {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))
+                  }
+                />
+                &emsp;
+              </>
+            ) : null}
+            <a
+              title='下载'
+              href={baseURL + file.filePath}
+              download={file.fileName}
+              target='_blank'
+              rel='noreferrer'
+              onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+            >
+              <DownloadOutlined rev={undefined} />
+            </a>
+            &emsp;
+            {!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')
+        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 = activeId ? fileList.find(file => file.id === activeId) : null
+
+  // 修复拖拽覆盖层显示问题
+  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) {
+      obj.thumb = fileList[0].thumb
+      obj.thumbPc = fileList[0].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>
+        )}
+
+        <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)

+ 209 - 0
src/pages/Eculture/E1tag/E1add.tsx

@@ -0,0 +1,209 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { Button, Cascader, Form, FormInstance, Input, InputNumber, Modal } from 'antd'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import { MessageFu } from '@/utils/message'
+import TextArea from 'antd/es/input/TextArea'
+import { I5AddInfoType, TypeI5Tree } from '@/pages/Isystem/I5organization/data'
+import { E1_APIgetInfo, E1_APIsave } from '@/store/action/Eculture/E1tag'
+
+type Props = {
+  addInfo: I5AddInfoType
+  addFu: () => void
+  closeFu: () => void
+}
+
+function E1add({ addInfo, addFu, closeFu }: Props) {
+  const treeData = useSelector((state: RootState) => state.E1tag.treeData)
+
+  // 级联选择器改变的时候 筛选当前级联的 信息出来
+  const [acCardInfo, setAcCardInfo] = useState({} as TypeI5Tree)
+  const [parentIdArr, setParentIdArr] = useState<string[] | null>(null)
+
+  // useEffect(() => {
+  //   console.log(acCardInfo, parentIdArr)
+  // }, [acCardInfo, parentIdArr])
+
+  useEffect(() => {
+    setAcCardInfo(addInfo.acInfo)
+
+    let ids: string[] | null = addInfo.acInfo.id ? [addInfo.acInfo.id] : null
+
+    if (addInfo.acInfo.ancestor) ids = [...addInfo.acInfo.ancestor.split(','), addInfo.acInfo.id]
+
+    let idsRes = ids
+    if (ids && ids.length >= 1 && addInfo.txt === '编辑') {
+      ids.pop()
+    }
+
+    if (idsRes) idsRes = idsRes.filter((v, i) => v !== '0')
+
+    setParentIdArr(idsRes)
+  }, [addInfo.acInfo, addInfo.txt])
+
+  const cardChange = useCallback((aa: any, bb: any) => {
+    setParentIdArr(aa)
+
+    if (bb && bb.length) setAcCardInfo(bb[bb.length - 1])
+    else setAcCardInfo({} as TypeI5Tree)
+  }, [])
+
+  const getInfoFu = useCallback(async (id: string) => {
+    const res = await E1_APIgetInfo(id)
+    if (res.code === 0) {
+      FormBoxRef.current?.setFieldsValue({
+        ...res.data
+      })
+    }
+  }, [])
+  useEffect(() => {
+    if (addInfo.txt === '编辑') getInfoFu(addInfo.id)
+    else {
+      FormBoxRef.current?.setFieldsValue({
+        sort: 999
+      })
+    }
+  }, [addInfo.id, addInfo.txt, getInfoFu])
+
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<FormInstance>(null)
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    // return MessageFu.warning("有表单不符号规则!");
+  }, [])
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (values: any) => {
+      let ancestor = ''
+      let parentId: string | null = null
+
+      if (parentIdArr && parentIdArr.length >= 6 && addInfo.txt === '新增')
+        return MessageFu.warning('最多支持六级!')
+
+      if (acCardInfo) parentId = addInfo.txt === '编辑' ? acCardInfo.parentId : acCardInfo.id
+      if (parentIdArr && parentId) ancestor = parentIdArr.filter(v => v !== addInfo.id).join(',')
+
+      let level = 1
+      if (parentIdArr) {
+        level = addInfo.txt === '新增' ? acCardInfo.level + 1 : acCardInfo.level
+      }
+
+      // 补0
+      if (addInfo.id === '-1') {
+        // 新增
+        if (!parentId && !ancestor) {
+          ancestor = '0'
+          parentId = '0'
+        } else ancestor = '0,' + ancestor
+      } else {
+        // 编辑
+        if (!ancestor) ancestor = '0'
+        else ancestor = '0,' + ancestor
+      }
+
+      const obj = {
+        ...values,
+        id: addInfo.id === '-1' ? null : addInfo.id,
+        ancestor,
+        level,
+        parentId
+      }
+      // console.log(123, obj)
+      // if (1 + 1 === 2) {
+      //   return
+      // }
+
+      const res = await E1_APIsave(obj)
+
+      if (res.code === 0) {
+        MessageFu.success(addInfo.txt + '成功!')
+        addFu()
+        closeFu()
+      }
+    },
+    [acCardInfo, addFu, addInfo.id, addInfo.txt, closeFu, parentIdArr]
+  )
+
+  return (
+    <Modal
+      wrapClassName={styles.E1add}
+      open={true}
+      title={addInfo.txt}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className='E1aMain'>
+        <Form
+          scrollToFirstError={true}
+          ref={FormBoxRef}
+          name='basic'
+          labelCol={{ span: 3 }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete='off'
+        >
+          <div className='fromRow'>
+            <div className='fromRowll'>上级标签:</div>
+            <div className='fromRowrr'>
+              <Cascader
+                style={{ width: 658 }}
+                disabled={addInfo.txt === '编辑'}
+                changeOnSelect
+                fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+                options={treeData}
+                placeholder={addInfo.txt === '编辑' ? '空' : '请选择'}
+                value={parentIdArr ? [...parentIdArr] : []}
+                onChange={cardChange}
+              />
+            </div>
+          </div>
+
+          <Form.Item
+            label='标签名称'
+            name='name'
+            rules={[{ required: true, message: '请输入标签名称!' }]}
+            getValueFromEvent={e => e.target.value.replace(/\s+/g, '')}
+          >
+            <Input maxLength={20} showCount placeholder='请输入内容' />
+          </Form.Item>
+
+          <Form.Item label='标签说明' name='description'>
+            <TextArea maxLength={100} showCount placeholder='请输入内容' />
+          </Form.Item>
+
+          <div className='fromRow2'>
+            <Form.Item
+              label='排序值'
+              name='sort'
+              rules={[{ required: true, message: '请输入排序值!' }]}
+            >
+              <InputNumber min={1} max={999} precision={0} placeholder='请输入' />
+            </Form.Item>
+            <div className='fromRowTit'>
+              请输入1~999的数字。数字越小,排序越靠前。数字相同时,更新发布的内容排在前面
+            </div>
+          </div>
+
+          {/* 确定和取消按钮 */}
+          <br />
+          <Form.Item wrapperCol={{ offset: 9, span: 16 }}>
+            <Button type='primary' htmlType='submit'>
+              提交
+            </Button>
+            &emsp;
+            <MyPopconfirm txtK='取消' onConfirm={closeFu} />
+          </Form.Item>
+        </Form>
+      </div>
+    </Modal>
+  )
+}
+
+const MemoE1add = React.memo(E1add)
+
+export default MemoE1add

+ 134 - 0
src/pages/Eculture/E1tag/index.module.scss

@@ -1,4 +1,138 @@
 .E1tag {
+  background-color: var(--boxBcaColor);
+  border-radius: 10px;
+  padding: 24px;
   :global {
+    .E1top {
+      text-align: right;
+    }
+
+    .E1main {
+      margin-top: 15px;
+      height: calc(100% - 50px);
+      & > div {
+        height: 100%;
+      }
+      .E1Null {
+        width: 100%;
+        height: 300px;
+        font-size: 24px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+      .E1m1 {
+        display: flex;
+        justify-content: space-between;
+        .E1m1ll {
+          width: 50%;
+          overflow-y: auto;
+          .site-tree-search-value {
+            color: red;
+            font-weight: 700;
+          }
+          .ant-tree {
+            background-color: var(--boxBcaColor);
+            min-height: 100%;
+          }
+          .ant-tree-node-selected {
+            background-color: var(--themeColor2) !important;
+          }
+        }
+        .E1m1rr {
+          width: calc(50% - 24px);
+
+          .E1mr2 {
+            .E1mr2Row {
+              display: flex;
+              // align-items: center;
+              margin-bottom: 10px;
+              font-size: 20px;
+              .E1mr2Row1 {
+                font-weight: 700;
+                font-size: 20px;
+              }
+
+              .ant-btn {
+                font-size: 20px;
+                height: 46px;
+              }
+
+              .E1mr2Row2 {
+                width: calc(100% - 105px);
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+              }
+              .E1mr2Row2_1 {
+                width: calc(100% - 105px);
+              }
+            }
+            .E1mr2Row0 {
+              margin-bottom: 0;
+              align-items: center;
+            }
+            .E1mr2RowLast {
+              align-items: self-start;
+              .E1mr2Row2 {
+                display: flex;
+                flex-wrap: wrap;
+              }
+            }
+          }
+
+          .E1mr3 {
+            margin-top: 15px;
+            height: calc(100% - 100px);
+            background-color: red;
+          }
+        }
+      }
+    }
+  }
+}
+
+// 新增弹窗页面
+.E1add {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+
+    .ant-modal {
+      width: 800px !important;
+    }
+
+    .ant-modal-body {
+      border-top: 1px solid #ccc;
+    }
+
+    .E1aMain {
+      padding-top: 15px;
+      .fromRow2 {
+        position: relative;
+
+        .fromRowTit {
+          position: absolute;
+          left: 200px;
+          top: 5px;
+          color: #999;
+          font-size: 12px;
+        }
+      }
+      .fromRow {
+        display: flex;
+        align-items: center;
+        margin-bottom: 24px;
+        .fromRowll {
+          width: 94px;
+          text-align: right;
+          position: relative;
+        }
+        .fromRowrr {
+          width: calc(100% - 94px);
+        }
+      }
+    }
   }
 }

+ 189 - 1
src/pages/Eculture/E1tag/index.tsx

@@ -1,9 +1,197 @@
-import React from 'react'
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
 import styles from './index.module.scss'
+import { Button, Input, Tree, TreeDataNode } from 'antd'
+import { useDispatch, useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import { MessageFu } from '@/utils/message'
+import { E1_APIdel, E1_APIgetTree } from '@/store/action/Eculture/E1tag'
+import { treeResIdFu } from '@/pages/Isystem/I2dict/data'
+import { I5AddInfoType, TypeI5Tree } from '@/pages/Isystem/I5organization/data'
+import E1add from './E1add'
+
 function E1tag() {
+  const dispatch = useDispatch()
+
+  const [loding, setLoding] = useState(false)
+
+  useEffect(() => {
+    dispatch(
+      E1_APIgetTree(data => {
+        setLoding(true)
+        if (data && data.length) setAcShu(data[0].id)
+      })
+    )
+  }, [dispatch])
+
+  const { treeData: treeDataTemp } = useSelector((state: RootState) => state.E1tag)
+
+  // 当前选中的树节点ID
+  const [acShu, setAcShu] = useState('')
+
+  // 点击树节点
+  const onSelect = (id: any) => {
+    if (id[0]) setAcShu(id[0])
+  }
+
+  // 搜索高亮
+  const [value, setValue] = useState('')
+
+  const treeData = useMemo(() => {
+    const loop = (dataTemp: any[]): TreeDataNode[] => {
+      const data = dataTemp || []
+
+      return data.map(item => {
+        const strTitle = ((item.num ? item.num + ' ' : '') + item.name) as string
+
+        const index = strTitle.indexOf(value)
+
+        const beforeStr = strTitle.substring(0, index)
+        const afterStr = strTitle.slice(index + value.length)
+        const title =
+          index > -1 ? (
+            <span key={item.id}>
+              {beforeStr}
+              <span className='site-tree-search-value'>{value}</span>
+              {afterStr}
+            </span>
+          ) : (
+            <span key={item.id}>{strTitle}</span>
+          )
+        if (item.children) {
+          return { title, key: item.id, children: loop(item.children) }
+        }
+        return {
+          title,
+          key: item.id
+        }
+      })
+    }
+
+    return loop(treeDataTemp)
+  }, [treeDataTemp, value])
+
+  // 右侧信息
+  const rightData = useMemo(() => {
+    let obj = {} as TypeI5Tree
+
+    obj = treeResIdFu(treeDataTemp, acShu)
+
+    return obj || {}
+  }, [acShu, treeDataTemp])
+
+  // 点击删除
+  const delTree = useCallback(
+    async (id: string) => {
+      const res = await E1_APIdel(id)
+      if (res.code === 0) {
+        MessageFu.success('删除成功!')
+        dispatch(E1_APIgetTree())
+      }
+    },
+    [dispatch]
+  )
+
+  // 点击新增和编辑
+  const [addInfo, setAddInfo] = useState({} as I5AddInfoType)
+
+  const addSonFu = useCallback(
+    (id: string) => {
+      setAddInfo({
+        id,
+        txt: id === '-1' ? '新增' : '编辑',
+        acInfo: rightData
+      })
+    },
+    [rightData]
+  )
+
   return (
     <div className={styles.E1tag}>
       <div className='pageTitle'>标签管理</div>
+      <div className='E1top'>
+        <Input value={value} onChange={e => setValue(e.target.value)} hidden />
+        <Button
+          type='primary'
+          onClick={() => setAddInfo({ id: '-1', txt: '新增', acInfo: rightData })}
+        >
+          新增
+        </Button>
+      </div>
+
+      {/* 主体 */}
+      <div className='E1main'>
+        <div className='E1m1'>
+          <div className='E1m1ll'>
+            {treeDataTemp && treeDataTemp.length ? (
+              <Tree
+                // 默认全部展开
+                defaultExpandAll={true}
+                // 数据
+                treeData={treeData}
+                // 自定义字段
+                // fieldNames={{ title: 'title', key: 'key', children: 'children' }}
+                // 选中
+                selectedKeys={[acShu]}
+                // 点击
+                onSelect={onSelect}
+              />
+            ) : loding ? (
+              <div className='E1Null'>暂无数据</div>
+            ) : null}
+          </div>
+          <div className='E1m1rr'>
+            {rightData.id ? (
+              <>
+                <div className='E1mr2'>
+                  <div className='E1mr2Row E1mr2Row0'>
+                    <div className='E1mr2Row1'>操作:</div>
+                    <div>
+                      <Button type='text' onClick={() => addSonFu(rightData.id)}>
+                        编辑
+                      </Button>
+                      &emsp;
+                      <MyPopconfirm txtK='删除' onConfirm={() => delTree(rightData.id)} />
+                    </div>
+                  </div>
+
+                  <div className='E1mr2Row'>
+                    <div className='E1mr2Row1'>标签名称:</div>
+                    <div>{rightData.name}</div>
+                  </div>
+
+                  <div className='E1mr2Row'>
+                    <div className='E1mr2Row1'>标签说明:</div>
+                    <div className='E1mr2Row2_1'>{rightData.description || '(空)'}</div>
+                  </div>
+
+                  <div className='E1mr2Row'>
+                    <div className='E1mr2Row1'>层级:</div>
+                    <div>{rightData.level}</div>
+                  </div>
+                  {/* <div className='E1mr2Row'>
+                    <div className='E1mr2Row1'>id:</div>
+                    <div>{rightData.id}</div>
+                  </div> */}
+
+                  <div className='E1mr2Row'>
+                    <div className='E1mr2Row1'>排序值:</div>
+                    <div>{rightData.sort}</div>
+                  </div>
+                </div>
+              </>
+            ) : null}
+          </div>
+        </div>
+      </div>
+      {/* 新增/编辑页面 中图法分类 */}
+      {addInfo.id ? (
+        <E1add
+          addInfo={addInfo}
+          addFu={() => dispatch(E1_APIgetTree())}
+          closeFu={() => setAddInfo({} as I5AddInfoType)}
+        />
+      ) : null}
     </div>
   )
 }

+ 3 - 3
src/pages/Login/index.tsx

@@ -61,9 +61,9 @@ export default function Login() {
       const urlAll = window.location.href
       if (urlAll.includes('?back=')) {
         const url = urlAll.split('?back=')[1]
-        if (url) history.push(`/${url}`)
-        else history.push('/')
-      } else history.push('/')
+        if (url) history.replace(`/${url}`)
+        else history.replace('/')
+      } else history.replace('/')
     } else if (res.code === 3014) {
       MessageFu.warning('账号不存在或密码错误,请联系管理员!')
     } else if (res.code === -1 && res.msg === '验证码有误') {

+ 105 - 0
src/pages/Zother/AddGoods/index.module.scss

@@ -0,0 +1,105 @@
+.AddGoods {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 99;
+  background-color: rgba(0, 0, 0, 0.8);
+  padding: 50px 100px;
+  :global {
+    .AGbox {
+      width: 100%;
+      height: 100%;
+      background-color: #fff;
+      border-radius: 10px;
+      padding: 20px 20px 0;
+      .AG1 {
+        font-weight: 700;
+        padding-bottom: 10px;
+        border-bottom: 1px solid #ccc;
+      }
+      .ant-form {
+        height: calc(100% - 40px);
+      }
+      .AGmain {
+        width: 100%;
+        height: 100%;
+        padding-right: 150px;
+        display: flex;
+        flex-wrap: wrap;
+        padding-top: 10px;
+        justify-content: space-between;
+        overflow-y: auto;
+        padding-bottom: 30px;
+        .ant-form-item {
+          width: 48%;
+          .ant-form-item-label {
+            font-weight: 700;
+            width: 110px;
+          }
+          .ant-form-item-control {
+            width: 100%;
+            .ant-picker,
+            .ant-input-number {
+              width: 100%;
+            }
+          }
+        }
+        .AGrow {
+          display: flex;
+          width: 48%;
+          .AGrow1 {
+            width: 70%;
+          }
+          .AGrow2 {
+            width: 30%;
+            .ant-form-item-label {
+              display: none;
+            }
+          }
+        }
+        .AGrowSize {
+          position: relative;
+          display: flex;
+          width: 48%;
+
+          & > div {
+            width: 30%;
+            &:nth-of-type(2) {
+              width: 40%;
+            }
+          }
+          .AGrowSizeTxt {
+            width: 100% !important;
+            position: absolute;
+            top: -30px;
+            left: 110px;
+            font-size: 14px;
+            color: #999;
+          }
+        }
+        .AGxian {
+          height: 1px;
+          width: 100%;
+          background-color: #ccc;
+          margin-bottom: 24px;
+        }
+        .AGfull {
+          width: 100%;
+          display: flex;
+          .AGfullll {
+            width: 110px;
+            position: relative;
+            top: 4px;
+            text-align: right;
+            font-weight: 700;
+          }
+          .AGfullrr {
+            width: calc(100% - 110px);
+          }
+        }
+      }
+    }
+  }
+}

+ 305 - 0
src/pages/Zother/AddGoods/index.tsx

@@ -0,0 +1,305 @@
+import React, { useCallback, useEffect, useRef } from 'react'
+import styles from './index.module.scss'
+import { GoodsType } from '../SonGoodsList/data'
+import { Cascader, DatePicker, Form, FormInstance, Input, InputNumber, Select } from 'antd'
+import { useDispatch, useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import { E1_APIgetTree } from '@/store/action/Eculture/E1tag'
+import dayjs from 'dayjs'
+import { getTokenInfo } from '@/utils/storage'
+import { getDictFu, selectObj } from '@/utils/dataChange'
+import TextArea from 'antd/es/input/TextArea'
+import Z3upFilesRef from '@/components/Z3upFilesRef'
+
+type Props = {
+  info: GoodsType
+  closeFu: () => void
+  isEdit: boolean //藏品编辑模块
+  editSnap?: GoodsType //藏品模块属于新增还是编辑
+  succFu: (val: GoodsType, newFlag?: boolean) => void
+  moduleId: number
+}
+
+function AddGoods({ closeFu, succFu, isEdit, editSnap, moduleId, info }: Props) {
+  // 获取标签
+  const dispatch = useDispatch()
+
+  const E1tree = useSelector((state: RootState) => state.E1tag.treeData)
+
+  useEffect(() => {
+    dispatch(E1_APIgetTree())
+  }, [dispatch])
+
+  // 设置表单ref
+  const FormBoxRef = useRef<FormInstance>(null)
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {}, [])
+
+  // 通过校验点击确定
+  const onFinish = useCallback(async (values: any) => {}, [])
+
+  return (
+    <div className={styles.AddGoods}>
+      <div className='AGbox'>
+        <div className='AG1'>{isEdit ? '藏品修改' : editSnap ? '藏品编辑' : '藏品新增'}</div>
+
+        <Form
+          scrollToFirstError={true}
+          ref={FormBoxRef}
+          name='basic'
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete='off'
+          initialValues={{
+            makeDate: dayjs(),
+            makeUser: getTokenInfo().user.realName,
+            inGoodYear: dayjs()
+          }}
+        >
+          <div className='AGmain'>
+            <Form.Item
+              label='藏品登记号'
+              name='num'
+              rules={[{ required: true, message: '请输入' }]}
+              getValueFromEvent={e => e.target.value.replace(/\s+/g, '')}
+            >
+              <Input placeholder='请输入' showCount maxLength={20} />
+            </Form.Item>
+
+            <Form.Item label='藏品标签' name='tagDictId'>
+              <Cascader
+                changeOnSelect
+                options={E1tree}
+                placeholder='请选择'
+                fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+                allowClear={true}
+                showSearch={true}
+              />
+            </Form.Item>
+
+            <Form.Item
+              label='制档日期'
+              name='makeDate'
+              rules={[{ required: true, message: '请选择' }]}
+            >
+              <DatePicker
+                allowClear={false}
+                disabledDate={current => current && current > dayjs().endOf('day')}
+              />
+            </Form.Item>
+
+            <Form.Item
+              label='制档人'
+              name='makeUser'
+              rules={[{ required: true, message: '请输入' }]}
+              getValueFromEvent={e => e.target.value.trim()}
+            >
+              <Input placeholder='请输入' showCount maxLength={20} />
+            </Form.Item>
+
+            {/* 横线 */}
+            <div className='AGxian'></div>
+
+            <Form.Item
+              label='藏品名称'
+              name='name'
+              rules={[{ required: true, message: '请输入' }]}
+              getValueFromEvent={e => e.target.value.replace(/\s+/g, '')}
+            >
+              <Input placeholder='请输入' showCount maxLength={20} />
+            </Form.Item>
+
+            <Form.Item label='藏品原名' name='oldName'>
+              <Input placeholder='请输入' showCount maxLength={20} />
+            </Form.Item>
+
+            <Form.Item label='藏品级别' name='level'>
+              <Select allowClear={true} placeholder='请选择' options={selectObj['藏品级别']} />
+            </Form.Item>
+
+            <Form.Item label='藏品类别' name='typeDictId'>
+              <Cascader
+                changeOnSelect
+                options={getDictFu('藏品类别')}
+                placeholder='请选择'
+                fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+                allowClear={true}
+                showSearch={true}
+              />
+            </Form.Item>
+
+            <Form.Item label='藏品年代' name='ageDictId'>
+              <Cascader
+                changeOnSelect
+                options={getDictFu('藏品年代')}
+                placeholder='请选择'
+                fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+                allowClear={true}
+                showSearch={true}
+              />
+            </Form.Item>
+
+            <Form.Item label='具体年代' name='ageInfo'>
+              <Input placeholder='请输入' showCount maxLength={50} />
+            </Form.Item>
+
+            <div className='AGrow'>
+              <Form.Item
+                className='AGrow1'
+                label='实际数量'
+                name='pcs'
+                rules={[{ required: true, message: '请输入' }]}
+              >
+                <InputNumber min={1} max={99999999} precision={0} placeholder='请输入正整数' />
+              </Form.Item>
+              <Form.Item
+                className='AGrow2'
+                name='pcsUnitDictId'
+                rules={[{ required: true, message: '请选择' }]}
+              >
+                <Cascader
+                  changeOnSelect
+                  options={getDictFu('数量单位')}
+                  placeholder='请选择'
+                  fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+                  allowClear={true}
+                  showSearch={true}
+                />
+              </Form.Item>
+            </div>
+
+            <Form.Item label='质地' name='textureDictId'>
+              <Cascader
+                changeOnSelect
+                options={getDictFu('质地')}
+                placeholder='请选择'
+                fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+                allowClear={true}
+                showSearch={true}
+              />
+            </Form.Item>
+
+            <Form.Item label='完残程度' name='tornLevel'>
+              <Select allowClear={true} placeholder='请选择' options={selectObj['完残程度']} />
+            </Form.Item>
+
+            <Form.Item label='完残状况' name='tornInfo'>
+              <Input placeholder='请输入' showCount maxLength={50} />
+            </Form.Item>
+
+            <Form.Item label='保存状态' name='keep'>
+              <Select allowClear={true} placeholder='请选择' options={selectObj['保存状态']} />
+            </Form.Item>
+
+            <Form.Item label='保护修复情况' name='keepInfo'>
+              <TextArea placeholder='请输入' showCount maxLength={500} />
+            </Form.Item>
+
+            <div className='AGrowSize'>
+              <div className='AGrowSizeTxt'>
+                通长、通宽、通高 - 请填入非负数字,最多三位小数,单位cm
+              </div>
+
+              <Form.Item label='尺寸' name='sizeL'>
+                <InputNumber min={0} max={99999999} precision={3} placeholder='通长' />
+              </Form.Item>
+
+              <Form.Item name='sizeW'>
+                <InputNumber min={0} max={99999999} precision={3} placeholder='通宽' />
+              </Form.Item>
+              <Form.Item name='sizeH'>
+                <InputNumber min={0} max={99999999} precision={3} placeholder='通高' />
+              </Form.Item>
+            </div>
+
+            <Form.Item label='具体尺寸' name='sizeInfo'>
+              <Input placeholder='请输入' showCount maxLength={50} />
+            </Form.Item>
+
+            <Form.Item label='质量范围' name='qualityScope'>
+              <Select allowClear={true} placeholder='请选择' options={selectObj['质量范围']} />
+            </Form.Item>
+
+            <div className='AGrow'>
+              <Form.Item className='AGrow1' label='具体质量' name='quality'>
+                <InputNumber min={0} max={99999999} precision={3} placeholder='请输入非负数字' />
+              </Form.Item>
+              <Form.Item className='AGrow2' name='qualityUnitDictId'>
+                <Select allowClear={true} placeholder='请选择' options={selectObj['质量单位']} />
+              </Form.Item>
+            </div>
+
+            <Form.Item label='入藏年度' name='inGoodYear'>
+              <DatePicker
+                picker='year'
+                disabledDate={current => current && current > dayjs().endOf('year')}
+              />
+            </Form.Item>
+
+            <Form.Item label='入藏时间范围' name='inGoodYearScope'>
+              <Select allowClear={true} placeholder='请选择' options={selectObj['入藏时间范围']} />
+            </Form.Item>
+
+            <Form.Item label='文物来源' name='source'>
+              <Select allowClear={true} placeholder='请选择' options={selectObj['文物来源']} />
+            </Form.Item>
+
+            <Form.Item label='产地' name='born'>
+              <Input placeholder='请输入' showCount maxLength={50} />
+            </Form.Item>
+
+            <Form.Item label='质量等级' name='qualityLevelDictId'>
+              <Cascader
+                changeOnSelect
+                options={getDictFu('质量等级')}
+                placeholder='请选择'
+                fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+                allowClear={true}
+                showSearch={true}
+              />
+            </Form.Item>
+
+            <Form.Item label='著者' name='pressAuthor'>
+              <Input placeholder='请输入' showCount maxLength={50} />
+            </Form.Item>
+            <Form.Item label='版本' name='pressVersion'>
+              <Input placeholder='请输入' showCount maxLength={50} />
+            </Form.Item>
+            <Form.Item label='存卷' name='pressFile'>
+              <Input placeholder='请输入' showCount maxLength={50} />
+            </Form.Item>
+            <Form.Item label='藏品简介' name='intro'>
+              <TextArea placeholder='请输入' showCount maxLength={2000} />
+            </Form.Item>
+            <Form.Item label='备注' name='remark'>
+              <TextArea placeholder='请输入' showCount maxLength={500} />
+            </Form.Item>
+
+            {/* 横线 */}
+            <div className='AGxian'></div>
+
+            {/* 附件 */}
+            <div className='AGfull'>
+              <div className='AGfullll'>附件:</div>
+              <div className='AGfullrr'>
+                <Z3upFilesRef
+                  oneIsCover={true}
+                  moduleId={moduleId}
+                  dirCode='registerGoods'
+                  myUrl='cms/order/register/son/upload'
+                />
+              </div>
+            </div>
+
+            {/* 故事模块待完善sg */}
+          </div>
+        </Form>
+      </div>
+    </div>
+  )
+}
+
+const MemoAddGoods = React.memo(AddGoods)
+
+export default MemoAddGoods

+ 28 - 2
src/pages/Zother/EditBtn/index.tsx

@@ -6,6 +6,8 @@ import { backPageFu } from '@/utils/history'
 import { useParams } from 'react-router-dom'
 import { editBtnShowFu, pageSkitFu } from '@/utils/authority'
 import { useInfo } from '../InfoContext'
+import { MessageFu } from '@/utils/message'
+import { fileIdsResFu } from '@/utils'
 
 type Props = {
   path: string
@@ -13,7 +15,7 @@ type Props = {
 }
 
 function EditBtn({ path, APIobj }: Props) {
-  const { info } = useInfo()
+  const { info, snaps, delSnapIdsRef } = useInfo()
 
   const [btnCan, setBtnCan] = useState({ status: 0, creatorId: 0 })
 
@@ -33,6 +35,16 @@ function EditBtn({ path, APIobj }: Props) {
     }
   }, [btnCan])
 
+  // 触发检查
+  const checkFu = useCallback(() => {
+    let flag = false
+    if (snaps.length === 0) {
+      flag = true
+      return MessageFu.warning('请添加藏品')
+    }
+    return flag
+  }, [snaps.length])
+
   // 点击按钮触发事件
   const sonClick = useCallback(
     (val: string, flag?: string) => {
@@ -42,9 +54,23 @@ function EditBtn({ path, APIobj }: Props) {
       else {
         // 草稿、发起、审批、撤回、删除
         // 各种操作按钮逻辑待完善sg
+
+        const obj = {
+          ...info,
+          fileIds: fileIdsResFu(info.files),
+          goodsIds: snaps.map(v => v.id).join(','),
+          delSnapIds: delSnapIdsRef.current.length ? delSnapIdsRef.current : '',
+          snaps: snaps.map(v => ({
+            goodsId: v.id,
+            id: v.id2 ? v.id2 : null,
+            orderId: info.id,
+            snap: JSON.stringify(v)
+          }))
+        }
+        console.log('xxxxxxxxx', obj)
       }
     },
-    [id, path]
+    [delSnapIdsRef, id, info, path, snaps]
   )
 
   return (

+ 5 - 0
src/pages/Zother/EditTop/index.module.scss

@@ -4,7 +4,12 @@
       padding: 15px;
       font-weight: 700;
       font-size: 18px;
+      display: flex;
+      justify-content: space-between;
       .ant-btn {
+        margin-left: 15px;
+      }
+      .EdTitBtn {
         pointer-events: none;
       }
     }

+ 34 - 6
src/pages/Zother/EditTop/index.tsx

@@ -8,6 +8,7 @@ import classNames from 'classnames'
 import TextArea from 'antd/es/input/TextArea'
 import { useInfo } from '../InfoContext'
 import Z3upFiles from '@/components/Z3upFiles'
+import SonGoodsList from '../SonGoodsList'
 
 const pageKeyTxtObj = {
   1: '新增',
@@ -26,7 +27,7 @@ type Props = {
 }
 
 function EditTop({ rowArr, pageTxt, APIobj }: Props) {
-  const { info, setInfoFu } = useInfo()
+  const { info, setInfoFu, setSnapsFu, snapsID2ref } = useInfo()
 
   const { key, id } = useParams<any>()
   // key:1 新增 2编辑 3审批 4查看
@@ -44,10 +45,29 @@ function EditTop({ rowArr, pageTxt, APIobj }: Props) {
     async (id: number) => {
       const res = await APIobj['获取详情'](id)
       if (res.code === 0) {
-        setInfoFu(res.data)
+        const data = res.data
+        setInfoFu(data)
+
+        // 设置有关藏品的信息
+        // 藏品清单快照信息id对比
+        const arrTemp: any = []
+        const snapsTemp = data.snaps || []
+        snapsTemp.forEach((v: any) => {
+          snapsID2ref.current.push({ goodsId: v.goodsId, id: v.id })
+
+          const obj = JSON.parse(v.snap || '{}')
+
+          if (obj.id) obj.id2 = v.id
+
+          arrTemp.push({
+            ...obj
+          })
+        })
+
+        setSnapsFu(arrTemp)
       }
     },
-    [APIobj, setInfoFu]
+    [APIobj, setInfoFu, setSnapsFu, snapsID2ref]
   )
 
   useEffect(() => {
@@ -100,7 +120,14 @@ function EditTop({ rowArr, pageTxt, APIobj }: Props) {
       {info.id ? (
         <>
           <div className='EdTit'>
-            申请信息&emsp;{statusTxt ? <Button type='dashed'>{statusTxt}</Button> : null}
+            <div>
+              申请信息
+              {statusTxt ? (
+                <Button className='EdTitBtn' type='dashed'>
+                  {statusTxt}
+                </Button>
+              ) : null}
+            </div>
           </div>
           <div className='Edtop1'>
             {rowArr.map((item, index) => (
@@ -166,15 +193,16 @@ function EditTop({ rowArr, pageTxt, APIobj }: Props) {
               <div className='Edtop1ll'>附件:</div>
               <div className='Edtop1rr'>
                 <Z3upFiles
-                  moduleId={info.id}
                   isLook={['3', '4'].includes(key)}
-                  fileCheck={false}
                   dirCode='register'
                   myUrl='cms/order/register/upload'
                 />
               </div>
             </div>
           </div>
+
+          {/* 藏品清单 */}
+          <SonGoodsList />
         </>
       ) : null}
     </div>

+ 23 - 2
src/pages/Zother/InfoContext.tsx

@@ -1,9 +1,16 @@
-import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react'
+import { createContext, ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react'
 import { Typetable } from './data'
+import { GoodsType } from './SonGoodsList/data'
+
+export type SnapsID2refType = { goodsId: number; id: number }
 
 type InfoContextValue = {
   info: Typetable
   setInfoFu: (info: Typetable | ((prevInfo: Typetable) => Typetable)) => void
+  snaps: GoodsType[]
+  setSnapsFu: (snaps: GoodsType[]) => void
+  delSnapIdsRef: React.MutableRefObject<number[]>
+  snapsID2ref: React.MutableRefObject<SnapsID2refType[]>
 }
 
 type Props = {
@@ -13,6 +20,7 @@ type Props = {
 const InfoContext = createContext<InfoContextValue | undefined>(undefined)
 
 export function InfoProvider({ children }: Props) {
+  // 顶部信息
   const [info, setInfo] = useState({} as Typetable)
 
   const setInfoFu = useCallback(
@@ -26,7 +34,20 @@ export function InfoProvider({ children }: Props) {
     []
   )
 
-  const value = useMemo(() => ({ info, setInfoFu }), [info, setInfoFu])
+  // 藏品信息
+  const [snaps, setSnaps] = useState<GoodsType[]>([])
+  const setSnapsFu = useCallback((snaps: GoodsType[]) => {
+    setSnaps(snaps)
+  }, [])
+
+  // 藏品清单快照数据
+  const delSnapIdsRef = useRef<number[]>([])
+  const snapsID2ref = useRef<SnapsID2refType[]>([])
+
+  const value = useMemo(
+    () => ({ info, setInfoFu, snaps, setSnapsFu, delSnapIdsRef, snapsID2ref }),
+    [info, setInfoFu, setSnapsFu, snaps]
+  )
 
   return <InfoContext.Provider value={value}>{children}</InfoContext.Provider>
 }

+ 5 - 0
src/pages/Zother/SonGoodsList/data.ts

@@ -0,0 +1,5 @@
+// 待完善sg-藏品类型
+export type GoodsType = {
+  id: number
+  id2: number
+}

+ 12 - 0
src/pages/Zother/SonGoodsList/index.module.scss

@@ -0,0 +1,12 @@
+.SonGoodsList {
+  width: 100%;
+  border-top: 1px solid #ccc;
+  :global {
+    .SonGoodsListTable {
+      padding: 15px;
+      .ant-table-cell {
+        padding: 8px !important;
+      }
+    }
+  }
+}

+ 86 - 0
src/pages/Zother/SonGoodsList/index.tsx

@@ -0,0 +1,86 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { Button } from 'antd'
+import MyTable from '@/components/MyTable'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import AddGoods from '../AddGoods'
+import { useInfo } from '../InfoContext'
+import { GoodsType } from './data'
+function SonGoodsList() {
+  const { info, snaps, setSnapsFu, delSnapIdsRef } = useInfo()
+
+  const [openInfo, setOpenInfo] = useState({} as GoodsType)
+
+  const tableLastBtn = useMemo(() => {
+    return [
+      {
+        title: '操作',
+        render: (item: GoodsType) => (
+          <>
+            <Button size='small' type='text' onClick={() => setOpenInfo(item)}>
+              编辑
+            </Button>
+            <MyPopconfirm
+              txtK='删除'
+              onConfirm={() => {
+                if (item.id2 && !delSnapIdsRef.current.includes(item.id2)) {
+                  delSnapIdsRef.current.push(item.id2)
+                  setSnapsFu(snaps.filter(v => v.id !== item.id))
+                }
+              }}
+            />
+          </>
+        )
+      }
+    ]
+  }, [delSnapIdsRef, setSnapsFu, snaps])
+
+  return (
+    <div className={styles.SonGoodsList}>
+      <div className='EdTit'>
+        <div>藏品清单</div>
+        <div>
+          {/* 待完善sg-等蓝鑫写完征集 */}
+          <Button type='primary'>从征集线索中添加</Button>
+          <Button type='primary' onClick={() => setOpenInfo({ id: -1 } as GoodsType)}>
+            新增
+          </Button>
+        </div>
+      </div>
+      <div className='SonGoodsListTable'>
+        <MyTable
+          classKey='SonGoodsList'
+          list={snaps}
+          columnsTemp={[]}
+          lastBtn={tableLastBtn}
+          pagingInfo={false}
+        />
+      </div>
+      {openInfo.id ? (
+        <AddGoods
+          info={openInfo}
+          isEdit={false}
+          closeFu={() => setOpenInfo({} as GoodsType)}
+          moduleId={info.id}
+          succFu={(obj, newFlag) => {
+            if (newFlag) {
+              // 是新增
+              setSnapsFu([obj, ...snaps])
+            } else {
+              setSnapsFu(
+                snaps.map(v => {
+                  if (v.id === obj.id) return obj
+                  else return v
+                })
+              )
+            }
+          }}
+        />
+      ) : null}
+    </div>
+  )
+}
+
+const MemoSonGoodsList = React.memo(SonGoodsList)
+
+export default MemoSonGoodsList

+ 37 - 0
src/store/action/Eculture/E1tag.ts

@@ -0,0 +1,37 @@
+import { TypeI5Tree } from '@/pages/Isystem/I5organization/data'
+import { AppDispatch } from '@/store'
+import http from '@/utils/http'
+
+/**
+ * 标签 - 获取树列表
+ */
+export const E1_APIgetTree = (callFu?: (data: TypeI5Tree[]) => void): any => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.get('cms/tag/getTree')
+    if (res.code === 0) {
+      callFu && callFu(res.data || [])
+      dispatch({ type: 'E1/getTree', payload: res.data || [] })
+    }
+  }
+}
+
+/**
+ * 标签 - 删除
+ */
+export const E1_APIdel = (id: string) => {
+  return http.get(`cms/tag/remove/${id}`)
+}
+
+/**
+ * 标签 - 获取详情
+ */
+export const E1_APIgetInfo = (id: string) => {
+  return http.get(`cms/tag/detail/${id}`)
+}
+
+/**
+ * 标签 - 新增或修改
+ */
+export const E1_APIsave = (data: any) => {
+  return http.post('cms/tag/save', { ...data, type: 'tag' })
+}

+ 2 - 2
src/store/action/Isystem/I2dict.ts

@@ -9,8 +9,8 @@ export const I2_APIgetDict = (backFu?: (data: TypeI2dict[]) => void): any => {
     let url = 'cms/dict/getTree'
     const res = await http.get(url)
     if (res.code === 0) {
-      dispatch({ type: 'I2/getDictAll', payload: res.data })
-      backFu && backFu(res.data)
+      dispatch({ type: 'I2/getDictAll', payload: res.data || [] })
+      backFu && backFu(res.data || [])
     }
   }
 }

+ 2 - 2
src/store/action/Isystem/I5organization.ts

@@ -9,8 +9,8 @@ export const I5_APIgetTree = (callFu?: (data: TypeI5Tree[]) => void): any => {
   return async (dispatch: AppDispatch) => {
     const res = await http.get('sys/dept/getTree')
     if (res.code === 0) {
-      callFu && callFu(res.data)
-      dispatch({ type: 'I5/getTree', payload: res.data })
+      callFu && callFu(res.data || [])
+      dispatch({ type: 'I5/getTree', payload: res.data || [] })
     }
   }
 }

+ 25 - 0
src/store/reducer/Eculture/E1tag.ts

@@ -0,0 +1,25 @@
+import { TypeI5Tree } from '@/pages/Isystem/I5organization/data'
+
+// 初始化状态
+const initState = {
+  // 树数据
+  treeData: [] as TypeI5Tree[]
+}
+
+// 定义 action 类型
+type Props = {
+  type: 'E1/getTree'
+  payload: TypeI5Tree[]
+}
+
+// reducer
+export default function Reducer(state = initState, action: Props) {
+  switch (action.type) {
+    // 获取树数据
+    case 'E1/getTree':
+      return { ...state, treeData: action.payload }
+
+    default:
+      return state
+  }
+}

+ 2 - 0
src/store/reducer/index.ts

@@ -4,6 +4,7 @@ import { combineReducers } from 'redux'
 // 导入 登录 模块的 reducer
 import A0Layout from './layout'
 import D1register from './Dmanage/D1register'
+import E1tag from './Eculture/E1tag'
 
 import I2dict from './Isystem/I2dict'
 import I3numSet from './Isystem/I3numSet'
@@ -17,6 +18,7 @@ import I8log from './Isystem/I8log'
 const rootReducer = combineReducers({
   A0Layout,
   D1register,
+  E1tag,
 
   I2dict,
   I3numSet,

+ 50 - 0
src/utils/dataChange.ts

@@ -58,6 +58,56 @@ export const selectObj = {
   启用状态: [
     { value: 1, label: '启用' },
     { value: 0, label: '禁用' }
+  ],
+  藏品级别: [
+    { value: '一级', label: '一级' },
+    { value: '二级', label: '二级' },
+    { value: '三级', label: '三级' },
+    { value: '一般', label: '一般' },
+    { value: '未定级', label: '未定级' }
+  ],
+  完残程度: [
+    { value: '完整', label: '完整' },
+    { value: '基本完整', label: '基本完整' },
+    { value: '残缺', label: '残缺' },
+    { value: '严重残缺(含缺失部件)', label: '严重残缺(含缺失部件)' }
+  ],
+  保存状态: [
+    { value: '状态稳定,不需修复', label: '状态稳定,不需修复' },
+    { value: '部分损腐,需要修复', label: '部分损腐,需要修复' },
+    { value: '腐蚀损毁严重,急需修复', label: '腐蚀损毁严重,急需修复' },
+    { value: '严重残缺(已修复)', label: '严重残缺(已修复)' }
+  ],
+  质量范围: [
+    { value: '<0.01 kg', label: '<0.01 kg' },
+    { value: '0.01-1 kg', label: '0.01-1 kg' },
+    { value: '1-50 kg', label: '1-50 kg' },
+    { value: '50-100 kg', label: '50-100 kg' },
+    { value: '100-1000 kg', label: '100-1000 kg' },
+    { value: '>1000 kg', label: '>1000 kg' }
+  ],
+  质量单位: [
+    { value: 'g', label: 'g' },
+    { value: 'kg', label: 'kg' }
+  ],
+  入藏时间范围: [
+    { value: '1949.10.1前', label: '1949.10.1前' },
+    { value: '1949.10.1-1965', label: '1949.10.1-1965' },
+    { value: '1966-1976', label: '1966-1976' },
+    { value: '1977-2000', label: '1977-2000' },
+    { value: '2001至今', label: '2001至今' }
+  ],
+  文物来源: [
+    { value: '征集购买', label: '征集购买' },
+    { value: '接受捐赠', label: '接受捐赠' },
+    { value: '依法交换', label: '依法交换' },
+    { value: '拨交', label: '拨交' },
+    { value: '移交', label: '移交' },
+    { value: '旧藏', label: '旧藏' },
+    { value: '发掘', label: '发掘' },
+    { value: '采集', label: '采集' },
+    { value: '拣选', label: '拣选' },
+    { value: '其他', label: '其他' }
   ]
 }
 

+ 2 - 2
src/utils/history.ts

@@ -9,8 +9,8 @@ export const loginOutFu = () => {
 
   if (!urlAll.includes('/login')) {
     if (urlArr[1]) {
-      history.push(`/login?back=${urlArr[1]}`)
-    } else history.push('/login')
+      history.replace(`/login?back=${urlArr[1]}`)
+    } else history.replace('/login')
   }
 }
 

+ 11 - 0
src/utils/index.ts

@@ -1,3 +1,5 @@
+import { FileListType } from '@/components/Z3upFiles/data'
+
 // 上传文件自动归类
 type FileObjType = {
   [key: string]: string[]
@@ -25,3 +27,12 @@ export const fileTypeRes = (fileName: string) => {
   }
   return type
 }
+
+// 处理附件id顺序-把图片放在前面
+export const fileIdsResFu = (listTemp: FileListType[]) => {
+  const files = listTemp || []
+  const fileImgArr = files.filter(v => v.type === 'img')
+  const fileOtherArr = files.filter(v => v.type !== 'img')
+  const fileIds = [...fileImgArr, ...fileOtherArr].join(',')
+  return fileIds
+}