Przeglądaj źródła

Merge branch 'master' of http://192.168.0.115:3000/shaogen1995/qingDao_goods

lanxin 23 godzin temu
rodzic
commit
505f7c6cda
29 zmienionych plików z 2010 dodań i 60 usunięć
  1. BIN
      .nvmrc
  2. 1 0
      package.json
  3. 121 0
      src/pages/Abench/A1statistics/index.module.scss
  4. 401 1
      src/pages/Abench/A1statistics/index.tsx
  5. 25 31
      src/pages/Abench/A3flow/index.tsx
  6. 21 0
      src/pages/Dmanage/D4resource/D4edit/ResourceDownLog/index.module.scss
  7. 55 0
      src/pages/Dmanage/D4resource/D4edit/ResourceDownLog/index.tsx
  8. 7 1
      src/pages/Dmanage/D4resource/D4edit/index.module.scss
  9. 68 10
      src/pages/Dmanage/D4resource/D4edit/index.tsx
  10. 17 2
      src/pages/Fstorehouse/F1inStorage/F1edit/index.tsx
  11. 36 4
      src/pages/Fstorehouse/F3outStorage/F3edit/index.tsx
  12. 1 1
      src/pages/Fstorehouse/F4check/F4edit/index.tsx
  13. 4 4
      src/pages/Fstorehouse/F5staff/F5edit/components/StaffModal.tsx
  14. 97 0
      src/pages/Hexhibits/H1loan/H1detail/index.module.scss
  15. 438 0
      src/pages/Hexhibits/H1loan/H1detail/index.tsx
  16. 243 0
      src/pages/Hexhibits/H1loan/components/ExhibitionModal.tsx
  17. 79 0
      src/pages/Hexhibits/H1loan/components/RegionSettingModal.tsx
  18. 110 1
      src/pages/Hexhibits/H1loan/index.tsx
  19. 6 0
      src/pages/Layout/data.ts
  20. 5 1
      src/pages/Zother/EditBtn/index.tsx
  21. 125 2
      src/pages/Zother/EditInput/index.tsx
  22. 14 0
      src/store/action/Dmanage/D4resource.ts
  23. 11 0
      src/store/action/Fstorehouse/F1inStorage.ts
  24. 47 0
      src/store/action/Hexhibits/H1loan.ts
  25. 28 0
      src/store/reducer/Hexhibits/H1loan.ts
  26. 3 1
      src/store/reducer/index.ts
  27. 5 0
      src/utils/dataChange.tsx
  28. 29 1
      src/utils/tableData.ts
  29. 13 0
      yarn.lock

BIN
.nvmrc


+ 1 - 0
package.json

@@ -21,6 +21,7 @@
     "braft-utils": "^3.0.12",
     "dayjs": "^1.11.10",
     "echarts": "^6.0.0",
+    "echarts-for-react": "^3.0.6",
     "file-saver": "^2.0.5",
     "js-base64": "^3.7.3",
     "js-export-excel": "^1.1.4",

+ 121 - 0
src/pages/Abench/A1statistics/index.module.scss

@@ -1,4 +1,125 @@
 .A1statistics {
+  padding: 24px;
   :global {
+    .A1_1 {
+      display: flex;
+      margin-bottom: 15px;
+      & > div {
+        width: 18%;
+        height: 150px;
+        border-radius: 6px;
+        border: 1px solid #ccc;
+        background-color: #fff;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        margin-right: 14px;
+        & > div {
+          padding-left: 28px;
+          display: flex;
+          align-items: center;
+          margin-bottom: 20px;
+          img {
+            margin-right: 8px;
+            width: 36px;
+            height: 36px;
+          }
+          h3 {
+            font-size: 16px;
+          }
+        }
+        & > p {
+          padding-left: 55px;
+          & > span {
+            font-size: 34px;
+            font-weight: 700;
+            color: var(--myAntdColor);
+          }
+          & > i {
+            display: inline-block;
+            margin-left: 15px;
+            & > i {
+              margin-left: 5px;
+              font-weight: 700;
+              font-size: 24px;
+              color: var(--myAntdColor);
+            }
+          }
+        }
+      }
+      .A1_1_3 {
+        width: 520px;
+      }
+      .A1_1_4 {
+        margin-right: 0;
+        width: 29%;
+        & > div {
+          position: relative;
+          .A1_1_4_1 {
+            position: absolute;
+            right: 15px;
+            top: 50%;
+            transform: translateY(-50%);
+          }
+        }
+      }
+    }
+
+    .A1_box {
+      width: 100%;
+      height: calc(100% - 160px);
+      display: flex;
+      & > div {
+        padding: 15px;
+        .A1tit {
+          font-weight: 700;
+          font-size: 20px;
+        }
+      }
+      .A1_2 {
+        width: 65%;
+        background-color: #fff;
+        border: 1px solid #ccc;
+        border-radius: 6px;
+        display: flex;
+        justify-content: space-between;
+        .A1_2row {
+          width: 49%;
+          .A1_2ech {
+            width: 100%;
+            height: calc(100% - 20px);
+          }
+        }
+      }
+      .A1_3 {
+        padding: 0;
+        margin-left: 15px;
+        width: calc(35% - 15px);
+        .A1_3row {
+          padding: 15px;
+          background-color: #fff;
+          border: 1px solid #ccc;
+          border-radius: 6px;
+          height: calc(50% - 9px);
+          margin-bottom: 18px;
+          &:nth-of-type(2) {
+            margin-bottom: 0px;
+          }
+          .A1tit2 {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            & > div {
+              font-weight: 700;
+              font-size: 20px;
+            }
+          }
+          .A1_3ech {
+            width: 100%;
+            height: calc(100% - 30px);
+          }
+        }
+      }
+    }
   }
 }

+ 401 - 1
src/pages/Abench/A1statistics/index.tsx

@@ -1,9 +1,409 @@
-import React from 'react'
+import React, { useCallback, useEffect } from 'react'
 import styles from './index.module.scss'
+import { Select } from 'antd'
+import { iconUrl } from '@/utils/http'
+import * as echarts from 'echarts'
+
+const chartData1 = [
+  { name: '啤酒产品类', value: 2500 },
+  { name: '饮用器皿类', value: 1800 },
+  { name: '品牌文化与节庆类', value: 1200 },
+  { name: '企业文化与档案类', value: 800 },
+  { name: '艺术与礼品类', value: 1000 },
+  { name: '工具与设备类', value: 900 },
+  { name: '其他综合类', value: 2500 }
+]
+
+const chartData2 = [
+  { name: '酒类实物', value: 2500 },
+  { name: '包装与标识', value: 1800 },
+  { name: '杯子', value: 1200 },
+  { name: '品牌节庆活动', value: 1300 },
+  { name: '品牌视觉传播', value: 1600 },
+  { name: '文书档案', value: 1700 },
+  { name: '影像资料', value: 2200 },
+  { name: '荣誉与表彰', value: 1800 },
+  { name: '企业藏品', value: 1800 },
+  { name: '其他', value: 1900 }
+]
+
+const DEFAULT_COLORS = ['#c11b2d', '#243220', '#24664b', '#8acfb2', '#806e4c', '#988364', '#972d00']
+
+const binData1 = [
+  {
+    name: '啤酒产品类',
+    value: 2500
+  },
+  {
+    name: '饮用器皿类',
+    value: 1800
+  },
+  {
+    name: '品牌文化与节庆类',
+    value: 1200
+  },
+  {
+    name: '企业历史与档案类',
+    value: 800
+  },
+  {
+    name: '艺术与礼品类',
+    value: 1000
+  },
+  {
+    name: '工具与设备类',
+    value: 900
+  },
+  {
+    name: '其他综合类',
+    value: 600
+  }
+]
+
+const binData2 = [
+  {
+    name: '酒起',
+    value: 400
+  },
+  {
+    name: '杯子',
+    value: 100
+  },
+  {
+    name: '护肤品',
+    value: 100
+  },
+  {
+    name: '冰箱贴',
+    value: 100
+  },
+  {
+    name: '衣服',
+    value: 100
+  }
+]
+
+// 先写一些静态的
 function A1statistics() {
+  // 生成柱状图
+  const initEchFu = useCallback((data: any[], dom: any) => {
+    if (!dom) return
+
+    // 计算总数用于百分比
+    const total = data.reduce((sum, item) => sum + item.value, 0)
+    const myChart = echarts.getInstanceByDom(dom) || echarts.init(dom)
+
+    const option: echarts.EChartsOption = {
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'shadow'
+        },
+        formatter: (params: any) => {
+          const param = params[0]
+          return `
+                  <div style="font-weight: bold;">${param.name}</div>
+                  <div>数量: ${param.value}</div>
+                  <div>占比: ${((param.value / total) * 100).toFixed(1) + '%'}</div>
+                `
+        },
+        backgroundColor: 'rgba(255, 255, 255, 0.95)',
+        borderColor: '#ddd',
+        textStyle: {
+          color: '#000'
+        }
+      },
+      grid: {
+        left: '5%',
+        right: '5%',
+        // 减小底部距离 - 调整这个值可以让柱子更靠近底部
+        bottom: '5%',
+        top: '5%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category',
+        data: data.map(item => item.name),
+        axisLabel: {
+          interval: 0,
+          rotate: 30,
+          fontSize: 12,
+          color: '#000',
+          margin: 15 // 调整标签与柱子的距离
+        },
+        axisLine: {
+          lineStyle: {
+            color: '#999'
+          }
+        },
+        axisTick: {
+          show: true,
+          alignWithLabel: true,
+          length: 4
+        },
+        // 调整X轴位置,使其更靠近柱子底部
+        offset: 10
+      },
+      yAxis: {
+        type: 'value',
+        name: '',
+        nameTextStyle: {
+          fontSize: 14,
+          color: '#000',
+          padding: [0, 0, 0, 10]
+        },
+        axisLabel: {
+          fontSize: 12,
+          color: '#000',
+          formatter: (value: number) => {
+            if (value >= 1000) {
+              return `${value / 1000}k`
+            }
+            return value.toString()
+          }
+        },
+        splitLine: {
+          lineStyle: {
+            type: 'dashed',
+            color: '#e0e0e0'
+          }
+        },
+        // 设置Y轴最大值
+        max: 3000,
+        min: 0,
+        // 调整Y轴刻度
+        interval: 500
+      },
+      series: [
+        {
+          name: '数量',
+          type: 'bar',
+          data: data.map((item, index) => ({
+            value: item.value,
+            percentage: ((item.value / total) * 100).toFixed(1) + '%',
+            // 为每个柱子设置独立的渐变
+            itemStyle: {
+              // 关键修改:使用线性渐变实现单个柱子的渐变效果
+              color: new echarts.graphic.LinearGradient(
+                0,
+                0,
+                0,
+                1, // 0,0表示起点,0,1表示终点(垂直方向渐变)
+                [
+                  { offset: 0, color: '#bb1e2e' }, // 顶部颜色
+                  { offset: 1, color: '#29644a' } // 底部颜色(更浅/白色)
+                ]
+              ),
+              borderRadius: [10, 10, 10, 10],
+              // 添加阴影效果增强立体感
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 4,
+              shadowOffsetY: 2
+            }
+          })),
+          barWidth: '50%', // 稍微调窄柱子
+          // 调整标签位置和样式
+          label: {
+            show: true,
+            position: 'top',
+            formatter: (params: any) => {
+              const itemData = params.data
+              return `${itemData.value}\n${((itemData.value / total) * 100).toFixed(1) + '%'}`
+            },
+            fontSize: 10,
+            fontWeight: 'bold',
+            color: '#000',
+            lineHeight: 16
+          },
+
+          emphasis: {
+            itemStyle: {
+              shadowColor: 'rgba(0, 0, 0, 0.3)',
+              shadowBlur: 10,
+              shadowOffsetY: 3
+            }
+          }
+        }
+      ]
+    }
+
+    myChart.setOption(option)
+  }, [])
+
+  // 生成饼图
+  const binInitFu = useCallback((data: any[], dom: any) => {
+    if (!dom) return
+
+    // 计算总数用于百分比
+    const total = data.reduce((sum, item) => sum + item.value, 0)
+
+    const myChart = echarts.getInstanceByDom(dom) || echarts.init(dom)
+    const option = {
+      color: DEFAULT_COLORS, // 设置颜色方案
+      tooltip: {
+        trigger: 'item',
+        formatter: '{b}: {c} ({d}%)'
+      },
+      legend: {
+        type: 'scroll',
+        orient: 'vertical',
+        left: '52%',
+        top: 'center',
+        itemGap: 15,
+        textStyle: {
+          fontSize: 12
+        },
+        formatter: function (name: string) {
+          const item = data.find(d => d.name === name)
+          if (item) {
+            const percent = ((item.value / total) * 100).toFixed(1)
+            return `${name}  ${item.value}  ${percent}%`
+          }
+          return name
+        }
+      },
+      series: [
+        {
+          name: '品类分布',
+          type: 'pie',
+          center: ['25%', '50%'], // 设置饼图中心位置
+          radius: ['50%', '90%'],
+          avoidLabelOverlap: false,
+          itemStyle: {
+            borderRadius: 0,
+            borderColor: '#fff',
+            borderWidth: 2
+          },
+          label: {
+            show: false
+          },
+          labelLine: {
+            show: false
+          },
+          emphasis: {
+            itemStyle: {
+              shadowBlur: 10,
+              shadowOffsetX: 0,
+              shadowColor: 'rgba(0, 0, 0, 0.5)'
+            }
+          },
+          data: data.map(item => ({
+            name: item.name,
+            value: item.value
+          }))
+        }
+      ]
+    }
+    myChart.setOption(option)
+  }, [])
+
+  useEffect(() => {
+    initEchFu(chartData1, document.querySelector('#echBox1'))
+    initEchFu(chartData2, document.querySelector('#echBox2'))
+
+    binInitFu(binData1, document.querySelector('#echBox3'))
+    binInitFu(binData2, document.querySelector('#echBox4'))
+  }, [binInitFu, initEchFu])
+
   return (
     <div className={styles.A1statistics}>
       <div className='pageTitle'>数据统计</div>
+
+      <div className='A1_1'>
+        <div>
+          <div>
+            <img src={iconUrl + '/a11.png'} alt='' />
+            <h3>藏品总数</h3>
+          </div>
+          <p>
+            <span>3741</span>(件/套)
+          </p>
+        </div>
+        <div>
+          <div>
+            <img src={iconUrl + '/a22.png'} alt='' />
+            <h3>藏品总数量</h3>
+          </div>
+          <p>
+            <span>5834</span>(个)
+          </p>
+        </div>
+
+        <div className='A1_1_3'>
+          <div>
+            <img src={iconUrl + '/a33.png'} alt='' />
+            <h3>定级文物数量</h3>
+          </div>
+          <p>
+            <span>120</span>(件/套)
+            <i>
+              一级<i>20</i>
+            </i>
+            <i>
+              二级<i>30</i>
+            </i>
+            <i>
+              三级<i>40</i>
+            </i>
+            <i>
+              一般<i>30</i>
+            </i>
+          </p>
+        </div>
+
+        <div className='A1_1_4'>
+          <div>
+            <img src={iconUrl + '/a44.png'} alt='' />
+            <h3>年度新增产品数量</h3>
+            <div className='A1_1_4_1'>
+              <Select
+                defaultValue={2026}
+                options={[
+                  { value: 2026, label: '2026年度' },
+                  { value: 2025, label: '2025年度' },
+                  { value: 2024, label: '2024年度' }
+                ]}
+              />
+            </div>
+          </div>
+          <p>
+            <span>123</span>(件/套)
+          </p>
+        </div>
+      </div>
+
+      <div className='A1_box'>
+        <div className='A1_2'>
+          <div className='A1_2row'>
+            <div className='A1tit'>一级分类</div>
+            <div className='A1_2ech' id='echBox1'></div>
+          </div>
+          <div className='A1_2row'>
+            <div className='A1tit'>二级分类</div>
+            <div className='A1_2ech' id='echBox2'></div>
+          </div>
+        </div>
+        <div className='A1_3'>
+          <div className='A1_3row'>
+            <div className='A1tit2'>
+              <div>材质统计</div>
+              <Select defaultValue={'杯子'} options={[{ value: '杯子', label: '杯子' }]} />
+            </div>
+            <div className='A1_3ech' id='echBox3'></div>
+          </div>
+
+          <div className='A1_3row'>
+            <div className='A1tit2'>
+              <div>品类统计</div>
+              <Select
+                defaultValue={'文创产品'}
+                options={[{ value: '文创产品', label: '文创产品' }]}
+              />
+            </div>
+            <div className='A1_3ech' id='echBox4'></div>
+          </div>
+        </div>
+      </div>
     </div>
   )
 }

+ 25 - 31
src/pages/Abench/A3flow/index.tsx

@@ -1,9 +1,9 @@
-import React, { useMemo, useState, useEffect } from 'react'
+import React, { useMemo, useState, useEffect, useCallback } from 'react'
 import styles from './index.module.scss'
 import { selectObj } from '@/utils/dataChange'
 import history from '@/utils/history'
 import { MessageFu } from '@/utils/message'
-import { authorityFu } from '@/utils/authority'
+import { authorityFu, tableListAuditBtnFu } from '@/utils/authority'
 import TableList from '@/pages/Zother/TableList'
 import { useSelector } from 'react-redux'
 import { RootState } from '@/store'
@@ -205,42 +205,36 @@ function A3flow() {
     )
   }
 
+  const btnFu = useCallback((pageKey: '3' | '4', obj: any, id: number) => {
+    if (obj) {
+      authorityFu(obj.id, obj.label, () => history.push(`${obj.path}_edit/${pageKey}/${id}`))
+    } else MessageFu.warning('申请类型错误')
+  }, [])
+
   // 故事管理/藏品总账定制右侧操作按钮
   const storyTableLastBtn = useMemo(() => {
     return [
       {
         title: '操作',
-        render: (item: any) => (
-          <>
-            <Button
-              size='small'
-              type='text'
-              onClick={() => {
-                console.log(item)
-                const obj = list.find(v => v.value === item.type)
-                if (obj) {
-                  authorityFu(obj.id, obj.label, () =>
-                    history.push(`${obj.path}_edit/4/${item.id}`)
-                  )
-                } else MessageFu.warning('申请类型错误')
-              }}
-            >
-              查看
-            </Button>
-            <Button
-              size='small'
-              type='text'
-              onClick={() => {
-                alert('审批')
-              }}
-            >
-              审批
-            </Button>
-          </>
-        )
+        render: (item: any) => {
+          const obj = list.find(v => v.value === item.type)
+          return (
+            <>
+              <Button size='small' type='text' onClick={() => btnFu('4', obj, item.id)}>
+                查看
+              </Button>
+
+              {tableListAuditBtnFu(item) ? (
+                <Button size='small' type='text' onClick={() => btnFu('3', obj, item.id)}>
+                  审批
+                </Button>
+              ) : null}
+            </>
+          )
+        }
       }
     ]
-  }, [list])
+  }, [btnFu, list])
 
   return (
     <div className={styles.A3flow}>

+ 21 - 0
src/pages/Dmanage/D4resource/D4edit/ResourceDownLog/index.module.scss

@@ -0,0 +1,21 @@
+.ResourceDownLog {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+    .ant-modal {
+      width: 800px !important;
+    }
+    .ant-modal-body {
+      padding-top: 10px !important;
+      border-top: 1px solid #ccc;
+      .ant-table-cell {
+        padding: 8px !important;
+      }
+    }
+    .ResourceDownLogBtn {
+      margin-top: 15px;
+      text-align: center;
+    }
+  }
+}

+ 55 - 0
src/pages/Dmanage/D4resource/D4edit/ResourceDownLog/index.tsx

@@ -0,0 +1,55 @@
+import React, { useCallback, useEffect, useState } from 'react'
+import styles from './index.module.scss'
+import { Button, Modal } from 'antd'
+import MyTable from '@/components/MyTable'
+import { D4API_downloadLog } from '@/store/action/Dmanage/D4resource'
+import { MessageFu } from '@/utils/message'
+
+type Props = {
+  ids: number[]
+  closeFu: () => void
+}
+
+function ResourceDownLog({ closeFu, ids }: Props) {
+  const [list, setList] = useState<any[]>([])
+
+  const getList = useCallback(async () => {
+    if (!ids.length) {
+      MessageFu.warning('暂无藏品')
+      return
+    }
+    const res = await D4API_downloadLog(ids)
+    if (res.code === 0) {
+      setList(res.data || [])
+    }
+  }, [ids])
+
+  useEffect(() => {
+    getList()
+  }, [getList])
+
+  return (
+    <Modal wrapClassName={styles.ResourceDownLog} title='下载记录' open={true} footer={[]}>
+      <MyTable
+        yHeight={500}
+        classKey='ResourceDownLog'
+        list={list}
+        columnsTemp={[
+          ['txt', '藏品登记号', 'joinNum'],
+          ['txt', '藏品名称', 'joinName'],
+          ['txt', '下载人员', 'creatorName'],
+          ['txt', '下载时间', 'createTime']
+        ]}
+        pagingInfo={false}
+      />
+
+      <div className='ResourceDownLogBtn'>
+        <Button onClick={closeFu}>关闭</Button>
+      </div>
+    </Modal>
+  )
+}
+
+const MemoResourceDownLog = React.memo(ResourceDownLog)
+
+export default MemoResourceDownLog

+ 7 - 1
src/pages/Dmanage/D4resource/D4edit/index.module.scss

@@ -6,7 +6,13 @@
       padding: 15px;
       display: flex;
       align-items: center;
-      gap: 15px;
+      justify-content: space-between;
+
+      > div {
+        display: flex;
+        align-items: center;
+        gap: 15px;
+      }
     }
     .D4editResourceTitle {
       font-weight: 700;

+ 68 - 10
src/pages/Dmanage/D4resource/D4edit/index.tsx

@@ -6,9 +6,16 @@ import { InfoProvider, useInfo } from '@/pages/Zother/InfoContext'
 import SonGoodsList from '@/pages/Zother/SonGoodsList'
 import FileArchive from '@/pages/Zother/FileArchive'
 import { getDictFu, selectObj } from '@/utils/dataChange'
-import { D4_APIgetClueList, D4API_obj } from '@/store/action/Dmanage/D4resource'
-import { Checkbox } from 'antd'
+import {
+  D4_APIgetClueList,
+  D4API_downloadFiles,
+  D4API_obj
+} from '@/store/action/Dmanage/D4resource'
+import ResourceDownLog from './ResourceDownLog'
+import { Button, Checkbox } from 'antd'
 import { useParams } from 'react-router-dom'
+import { baseURL } from '@/utils/http'
+import { MessageFu } from '@/utils/message'
 
 const rowArr = [
   {
@@ -50,12 +57,14 @@ const rowArr = [
 ]
 
 function D4editContent() {
-  const { info } = useInfo() as { info: any }
+  const { info, snaps } = useInfo() as { info: any; snaps: any[] }
   const { key } = useParams<any>()
   const [resourceType, setResourceType] = useState<string[]>(
     key === '1' ? selectObj['附件类型'].map((v: any) => v.value) : []
   )
   const canEdit = useMemo(() => ['1', '2'].includes(key), [key])
+  const isOk = useMemo(() => info.status === 4, [info.status])
+  const [showDownLog, setShowDownLog] = useState(false)
 
   const handleResourceTypeChange = useCallback((val: string[]) => {
     setResourceType(val)
@@ -72,6 +81,20 @@ function D4editContent() {
     setResourceType(info.snapType.split(','))
   }, [info.snapType])
 
+  const handleDownloadAll = useCallback(async () => {
+    const ids = snaps.map(v => v.id).filter(Boolean)
+    if (!ids.length) {
+      MessageFu.warning('暂无藏品可下载')
+      return
+    }
+    const res: any = await D4API_downloadFiles(ids)
+    if (res?.code === 0 && res?.data) {
+      const path = res.data as string
+      const fullUrl = path.startsWith('http') ? path : `${baseURL}${path}`
+      window.open(fullUrl)
+    }
+  }, [snaps])
+
   return (
     <div className={styles.D4edit} id='editBox'>
       <div className='editMain'>
@@ -84,13 +107,21 @@ function D4editContent() {
         />
 
         <div className='D4editResource'>
-          <p className='D4editResourceTitle'>资源类型</p>
-          <Checkbox.Group
-            disabled={!canEdit}
-            options={selectObj['附件类型']}
-            value={resourceType}
-            onChange={handleResourceTypeChange}
-          />
+          <div>
+            <p className='D4editResourceTitle'>资源类型</p>
+            <Checkbox.Group
+              disabled={!canEdit}
+              options={selectObj['附件类型']}
+              value={resourceType}
+              onChange={handleResourceTypeChange}
+            />
+          </div>
+          {isOk && (
+            <div>
+              <Button onClick={() => setShowDownLog(true)}>查看下载记录</Button>
+              <Button onClick={handleDownloadAll}>下载全部资源</Button>
+            </div>
+          )}
         </div>
 
         {/* 藏品清单 */}
@@ -100,11 +131,38 @@ function D4editContent() {
           addShow={false}
           isClueSelect={false}
           btnTxt='选择藏品'
+          customTableLastBtn={
+            isOk
+              ? item => (
+                  <Button
+                    size='small'
+                    type='text'
+                    onClick={async () => {
+                      const res: any = await D4API_downloadFiles([item.id])
+                      if (res?.code === 0 && res?.data) {
+                        const path = res.data as string
+                        const fullUrl = path.startsWith('http') ? path : `${baseURL}${path}`
+                        window.open(fullUrl)
+                      }
+                    }}
+                  >
+                    下载资源
+                  </Button>
+                )
+              : undefined
+          }
         />
 
         {/* 附件归档 */}
         <FileArchive />
 
+        {showDownLog && (
+          <ResourceDownLog
+            ids={snaps.map(v => v.id).filter(Boolean)}
+            closeFu={() => setShowDownLog(false)}
+          />
+        )}
+
         {/* 底部按钮 */}
         <EditBtn
           path='/resource'

+ 17 - 2
src/pages/Fstorehouse/F1inStorage/F1edit/index.tsx

@@ -8,6 +8,7 @@ import { rowArrTemp } from '@/pages/Zother/data'
 import SonGoodsList from '@/pages/Zother/SonGoodsList'
 import {
   F1_APIgetClueList,
+  F1_APIgetShowList,
   F1_APIgetShelfEmptyList,
   F1_APIgetShelfList,
   F1_APIgetStorageList,
@@ -17,7 +18,22 @@ import { Button, Select } from 'antd'
 import { selectObj } from '@/utils/dataChange'
 import FileArchive from '@/pages/Zother/FileArchive'
 
-const rowArr = rowArrTemp('入库')
+const rowArrBase = rowArrTemp('入库')
+const rowArr = [
+  rowArrBase[0],
+  {
+    name: '借展归还',
+    key: 'showId',
+    type: 'SelectSearch',
+    placeholder: '请搜索展览名称',
+    api: F1_APIgetShowList,
+    searchParam: 'searchKey',
+    labelKey: 'name',
+    valueKey: 'id',
+    optionLabel: (info: any) => info?.showName
+  },
+  ...rowArrBase.slice(1)
+]
 
 function F1editContent() {
   const { info } = useInfo() as { info: any }
@@ -128,7 +144,6 @@ function F1editContent() {
     <div className={styles.F1edit} id='editBox'>
       <div className='editMain'>
         {/* 顶部 */}
-        {/* TODO: 借展归还待完善 */}
         <EditTop
           pageTxt='藏品入库'
           rowArr={rowArr}

+ 36 - 4
src/pages/Fstorehouse/F3outStorage/F3edit/index.tsx

@@ -10,12 +10,31 @@ import { F3_APIgetClueList, F3API_obj } from '@/store/action/Fstorehouse/F3outSt
 import { Select } from 'antd'
 import { selectObj } from '@/utils/dataChange'
 import FileArchive from '@/pages/Zother/FileArchive'
-import { F1_APIgetStorageList } from '@/store/action/Fstorehouse/F1inStorage'
+import { F1_APIgetShowList, F1_APIgetStorageList } from '@/store/action/Fstorehouse/F1inStorage'
 
-const rowArr = rowArrTemp('出库')
+const rowArrBase = rowArrTemp('出库')
+const rowArr = [
+  rowArrBase[0],
+  {
+    name: '借展出库',
+    key: 'showId',
+    type: 'SelectSearch',
+    placeholder: '请搜索展览名称',
+    api: F1_APIgetShowList,
+    searchParam: 'searchKey',
+    labelKey: 'name',
+    valueKey: 'id',
+    optionLabel: (info: any) => info?.showName
+  },
+  ...rowArrBase.slice(1)
+]
 
 function F3editContent() {
-  const { info, setSnapsFu } = useInfo() as { info: any; setSnapsFu: (snaps: any[]) => void }
+  const { info, setSnapsFu, setInfoFu } = useInfo() as {
+    info: any
+    setSnapsFu: (snaps: any[]) => void
+    setInfoFu: (info: any) => void
+  }
   const { key } = useParams<any>()
   const canEdit = useMemo(() => ['1', '2'].includes(key), [key])
   const sonGoodsListRef = useRef<any>(null)
@@ -60,11 +79,24 @@ function F3editContent() {
     }
   }
 
+  useEffect(() => {
+    if (!info.num) return
+    const hash = window.location.hash
+    const queryIndex = hash.indexOf('?')
+    const search = queryIndex >= 0 ? hash.slice(queryIndex) : ''
+    const params = new URLSearchParams(search)
+    const showIdStr = params.get('showId')
+    const showName = params.get('showName')
+    if (showIdStr) {
+      const showId = Number(showIdStr)
+      setInfoFu((v: any) => ({ ...v, showId, ...(showName != null && { showName }) }))
+    }
+  }, [info.num, setInfoFu])
+
   return (
     <div className={styles.F3edit} id='editBox'>
       <div className='editMain'>
         {/* 顶部 */}
-        {/* TODO: 借展归还待完善 */}
         <EditTop
           pageTxt='藏品出库'
           rowArr={rowArr}

+ 1 - 1
src/pages/Fstorehouse/F4check/F4edit/index.tsx

@@ -20,7 +20,7 @@ import MyTable from '@/components/MyTable'
 import { GoodsType } from '@/pages/Zother/SonGoodsList/data'
 import { openLink } from '@/utils/history'
 
-const rowArr = rowArrTemp('入库')
+const rowArr = rowArrTemp('盘点')
 const registerMenu = [
   {
     key: 'accident',

+ 4 - 4
src/pages/Fstorehouse/F5staff/F5edit/components/StaffModal.tsx

@@ -58,16 +58,16 @@ function StaffModal({ open, editingStaff, onOk, onCancel }: Props) {
     >
       <Form form={form} layout='vertical'>
         <Form.Item label='姓名' name='name' rules={[{ required: true, message: '请输入姓名' }]}>
-          <Input placeholder='请输入姓名' />
+          <Input placeholder='请输入姓名' maxLength={30} showCount />
         </Form.Item>
         <Form.Item label='证件信息' name='papers'>
-          <Input placeholder='请输入证件信息' />
+          <Input placeholder='请输入证件信息' maxLength={30} showCount />
         </Form.Item>
         <Form.Item label='联系方式' name='phone'>
-          <Input placeholder='请输入联系方式' />
+          <Input placeholder='请输入联系方式' maxLength={30} showCount />
         </Form.Item>
         <Form.Item label='所在单位' name='unit'>
-          <Input placeholder='请输入所在单位' />
+          <Input placeholder='请输入所在单位' maxLength={30} showCount />
         </Form.Item>
       </Form>
     </Modal>

+ 97 - 0
src/pages/Hexhibits/H1loan/H1detail/index.module.scss

@@ -0,0 +1,97 @@
+.H1detail {
+  :global {
+    .H1detailMain {
+      width: 100%;
+      border-bottom: 1px solid #ccc;
+      display: flex;
+      flex-direction: column;
+    }
+    .H1detailMainTitle {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      width: 100%;
+      font-weight: 700;
+      font-size: 18px;
+      padding: 15px;
+
+      > div {
+        display: flex;
+        align-items: center;
+        gap: 15px;
+      }
+    }
+  }
+}
+
+.detailContent {
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+  border-top: 1px solid #ccc;
+  padding: 15px;
+}
+
+.detailGrid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 15px 24px;
+}
+
+.detailRow {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  line-height: 35px;
+
+  &[data-full='true'] {
+    grid-column: 1 / -1;
+  }
+}
+
+.detailLabel {
+  flex-shrink: 0;
+  font-weight: 700;
+  width: 100px;
+  text-align: right;
+  font-size: 16px;
+}
+
+.detailValue {
+  flex: 1;
+  word-break: break-all;
+}
+
+.fileList {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+
+  a {
+    color: #1677ff;
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+}
+
+.goodSearchRow {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 10px;
+  padding: 15px;
+  border-top: 1px solid #ccc;
+
+  .ant-input {
+    width: 200px;
+  }
+}
+
+.goodTableWrap {
+  background: var(--boxBcaColor, #fff);
+  border-radius: 8px;
+  margin: 0 15px 15px;
+}

+ 438 - 0
src/pages/Hexhibits/H1loan/H1detail/index.tsx

@@ -0,0 +1,438 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { Button, Cascader, Input, Select } from 'antd'
+import { useParams } from 'react-router-dom'
+import {
+  H1_APIgetInfo,
+  H1_APIsave,
+  H1_APIdelete,
+  H1_APIgetShowGoodPage,
+  H1_APIupdateRegion
+} from '@/store/action/Hexhibits/H1loan'
+import { API_getFileListByIds } from '@/store/action/Cledger/C4file'
+import ExhibitionModal, { ExhibitionFormValues } from '../components/ExhibitionModal'
+import RegionSettingModal from '../components/RegionSettingModal'
+import { MessageFu } from '@/utils/message'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import history, { openLink } from '@/utils/history'
+import dayjs from 'dayjs'
+import { baseURL } from '@/utils/http'
+import { FileListType } from '@/components/Z3upFiles/data'
+import MyTable from '@/components/MyTable'
+import { getDictFu, selectObj } from '@/utils/dataChange'
+import { showGoodTableC } from '@/utils/tableData'
+
+type DetailInfo = {
+  id?: number
+  name?: string
+  type?: string
+  dateOpen?: string
+  dateEnd?: string
+  region?: string
+  planner?: string
+  remark?: string
+  fileIds?: string
+}
+
+const EMPTY_VAL = '(空)'
+
+const formatVal = (v: React.ReactNode) =>
+  v === undefined || v === null || v === '' ? EMPTY_VAL : v
+
+const DetailRow = ({
+  label,
+  value,
+  fullRow
+}: {
+  label: string
+  value: React.ReactNode
+  fullRow?: boolean
+}) => (
+  <div className={styles.detailRow} data-full={fullRow}>
+    <span className={styles.detailLabel}>{label}</span>
+    <span className={styles.detailValue}>{formatVal(value)}</span>
+  </div>
+)
+
+function H1detail() {
+  const { id } = useParams<any>()
+  const [info, setInfo] = useState<DetailInfo>({})
+  const [fileList, setFileList] = useState<FileListType[]>([])
+  const [exhibitionModalOpen, setExhibitionModalOpen] = useState(false)
+  const [regionSettingOpen, setRegionSettingOpen] = useState(false)
+  const [regionEditList, setRegionEditList] = useState<
+    { id: number; goodNum: string; goodName: string; region: string; address: string }[]
+  >([])
+
+  const getInfo = useCallback(async () => {
+    if (!id) return
+    const res = await H1_APIgetInfo(Number(id))
+    if (res?.code === 0 && res.data) {
+      setInfo(res.data)
+      const fileIds = res.data.fileIds || ''
+      if (fileIds) {
+        const fileRes = await API_getFileListByIds(fileIds.split(',').filter(Boolean))
+        if (fileRes?.code === 0 && fileRes.data) {
+          setFileList(fileRes.data)
+        }
+      }
+    }
+  }, [id])
+
+  useEffect(() => {
+    getInfo()
+  }, [getInfo])
+
+  const dateStr = React.useMemo(() => {
+    const { dateOpen, dateEnd } = info
+    if (!dateOpen && !dateEnd) return '长期'
+    const open = dateOpen ? dayjs(dateOpen).format('YYYY年MM月DD日') : EMPTY_VAL
+    const end = dateEnd ? dayjs(dateEnd).format('YYYY年MM月DD日') : EMPTY_VAL
+    return `${open} - ${end}`
+  }, [info])
+
+  const regionStr = React.useMemo(() => {
+    const r = info.region
+    if (!r) return EMPTY_VAL
+    const arr = typeof r === 'string' ? r.split(',').filter(Boolean) : Array.isArray(r) ? r : []
+    return arr.length ? arr.join('、') : EMPTY_VAL
+  }, [info])
+
+  const titleTag = React.useMemo(() => {
+    const type = info.type || EMPTY_VAL
+    const planner = info.planner || EMPTY_VAL
+    return `${type} | ${dateStr} | ${planner}`
+  }, [info, dateStr])
+
+  const handleDelete = useCallback(async () => {
+    if (!info.id) return
+    const res = await H1_APIdelete(info.id)
+    if (res?.code === 0) {
+      MessageFu.success('删除成功')
+      history.push('/loan')
+    } else {
+      MessageFu.error(res?.message || '删除失败')
+    }
+  }, [info.id])
+
+  const handleEditOk = useCallback(
+    async (values: ExhibitionFormValues) => {
+      const res = await H1_APIsave({
+        ...values,
+        id: info.id,
+        region: values.region.join(',')
+      })
+      if (res?.code === 0) {
+        MessageFu.success('编辑成功')
+        setExhibitionModalOpen(false)
+        getInfo()
+      } else {
+        return Promise.reject(res?.message || '编辑失败')
+      }
+    },
+    [info.id, getInfo]
+  )
+
+  // 展品清单
+  const [goodFormData, setGoodFormData] = useState({
+    pageNum: 1,
+    pageSize: 10,
+    searchKey: '',
+    searchTagName: '',
+    level: undefined as string | undefined,
+    typeDictId: undefined as (string | number)[] | undefined,
+    region: undefined as string | undefined,
+    status: undefined as number | undefined
+  })
+  const goodFormDataRef = useRef(goodFormData)
+  useEffect(() => {
+    goodFormDataRef.current = goodFormData
+  }, [goodFormData])
+  const [goodTimeKey, setGoodTimeKey] = useState(0)
+  const [goodList, setGoodList] = useState<any[]>([])
+  const [goodTotal, setGoodTotal] = useState(0)
+
+  const getGoodList = useCallback(async () => {
+    if (!info.id) return
+    const fd = goodFormDataRef.current
+    const params: any = {
+      ...fd,
+      showId: info.id
+    }
+    if (Array.isArray(fd.typeDictId) && fd.typeDictId.length) {
+      params.typeDictId = fd.typeDictId[fd.typeDictId.length - 1]
+    }
+    const res = await H1_APIgetShowGoodPage(params)
+    if (res?.code === 0) {
+      setGoodList(res.data?.records || [])
+      setGoodTotal(res.data?.total || 0)
+    }
+  }, [info.id])
+
+  useEffect(() => {
+    getGoodList()
+  }, [getGoodList, goodTimeKey])
+
+  const goodClickSearch = useCallback(() => {
+    setGoodFormData(d => ({ ...d, pageNum: 1 }))
+    setTimeout(() => setGoodTimeKey(Date.now()), 50)
+  }, [])
+
+  const goodResetFu = useCallback(() => {
+    setGoodFormData({
+      pageNum: 1,
+      pageSize: 10,
+      searchKey: '',
+      searchTagName: '',
+      level: undefined,
+      typeDictId: undefined,
+      region: undefined,
+      status: undefined
+    })
+    setTimeout(() => setGoodTimeKey(Date.now()), 50)
+  }, [])
+
+  const goodPaginationChange = useCallback((pageNum: number, pageSize: number) => {
+    setGoodFormData(d => ({ ...d, pageNum, pageSize }))
+    setTimeout(() => setGoodTimeKey(Date.now()), 50)
+  }, [])
+
+  const openRegionSetting = useCallback(() => {
+    const list = goodList.map((item: any) => ({
+      id: item.id,
+      goodNum: item.goodNum || '',
+      goodName: item.goodName || '',
+      region: item.region || '',
+      address: item.address || ''
+    }))
+    setRegionEditList(list)
+    setRegionSettingOpen(true)
+  }, [goodList])
+
+  const updateRegionEditItem = useCallback(
+    (index: number, field: 'region' | 'address', value: string) => {
+      setRegionEditList(prev =>
+        prev.map((item, i) => (i === index ? { ...item, [field]: value } : item))
+      )
+    },
+    []
+  )
+
+  const handleRegionSubmit = useCallback(async () => {
+    const payload = regionEditList.map(item => ({
+      id: item.id,
+      region: item.region || '',
+      address: item.address || ''
+    }))
+    const res = await H1_APIupdateRegion(payload)
+    if (res?.code === 0) {
+      MessageFu.success('展位设置成功')
+      setRegionSettingOpen(false)
+      setGoodTimeKey(Date.now())
+    } else {
+      MessageFu.error(res?.message || '展位设置失败')
+    }
+  }, [regionEditList])
+
+  const goodTableLastBtn = useMemo(
+    () => [
+      {
+        title: '操作',
+        render: (item: any) => (
+          <Button
+            size='small'
+            type='text'
+            onClick={() => openLink(`/goodsLook/${item.goodId ?? item.id}`)}
+          >
+            查看
+          </Button>
+        )
+      }
+    ],
+    []
+  )
+
+  const regionOptions = useMemo(() => {
+    const r = info.region
+    if (!r) return []
+    const arr = typeof r === 'string' ? r.split(',').filter(Boolean) : []
+    return arr.map(v => ({ label: v, value: v }))
+  }, [info.region])
+
+  const typeDictOptions = useMemo(() => {
+    try {
+      return getDictFu('藏品类别') || []
+    } catch {
+      return []
+    }
+  }, [])
+
+  return (
+    <div className={styles.H1detail}>
+      <div className='H1detailMain'>
+        <div className='H1detailMainTitle'>
+          <div>
+            <span>{info.name}</span>
+            <Button type='dashed' style={{ pointerEvents: 'none' }}>
+              {titleTag}
+            </Button>
+          </div>
+          <div>
+            <Button type='primary' onClick={() => setExhibitionModalOpen(true)} disabled={!info.id}>
+              编辑
+            </Button>
+            <MyPopconfirm
+              txtK='删除'
+              Dom={
+                <Button color='danger' disabled={!info.id}>
+                  删除
+                </Button>
+              }
+              onConfirm={handleDelete}
+            />
+          </div>
+        </div>
+
+        <div className={styles.detailContent}>
+          <div className={styles.detailGrid}>
+            <DetailRow label='起止日期:' value={dateStr} />
+            <DetailRow label='展览类型:' value={info.type} />
+            <DetailRow label='展区设置:' value={regionStr} />
+            <DetailRow label='策展人:' value={info.planner} />
+            <DetailRow label='展览说明:' value={info.remark} fullRow />
+            <DetailRow
+              label='附件:'
+              value={
+                fileList.length ? (
+                  <span className={styles.fileList}>
+                    {fileList.map(f => (
+                      <a
+                        key={f.id}
+                        href={baseURL + f.filePath}
+                        download
+                        target='_blank'
+                        rel='noreferrer'
+                      >
+                        {f.fileName}
+                      </a>
+                    ))}
+                  </span>
+                ) : undefined
+              }
+              fullRow
+            />
+          </div>
+        </div>
+      </div>
+
+      <div className='H1detailMain'>
+        <div className='H1detailMainTitle'>
+          <span>展品清单</span>
+          <div>
+            <Button onClick={openRegionSetting} disabled={!info.id || !goodList.length}>
+              展位设置
+            </Button>
+            <Button
+              onClick={() =>
+                info.id &&
+                history.push(`/outStorage_edit/1/null?showId=${info.id}&showName=${info.name}`)
+              }
+              disabled={!info.id}
+            >
+              借展出库
+            </Button>
+            <Button>归还展品</Button>
+          </div>
+        </div>
+        <div className={styles.goodSearchRow}>
+          <Input
+            placeholder='搜索藏品登记号、藏品名称'
+            maxLength={30}
+            value={goodFormData.searchKey}
+            onChange={e => setGoodFormData(d => ({ ...d, searchKey: e.target.value }))}
+            allowClear
+            style={{ width: 250 }}
+          />
+          <Input
+            placeholder='搜索藏品标签'
+            value={goodFormData.searchTagName}
+            onChange={e => setGoodFormData(d => ({ ...d, searchTagName: e.target.value }))}
+            allowClear
+            style={{ width: 200 }}
+          />
+          <Select
+            placeholder='级别'
+            allowClear
+            value={goodFormData.level}
+            onChange={v => setGoodFormData(d => ({ ...d, level: v }))}
+            options={selectObj['藏品级别']}
+            style={{ width: 120 }}
+          />
+          <Cascader
+            placeholder='类别'
+            allowClear
+            value={goodFormData.typeDictId}
+            onChange={(v: any) => setGoodFormData(d => ({ ...d, typeDictId: v }))}
+            options={typeDictOptions}
+            fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+            style={{ width: 160 }}
+          />
+          <Select
+            placeholder='展区'
+            allowClear
+            value={goodFormData.region}
+            onChange={v => setGoodFormData(d => ({ ...d, region: v }))}
+            options={regionOptions}
+            style={{ width: 120 }}
+          />
+          <Select
+            placeholder='展览状态'
+            allowClear
+            value={goodFormData.status}
+            onChange={v => setGoodFormData(d => ({ ...d, status: v }))}
+            options={selectObj['展览状态']}
+            style={{ width: 120 }}
+          />
+          <Button type='primary' onClick={goodClickSearch}>
+            查询
+          </Button>
+          <Button onClick={goodResetFu}>重置</Button>
+        </div>
+        <div className={styles.goodTableWrap}>
+          <MyTable
+            classKey='H1detailGood'
+            list={goodList}
+            columnsTemp={showGoodTableC}
+            total={goodTotal}
+            pageNum={goodFormData.pageNum}
+            pageSize={goodFormData.pageSize}
+            yHeight={400}
+            onChange={goodPaginationChange}
+            lastBtn={goodTableLastBtn}
+          />
+        </div>
+      </div>
+
+      <ExhibitionModal
+        open={exhibitionModalOpen}
+        onOk={handleEditOk}
+        onCancel={() => setExhibitionModalOpen(false)}
+        initialValues={info}
+        initialFileList={fileList}
+      />
+
+      <RegionSettingModal
+        open={regionSettingOpen}
+        onCancel={() => setRegionSettingOpen(false)}
+        dataSource={regionEditList}
+        regionOptions={regionOptions}
+        onUpdateItem={updateRegionEditItem}
+        onSubmit={handleRegionSubmit}
+      />
+    </div>
+  )
+}
+
+const MemoH1detail = React.memo(H1detail)
+
+export default MemoH1detail

+ 243 - 0
src/pages/Hexhibits/H1loan/components/ExhibitionModal.tsx

@@ -0,0 +1,243 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import { Button, Col, DatePicker, Form, Input, Modal, Row, Select } from 'antd'
+import TextArea from 'antd/es/input/TextArea'
+import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
+import dayjs from 'dayjs'
+import Z3upFilesRef from '@/components/Z3upFilesRef'
+import { FileListType } from '@/components/Z3upFiles/data'
+
+const { RangePicker } = DatePicker
+
+const EXHIBITION_TYPE_OPTIONS = [
+  { label: '常设展', value: '常设展' },
+  { label: '临时展', value: '临时展' },
+  { label: '巡回展', value: '巡回展' },
+  { label: '专题展', value: '专题展' },
+  { label: '交流展', value: '交流展' },
+  { label: '学术展', value: '学术展' },
+  { label: '其他', value: '其他' }
+]
+
+export type ExhibitionFormValues = {
+  name: string
+  type: string
+  dateOpen: string
+  dateEnd: string
+  planner: string
+  remark: string
+  fileIds: string
+  region: string[]
+}
+
+export type ExhibitionInitialValues = {
+  name?: string
+  type?: string
+  dateOpen?: string
+  dateEnd?: string
+  planner?: string
+  remark?: string
+  region?: string | string[]
+}
+
+type Props = {
+  open: boolean
+  onOk: (values: ExhibitionFormValues) => void | Promise<void>
+  onCancel: () => void
+  initialValues?: ExhibitionInitialValues
+  initialFileList?: FileListType[]
+}
+
+function ExhibitionModal({ open, onOk, onCancel, initialValues, initialFileList }: Props) {
+  const [form] = Form.useForm<ExhibitionFormValues>()
+  const [confirmLoading, setConfirmLoading] = useState(false)
+  const fileUploadRef = useRef<{
+    sonResListFu: () => { list: FileListType[] }
+    sonSetListFu: (list: FileListType[]) => void
+  }>(null)
+
+  const handleOk = useCallback(async () => {
+    try {
+      const values = await form.validateFields()
+      const fileIds = fileUploadRef.current
+        ?.sonResListFu()
+        ?.list?.map(f => f.id)
+        .join(',')
+      const dateRange = (values as any).dateRange
+      const submitData: ExhibitionFormValues = {
+        ...values,
+        dateOpen: dateRange?.[0] ? dayjs(dateRange[0]).format('YYYY-MM-DD') : '',
+        dateEnd: dateRange?.[1] ? dayjs(dateRange[1]).format('YYYY-MM-DD') : '',
+        fileIds: fileIds || '',
+        region: (values.region || []).map((r: string) => (r || '').trim()).filter(Boolean)
+      }
+      delete (submitData as any).dateRange
+      setConfirmLoading(true)
+      await onOk(submitData)
+      form.resetFields()
+      onCancel()
+    } catch (e) {
+      // 校验失败
+    } finally {
+      setConfirmLoading(false)
+    }
+  }, [form, onOk, onCancel])
+
+  const handleCancel = useCallback(() => {
+    form.resetFields()
+    onCancel()
+  }, [form, onCancel])
+
+  useEffect(() => {
+    if (open) {
+      if (initialValues) {
+        const regionArr =
+          typeof initialValues.region === 'string'
+            ? initialValues.region.split(',').filter(Boolean)
+            : Array.isArray(initialValues.region)
+              ? initialValues.region
+              : ['']
+        form.setFieldsValue({
+          name: initialValues.name ?? '',
+          type: initialValues.type ?? undefined,
+          planner: initialValues.planner ?? '',
+          remark: initialValues.remark ?? '',
+          region: regionArr.length ? regionArr : [''],
+          dateRange:
+            initialValues.dateOpen || initialValues.dateEnd
+              ? [
+                  initialValues.dateOpen ? dayjs(initialValues.dateOpen) : null,
+                  initialValues.dateEnd ? dayjs(initialValues.dateEnd) : null
+                ]
+              : undefined
+        } as ExhibitionFormValues & { dateRange?: any })
+        fileUploadRef.current?.sonSetListFu(initialFileList || [])
+      } else {
+        form.resetFields()
+        fileUploadRef.current?.sonSetListFu([])
+      }
+    }
+  }, [open, initialValues, initialFileList, form])
+
+  return (
+    <Modal
+      destroyOnClose
+      title='展览信息'
+      open={open}
+      onOk={handleOk}
+      onCancel={handleCancel}
+      okText='提交'
+      cancelText='取消'
+      width={800}
+      maskClosable={false}
+      confirmLoading={confirmLoading}
+    >
+      <Form form={form} layout='vertical' initialValues={{ region: [''] }}>
+        <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item
+              label='展览名称'
+              name='name'
+              rules={[
+                { required: true, message: '请输入展览名称' },
+                { max: 50, message: '不超过50个字' }
+              ]}
+            >
+              <Input placeholder='请输入展览名称' maxLength={50} showCount />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label='展览类型'
+              name='type'
+              rules={[{ required: true, message: '请选择展览类型' }]}
+            >
+              <Select placeholder='请选择' options={EXHIBITION_TYPE_OPTIONS} allowClear />
+            </Form.Item>
+          </Col>
+        </Row>
+
+        <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item label='起止日期' name='dateRange'>
+              <RangePicker style={{ width: '100%' }} placeholder={['开始日期', '结束日期']} />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label='策展人' name='planner' rules={[{ max: 20, message: '不超过20个字' }]}>
+              <Input placeholder='请输入策展人' maxLength={20} showCount />
+            </Form.Item>
+          </Col>
+        </Row>
+
+        <Form.Item label='备注' name='remark' rules={[{ max: 500, message: '不超过500个字' }]}>
+          <TextArea placeholder='请输入备注' maxLength={500} showCount rows={4} />
+        </Form.Item>
+
+        <Form.Item label='附件'>
+          <Z3upFilesRef
+            ref={fileUploadRef}
+            myUrl='cms/show/upload'
+            dirCode='exhibits'
+            moduleId={0}
+          />
+        </Form.Item>
+
+        <Form.Item
+          label='展区设置'
+          name='region'
+          rules={[
+            {
+              validator: (_, value) => {
+                const list = value || []
+                const names = list.map((v: string) => (v || '').trim()).filter(Boolean)
+                if (names.length > 20) {
+                  return Promise.reject(new Error('最多20个展区'))
+                }
+                const unique = new Set(names)
+                if (unique.size !== names.length) {
+                  return Promise.reject(new Error('同一展览内展区名称不能重复'))
+                }
+                return Promise.resolve()
+              }
+            }
+          ]}
+        >
+          <Form.List name='region'>
+            {(fields, { add, remove }) => (
+              <>
+                {fields.map(({ key, name, ...restField }) => (
+                  <div key={key} style={{ display: 'flex', marginBottom: 8, gap: 8 }}>
+                    <Form.Item {...restField} name={name} style={{ flex: 1, marginBottom: 0 }}>
+                      <Input placeholder='请填写展厅或展区名称' />
+                    </Form.Item>
+                    <Form.Item style={{ marginBottom: 0 }}>
+                      {fields.length > 1 ? (
+                        <MinusCircleOutlined
+                          onClick={() => remove(name)}
+                          style={{ fontSize: 18, color: '#ff4d4f' }}
+                        />
+                      ) : null}
+                    </Form.Item>
+                  </div>
+                ))}
+                <Form.Item style={{ marginBottom: 0 }}>
+                  <Button
+                    type='dashed'
+                    onClick={() => add()}
+                    block
+                    icon={<PlusOutlined rev={undefined} />}
+                    disabled={fields.length >= 20}
+                  >
+                    添加展区
+                  </Button>
+                </Form.Item>
+              </>
+            )}
+          </Form.List>
+        </Form.Item>
+      </Form>
+    </Modal>
+  )
+}
+
+export default ExhibitionModal

+ 79 - 0
src/pages/Hexhibits/H1loan/components/RegionSettingModal.tsx

@@ -0,0 +1,79 @@
+import React from 'react'
+import { Button, Input, Modal, Select, Table } from 'antd'
+
+export type RegionEditItem = {
+  id: number
+  goodNum: string
+  goodName: string
+  region: string
+  address: string
+}
+
+type Props = {
+  open: boolean
+  onCancel: () => void
+  dataSource: RegionEditItem[]
+  regionOptions: { label: string; value: string }[]
+  onUpdateItem: (index: number, field: 'region' | 'address', value: string) => void
+  onSubmit: () => void
+}
+
+function RegionSettingModal({
+  open,
+  onCancel,
+  dataSource,
+  regionOptions,
+  onUpdateItem,
+  onSubmit
+}: Props) {
+  return (
+    <Modal title='展位设置' open={open} onCancel={onCancel} footer={null} width={800}>
+      <Table
+        dataSource={dataSource}
+        rowKey='id'
+        pagination={false}
+        scroll={{ y: 600 }}
+        columns={[
+          { title: '藏品登记号', dataIndex: 'goodNum', width: 120 },
+          { title: '藏品名称', dataIndex: 'goodName', width: 150 },
+          {
+            title: '展区',
+            dataIndex: 'region',
+            width: 150,
+            render: (_: any, record: RegionEditItem, index: number) => (
+              <Select
+                placeholder='请选择展区'
+                allowClear
+                value={record.region || undefined}
+                onChange={v => onUpdateItem(index, 'region', v || '')}
+                options={regionOptions}
+                style={{ width: '100%' }}
+              />
+            )
+          },
+          {
+            title: '具体位置',
+            dataIndex: 'address',
+            render: (_: any, record: RegionEditItem, index: number) => (
+              <Input
+                placeholder='请填写具体位置'
+                maxLength={100}
+                showCount
+                value={record.address}
+                onChange={e => onUpdateItem(index, 'address', e.target.value)}
+              />
+            )
+          }
+        ]}
+      />
+      <div style={{ marginTop: 16, textAlign: 'right' }}>
+        <Button onClick={onCancel}>取消</Button>
+        <Button type='primary' onClick={onSubmit} style={{ marginLeft: 8 }}>
+          提交
+        </Button>
+      </div>
+    </Modal>
+  )
+}
+
+export default React.memo(RegionSettingModal)

+ 110 - 1
src/pages/Hexhibits/H1loan/index.tsx

@@ -1,9 +1,118 @@
-import React from 'react'
+import React, { useCallback, useMemo, useRef, useState } from 'react'
 import styles from './index.module.scss'
+import { baseFormData } from '@/pages/Zother/data'
+import TableList from '@/pages/Zother/TableList'
+import { RootState } from '@/store'
+import { H1_APIgetList, H1_APIsave } from '@/store/action/Hexhibits/H1loan'
+import { tableListAuditBtnFu } from '@/utils/authority'
+import { loanTableC } from '@/utils/tableData'
+import { Button } from 'antd'
+import { useSelector } from 'react-redux'
+import ExhibitionModal, { ExhibitionFormValues } from './components/ExhibitionModal'
+import { MessageFu } from '@/utils/message'
+import { openLink } from '@/utils/history'
+
+const H1baseFormData = baseFormData()
+
 function H1loan() {
+  const tableListRef = useRef<any>(null)
+  const [exhibitionModalOpen, setExhibitionModalOpen] = useState(false)
+  // 从仓库拿数据
+  const tableInfo = useSelector((state: RootState) => state.H1loan.tableInfo)
+  const SEARCH_DOM = useMemo(
+    () => [
+      {
+        type: 'time',
+        key: ['dateOpen', 'dateEnd'],
+        placeholder: `入藏日期`
+      },
+      {
+        type: 'input',
+        key: 'searchKey',
+        placeholder: `请输入申请编号、发起人或藏品编号`
+      },
+      {
+        type: 'time',
+        key: ['startTime', 'endTime'],
+        placeholder: `发起日期`
+      }
+    ],
+    []
+  )
+
+  const dataExport = () => {
+    console.log('数据导出了')
+  }
+
+  const tableBtnFu = useCallback((id: number | null, key: string) => {
+    openLink(`/loan_detail/${id}`)
+  }, [])
+
+  const handleExhibitionOk = useCallback(async (values: ExhibitionFormValues) => {
+    const res = await H1_APIsave({
+      ...values,
+      region: values.region.join(',')
+    })
+    if (res?.code === 0) {
+      MessageFu.success('新增成功')
+      tableListRef.current?.getListFu?.()
+    } else {
+      return Promise.reject(res?.message || '新增失败')
+    }
+  }, [])
+
   return (
     <div className={styles.H1loan}>
       <div className='pageTitle'>借展管理</div>
+
+      <TableList
+        ref={tableListRef}
+        baseFormData={H1baseFormData}
+        getListAPI={H1_APIgetList}
+        pageKey='position'
+        tableInfo={tableInfo}
+        columnsTemp={loanTableC}
+        rightBtnWidth={340}
+        yHeight={585}
+        searchDom={SEARCH_DOM}
+        storyTableListToprr={({ clickSearch, resetSelectFu }) => (
+          <>
+            <Button type='primary' onClick={dataExport}>
+              数据导出
+            </Button>
+            <Button type='primary' ghost onClick={() => setExhibitionModalOpen(true)}>
+              新增
+            </Button>
+            <Button type='primary' onClick={clickSearch}>
+              查询
+            </Button>
+            <Button onClick={resetSelectFu}>重置</Button>
+          </>
+        )}
+        storyTableLastBtn={[
+          {
+            title: '操作',
+            render: (item: any) => (
+              <>
+                <Button type='text' onClick={() => tableBtnFu(item.id, '4')}>
+                  查看
+                </Button>
+                {tableListAuditBtnFu(item) ? (
+                  <Button size='small' type='text' onClick={() => tableBtnFu(item.id, '3')}>
+                    审批
+                  </Button>
+                ) : null}
+              </>
+            )
+          }
+        ]}
+      />
+
+      <ExhibitionModal
+        open={exhibitionModalOpen}
+        onOk={handleExhibitionOk}
+        onCancel={() => setExhibitionModalOpen(false)}
+      />
     </div>
   )
 }

+ 6 - 0
src/pages/Layout/data.ts

@@ -434,5 +434,11 @@ export const routerSon: RouterTypeRow[] = [
     name: '人员出入库-详情页',
     path: '/staff_edit/:key/:id',
     Com: React.lazy(() => import('../Fstorehouse/F5staff/F5edit'))
+  },
+  {
+    id: 910,
+    name: '借展管理-详情页',
+    path: '/loan_detail/:id',
+    Com: React.lazy(() => import('../Hexhibits/H1loan/H1detail'))
   }
 ]

+ 5 - 1
src/pages/Zother/EditBtn/index.tsx

@@ -64,7 +64,7 @@ function EditBtn({
     }
   }, [btnCan])
 
-  // 藏品登记号为空 和重复的判断
+  //   藏品数量不能为空判断 、藏品登记号为空 和重复的判断
   const numFlag = (data: GoodsType[]) => {
     const numSet = new Set<string>()
 
@@ -74,6 +74,10 @@ function EditBtn({
         return `${item.name} - 藏品登记号不能为空`
       }
 
+      if (!item.pcs || !item.pcsUnitDictId) {
+        return `${item.name} - 藏品数量不能为空`
+      }
+
       // 检查是否重复
       if (numSet.has(item.num)) {
         const duplicateNames = data.filter(d => d.num === item.num).map(d => d.name)

+ 125 - 2
src/pages/Zother/EditInput/index.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback, useMemo } from 'react'
+import React, { useCallback, useMemo, useRef, useState } from 'react'
 import classNames from 'classnames'
 import dayjs from 'dayjs'
 import { useInfo } from '../InfoContext'
@@ -7,6 +7,121 @@ import TextArea from 'antd/es/input/TextArea'
 
 const { RangePicker } = DatePicker
 
+const PAGE_SIZE = 20
+
+/** 可搜索+分页的远程 Select,用于 SelectSearch 类型 */
+function SelectSearchRemote({
+  item,
+  isLook,
+  infoRes,
+  dataChangeFu,
+  value
+}: {
+  item: any
+  isLook: boolean
+  infoRes: any
+  dataChangeFu: (val: any, item: any) => void
+  value: any
+}) {
+  const [options, setOptions] = useState<{ label: string; value: number }[]>([])
+  const [loading, setLoading] = useState(false)
+  const [pageNum, setPageNum] = useState(1)
+  const [total, setTotal] = useState(0)
+  const [searchKey, setSearchKey] = useState('')
+  const fetchRef = useRef(0)
+
+  const api = item.api
+  const labelKey = item.labelKey || 'name'
+  const valueKey = item.valueKey || 'id'
+  const searchParam = item.searchParam || 'searchKey'
+  const hasMore = options.length < total
+
+  const fetchList = useCallback(
+    async (page: number, keyword: string, append: boolean) => {
+      if (!api) return
+      const current = ++fetchRef.current
+      setLoading(true)
+      try {
+        const res = await api({
+          pageNum: page,
+          pageSize: PAGE_SIZE,
+          [searchParam]: keyword || undefined
+        })
+        if (current !== fetchRef.current) return
+        const records = res?.data?.records || []
+        const totalRes = res?.data?.total || 0
+        setTotal(totalRes)
+        setOptions(prev => (append ? [...prev, ...records] : records))
+      } finally {
+        if (current === fetchRef.current) setLoading(false)
+      }
+    },
+    [api, searchParam]
+  )
+
+  const onSearch = useCallback(
+    (val: string) => {
+      setSearchKey(val)
+      setPageNum(1)
+      fetchList(1, val, false)
+    },
+    [fetchList]
+  )
+
+  const onPopupScroll = useCallback(
+    (e: React.UIEvent<HTMLDivElement>) => {
+      const { scrollTop, scrollHeight, clientHeight } = e.currentTarget
+      if (scrollHeight - scrollTop - clientHeight < 50 && hasMore && !loading) {
+        const next = pageNum + 1
+        setPageNum(next)
+        fetchList(next, searchKey, true)
+      }
+    },
+    [fetchList, hasMore, loading, pageNum, searchKey]
+  )
+
+  const onDropdownVisibleChange = useCallback(
+    (open: boolean) => {
+      if (open && options.length === 0 && !loading) {
+        fetchList(1, '', false)
+      }
+    },
+    [fetchList, options.length, loading]
+  )
+
+  const opts = useMemo(() => {
+    const list = options.map((r: any) => ({
+      label: r[labelKey] ?? r.name,
+      value: r[valueKey] ?? r.id
+    }))
+    // 编辑回显:有 value 但不在列表中时,用 optionLabel 补充
+    const optionLabel =
+      typeof item.optionLabel === 'function' ? item.optionLabel(infoRes) : item.optionLabel
+    if (value != null && value !== '' && !list.some((o: any) => o.value === value) && optionLabel) {
+      list.unshift({ label: optionLabel, value })
+    }
+    return list
+  }, [options, item, infoRes, value, labelKey, valueKey])
+
+  return (
+    <Select
+      disabled={isLook}
+      showSearch
+      filterOption={false}
+      placeholder={(isLook ? '(空)' : item.placeholder) || '请选择'}
+      value={value}
+      onChange={e => dataChangeFu(e, item)}
+      options={opts}
+      allowClear={!item.must}
+      loading={loading}
+      onSearch={onSearch}
+      onPopupScroll={onPopupScroll}
+      onDropdownVisibleChange={onDropdownVisibleChange}
+      notFoundContent={loading ? '加载中...' : '暂无数据'}
+    />
+  )
+}
+
 type Props = {
   item: any
   isLook: boolean
@@ -39,7 +154,7 @@ function EditInput({ item, isLook, isTow }: Props) {
         )
       } else if (type === 'Cascader') {
         setInfoFu({ ...infoRes, dictIdApply: val ? val.join(',') : '' }, isTow)
-      } else if (type === 'Select') {
+      } else if (['Select', 'SelectSearch'].includes(type)) {
         setInfoFu({ ...infoRes, [key]: val }, isTow)
       } else if (type === 'DatePicker2') {
         const keys = Array.isArray(key) ? key : [key]
@@ -127,6 +242,14 @@ function EditInput({ item, isLook, isTow }: Props) {
             options={item.options}
             allowClear={!item.must}
           />
+        ) : item.type === 'SelectSearch' ? (
+          <SelectSearchRemote
+            item={item}
+            isLook={isLook}
+            infoRes={infoRes}
+            dataChangeFu={dataChangeFu}
+            value={(infoRes as any)[item.key]}
+          />
         ) : item.type === 'DatePicker2' ? (
           (() => {
             const keys = Array.isArray(item.key) ? item.key : [item.key]

+ 14 - 0
src/store/action/Dmanage/D4resource.ts

@@ -36,3 +36,17 @@ export const D4API_obj = {
   撤回: (id: number) => APIbase('get', `cms/order/resource/revocation/${id}`),
   删除: (id: number) => APIbase('get', `cms/order/resource/remove/${id}`)
 }
+
+/**
+ * 藏品附件 -根据附件id批量下载附件
+ */
+export const D4API_downloadFiles = (ids: number[]) => {
+  return http.post('cms/order/resource/downloadBatchZip', ids)
+}
+
+/**
+ * 资源使用 - 下载记录
+ */
+export const D4API_downloadLog = (ids: number[]) => {
+  return http.post('cms/order/resource/downloadLog', ids)
+}

+ 11 - 0
src/store/action/Fstorehouse/F1inStorage.ts

@@ -50,6 +50,17 @@ export const F1_APIgetShelfEmptyList = (params: any) => {
   return http.get('cms/orderSite/in/getStorageEmpty', { params })
 }
 
+/**
+ * 展览列表-分页(借展归还用,可搜索展览名称)
+ */
+export const F1_APIgetShowList = (data: {
+  pageNum: number
+  pageSize: number
+  searchKey?: string
+}) => {
+  return http.post('cms/show/pageList', data)
+}
+
 export const F1API_obj = {
   创建订单: () => APIbase('get', 'cms/orderSite/in/create'),
   获取详情: (id: number) => APIbase('get', `cms/orderSite/in/detail/${id}`),

+ 47 - 0
src/store/action/Hexhibits/H1loan.ts

@@ -0,0 +1,47 @@
+import { AppDispatch } from '@/store'
+import http from '@/utils/http'
+
+/**
+ * 分页列表
+ */
+export const H1_APIgetList = (data: any): any => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post('cms/show/pageList', data)
+    if (res.code === 0) {
+      const obj = {
+        list: res.data.records,
+        total: res.data.total
+      }
+
+      dispatch({ type: 'H1/getList', payload: obj })
+    }
+  }
+}
+
+export const H1_APIsave = (data: any): any => {
+  return http.post('cms/show/saveEntity', data)
+}
+
+export const H1_APIgetInfo = (id: number): any => {
+  return http.get(`cms/show/detail/${id}`)
+}
+
+export const H1_APIdelete = (id: number): any => {
+  return http.get(`cms/show/remove/${id}`)
+}
+
+/**
+ * 展品清单 - 分页列表
+ */
+export const H1_APIgetShowGoodPage = (data: any): any => {
+  return http.post('cms/show/showGoodPage', data)
+}
+
+/**
+ * 展位设置 - 更新展区与具体位置
+ */
+export const H1_APIupdateRegion = (
+  data: { id: number; region: string; address: string }[]
+): any => {
+  return http.post('cms/show/updateRegion', data)
+}

+ 28 - 0
src/store/reducer/Hexhibits/H1loan.ts

@@ -0,0 +1,28 @@
+import { Typetable } from '@/pages/Zother/data'
+
+// 初始化状态
+const initState = {
+  // 列表数据
+  tableInfo: {
+    list: [] as Typetable[],
+    total: 0
+  }
+}
+
+// 定义 action 类型
+type Props = {
+  type: 'H1/getList'
+  payload: { list: Typetable[]; total: number }
+}
+
+// reducer
+export default function Reducer(state = initState, action: Props) {
+  switch (action.type) {
+    // 获取列表数据
+    case 'H1/getList':
+      return { ...state, tableInfo: action.payload }
+
+    default:
+      return state
+  }
+}

+ 3 - 1
src/store/reducer/index.ts

@@ -33,7 +33,7 @@ import I5organization from './Isystem/I5organization'
 import I6role from './Isystem/I6role'
 import I7user from './Isystem/I7user'
 import I8log from './Isystem/I8log'
-
+import H1loan from './Hexhibits/H1loan'
 // 所有列表回调需要保存当前筛选数据
 import ZformData from './ZformData'
 
@@ -73,6 +73,8 @@ const rootReducer = combineReducers({
   I7user,
   I8log,
 
+  H1loan,
+
   ZformData
 })
 

+ 5 - 0
src/utils/dataChange.tsx

@@ -168,6 +168,11 @@ export const selectObj = {
   校验结果: [
     { value: 1, label: '成功' },
     { value: 0, label: '失败' }
+  ],
+  展览状态: [
+    { value: 1, label: '展览中' },
+    { value: 2, label: '归还中' },
+    { value: 3, label: '已归还' }
   ]
 }
 

+ 29 - 1
src/utils/tableData.ts

@@ -15,6 +15,7 @@
 //   ];
 
 import { selectObj } from './dataChange'
+import dayjs from 'dayjs'
 
 // 列表页面(大部分相同和复用的)
 export const baseTableC = (val: string) => {
@@ -182,7 +183,7 @@ export const GI3tableC = [
   ['select', '附件类型', 'type', selectObj['附件类型']],
   ['txt', '附件名称', 'fileName'],
   ['dateRes', '上传日期', 'createTime'],
-  ['txtC', '上传人', 'creatorName'],
+  ['txt', '上传人', 'creatorName'],
   ['sizeNum', '文件大小(mb)', 'fileSize', 1024]
 ]
 // 藏品详情----藏品故事
@@ -287,3 +288,30 @@ export const staffTableC = [
   ['txt', '发起日期', 'date'],
   ['select', '申请状态', 'status', selectObj['藏品入库申请状态']]
 ]
+
+// 借展详情-展品清单
+export const showGoodTableC = [
+  ['txt', '藏品登记号', 'goodNum'],
+  ['img', '封面', 'goodThumb'],
+  ['txtCTag', '藏品标签', 'goodTag'],
+  ['txt', '藏品名称', 'goodName'],
+  ['txt', '展区', 'region'],
+  ['txt', '具体位置', 'address'],
+  ['select', '展览状态', 'status', selectObj['展览状态']]
+]
+
+// 展品管理
+export const loanTableC = [
+  ['txt', '展览名称', 'name'],
+  ['txt', '展览类型', 'type'],
+  [
+    'custom',
+    '起止日期',
+    (item: any) =>
+      !item.dateOpen && !item.dateEnd
+        ? '长期'
+        : `${dayjs(item.dateOpen).format('YYYY年MM月DD日')} - ${dayjs(item.dateEnd).format('YYYY年MM月DD日')}`
+  ],
+  ['txt', '相关展区', 'region'],
+  ['txt', '相关展品', 'creatorName']
+]

+ 13 - 0
yarn.lock

@@ -4543,6 +4543,14 @@ eastasianwidth@^0.2.0:
   resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
   integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
 
+echarts-for-react@^3.0.6:
+  version "3.0.6"
+  resolved "https://registry.npmmirror.com/echarts-for-react/-/echarts-for-react-3.0.6.tgz#267908f53854856833dc39088d2a97ab4bbfad86"
+  integrity sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    size-sensor "^1.0.1"
+
 echarts@^6.0.0:
   version "6.0.0"
   resolved "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz#2935aa7751c282d1abbbf7d719d397199a15b9e7"
@@ -9719,6 +9727,11 @@ sisteransi@^1.0.5:
   resolved "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
   integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
 
+size-sensor@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/size-sensor/-/size-sensor-1.0.3.tgz#4ce525cbc5a098da09852e38b6f851ebb9b5116a"
+  integrity sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==
+
 slash@^3.0.0:
   version "3.0.0"
   resolved "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"