jinx 2 semanas atrás
pai
commit
66c3a38c5a

+ 119 - 0
src/pages/A1check/A1ledger/ImportData.module.scss

@@ -0,0 +1,119 @@
+.ImportData {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 10;
+  width: 100%;
+  height: 100%;
+  background-color: #fcf9f5;
+  border-radius: 10px;
+  padding: 15px 24px 0;
+
+  :global {
+    .A1ImportTop {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 18px;
+    }
+
+    .A1ImportBack {
+      display: inline-flex;
+      align-items: center;
+      gap: 8px;
+      font-size: 18px;
+      font-weight: 700;
+      color: #4c2f2f;
+      cursor: pointer;
+      user-select: none;
+    }
+    .tips {
+      font-size: 14px;
+      color: #4c2f2f;
+    }
+    .A1ImportRight {
+      display: flex;
+      align-items: center;
+      gap: 14px;
+    }
+
+    .A1ImportTable {
+      .ant-table-cell {
+        padding: 10px !important;
+      }
+    }
+  }
+}
+
+.A1ImportDetailHeader {
+  display: flex;
+  align-items: center;
+  flex-wrap: nowrap;
+  gap: 12px 24px;
+  margin-bottom: 18px;
+  padding: 14px 16px;
+  background-color: #f7f1e9;
+  border: 1px solid #dfcece;
+  border-radius: 8px;
+  font-size: 15px;
+  color: #4c2f2f;
+  overflow: hidden;
+
+  > div {
+    flex: 0 0 auto;
+    white-space: nowrap;
+  }
+}
+
+.A1ImportDetailFile {
+  flex: 1 1 auto !important;
+  min-width: 0;
+  font-size: 16px;
+  font-weight: 700;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.A1ImportSearch {
+  display: flex;
+  align-items: center;
+  flex-wrap: nowrap;
+  gap: 14px;
+  margin-bottom: 16px;
+  overflow: hidden;
+
+  :global {
+    .ant-input-affix-wrapper {
+      flex: 0 0 200px;
+      min-width: 200px;
+      width: 200px;
+      height: 40px;
+      border: none;
+      background-color: transparent;
+      background-size: 100% 100%;
+      background-image: url('../../../assets/layImg/home_input_bg.png');
+    }
+
+    .ant-select {
+      flex: 0 0 180px;
+      width: 180px;
+      height: 40px;
+      border: none;
+
+      .ant-select-selector {
+        background-color: transparent !important;
+        background-size: 100% 100% !important;
+        background-image: url('../../../assets/layImg/home_input_bg.png') !important;
+        border: none !important;
+        box-shadow: none !important;
+      }
+    }
+  }
+}
+
+.A1ImportDetailTable {
+  :global(.ant-table-cell) {
+    padding: 10px !important;
+  }
+}

+ 478 - 0
src/pages/A1check/A1ledger/ImportData.tsx

@@ -0,0 +1,478 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import styles from './ImportData.module.scss'
+import { ArrowLeftOutlined } from '@ant-design/icons'
+import { Button, Input, Modal, Select } from 'antd'
+import AAbtn from '@/pages/ZcomPage/AAbtn'
+import UpBtn from '@/pages/ZcomPage/UpBtn'
+import MyTable from '@/components/MyTable'
+import { downloadFileByUrl } from '@/utils/history'
+import http, { baseUrlTemp, envFlag } from '@/utils/http'
+import dayjs from 'dayjs'
+import { getTokenInfo } from '@/utils/storage'
+
+type Props = {
+  closeFu: () => void
+}
+
+type ImportRecordType = {
+  id: number | string
+  fileName: string
+  importCount: number | string
+  successCount: number | string
+  failCount: number | string
+  importDate: string
+  importUser: string
+  raw: any
+}
+
+type ImportDetailType = {
+  id: string | number
+  rowNum: string | number
+  num: string
+  name: string
+  result: string
+  failReason: string
+  raw: any
+}
+
+type DetailQueryType = {
+  pageNum: number
+  pageSize: number
+  keyword: string
+  result: string
+}
+
+const emptyTxt = '(空)'
+const requestBaseURL = envFlag ? '/api/' : `${baseUrlTemp}/api/`
+const detailApiCandidates = [
+  'cms/importLedger/detailPage',
+  'cms/importLedger/detailList',
+  'cms/importLedger/resultPage',
+  'cms/importLedger/checkPage',
+  'cms/importLedger/checkResultPage',
+  'cms/importLedger/pageDetail'
+]
+
+const pickValue = (item: any, keys: string[], defaultValue: any = emptyTxt) => {
+  for (const key of keys) {
+    const value = item?.[key]
+    if (value === 0 || value) return value
+  }
+  return defaultValue
+}
+
+const pickDateValue = (item: any, keys: string[]) => {
+  const value = pickValue(item, keys, '')
+  if (!value) return emptyTxt
+  return dayjs(value).isValid() ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : value
+}
+
+const normalizeRecord = (item: any, index: number): ImportRecordType => ({
+  id: pickValue(item, ['id', 'importId', 'batchId', 'logId'], `import-${index}`),
+  fileName: pickValue(item, ['fileName', 'excelName', 'originFileName', 'originalName', 'name']),
+  importCount: pickValue(
+    item,
+    ['importCount', 'importNum', 'dataCount', 'totalCount', 'totalNum', 'pcsRegister'],
+    0
+  ),
+  successCount: pickValue(item, ['successCount', 'successNum', 'okCount', 'okNum'], 0),
+  failCount: pickValue(item, ['failCount', 'failNum', 'errorCount', 'errorNum'], 0),
+  importDate: pickDateValue(item, ['importTime', 'uploadTime', 'createTime', 'updateTime']),
+  importUser: pickValue(item, ['importUser', 'importUserName', 'creatorName', 'updateByName']),
+  raw: item
+})
+
+const normalizeCheckResult = (value: any) => {
+  if (value === true || value === 1 || value === '1') return '成功'
+  if (value === false || value === 0 || value === '0') return '失败'
+
+  const text = `${value || ''}`.trim()
+  if (!text) return emptyTxt
+  if (text.includes('成功')) return '成功'
+  if (text.includes('失败')) return '失败'
+  if (['success', 'ok', 'pass'].includes(text.toLowerCase())) return '成功'
+  if (['fail', 'failed', 'error', 'noPass'].includes(text.toLowerCase())) return '失败'
+  return text
+}
+
+const normalizeDetail = (item: any, index: number): ImportDetailType => {
+  const result = normalizeCheckResult(
+    pickValue(item, ['checkResult', 'validateResult', 'result', 'status', 'successFlag'], '')
+  )
+  const failReason = pickValue(
+    item,
+    ['failReason', 'errorMsg', 'msg', 'reason', 'remark'],
+    result === '成功' ? emptyTxt : ''
+  )
+
+  return {
+    id: pickValue(item, ['id', 'detailId', 'rowId'], `detail-${index}`),
+    rowNum: pickValue(
+      item,
+      ['rowNum', 'rowNo', 'lineNum', 'excelRowNum', 'sort', 'index'],
+      index + 1
+    ),
+    num: pickValue(
+      item,
+      ['num', 'registerNum', 'goodsNum', 'collectionNum', 'relicNo', 'relicNum'],
+      emptyTxt
+    ),
+    name: pickValue(item, ['name', 'goodsName', 'antiqueName', 'collectionName'], emptyTxt),
+    result,
+    failReason: failReason || emptyTxt,
+    raw: item
+  }
+}
+
+const getInlineDetailList = (item: any) => {
+  const detailList = pickValue(
+    item,
+    ['detailList', 'details', 'checkList', 'checkResultList', 'resultList'],
+    null
+  )
+  return Array.isArray(detailList) ? detailList : []
+}
+
+const filterInlineDetails = (list: ImportDetailType[], query: DetailQueryType) => {
+  const keyword = query.keyword.trim()
+  const result = query.result
+
+  return list.filter(item => {
+    const keywordPass =
+      !keyword || `${item.num}${item.name}`.toLowerCase().includes(keyword.toLowerCase())
+    const resultPass = !result || item.result === result
+    return keywordPass && resultPass
+  })
+}
+
+const buildDetailPayload = (record: ImportRecordType, query: DetailQueryType) => {
+  const keyword = query.keyword.trim()
+  const result = query.result
+
+  return {
+    id: record.id,
+    importId: record.id,
+    batchId: record.id,
+    logId: record.id,
+    pageNum: query.pageNum,
+    pageSize: query.pageSize,
+    keyword,
+    keyWord: keyword,
+    searchText: keyword,
+    searchKey: keyword,
+    nameOrNum: keyword,
+    result,
+    status: result,
+    checkResult: result,
+    validateResult: result
+  }
+}
+
+const silentPost = async (url: string, data: any) => {
+  const { token } = getTokenInfo()
+  try {
+    const response = await fetch(`${requestBaseURL}${url}`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        ...(token ? { token } : {})
+      },
+      body: JSON.stringify(data)
+    })
+
+    if (!response.ok) return null
+    const result = await response.json().catch(() => null)
+    if (!result || result.code !== 0) return null
+    return result
+  } catch (error) {
+    return null
+  }
+}
+
+function ImportData({ closeFu }: Props) {
+  const [formData, setFormData] = useState({ pageNum: 1, pageSize: 10 })
+  const [tableInfo, setTableInfo] = useState({ list: [] as ImportRecordType[], total: 0 })
+  const [loading, setLoading] = useState(false)
+  const [detailInfo, setDetailInfo] = useState<ImportRecordType | null>(null)
+  const [detailInput, setDetailInput] = useState({ keyword: '', result: '' })
+  const [detailQuery, setDetailQuery] = useState<DetailQueryType>({
+    pageNum: 1,
+    pageSize: 10,
+    keyword: '',
+    result: ''
+  })
+  const [detailTableInfo, setDetailTableInfo] = useState({
+    list: [] as ImportDetailType[],
+    total: 0
+  })
+  const [detailLoading, setDetailLoading] = useState(false)
+  const detailApiPathRef = useRef('')
+
+  const getListFu = useCallback(async () => {
+    setLoading(true)
+    try {
+      const res = await http.post('cms/importLedger/pageList', formData)
+      if (res.code === 0) {
+        const records = (res.data?.records || []).map((item: any, index: number) =>
+          normalizeRecord(item, index)
+        )
+        setTableInfo({
+          list: records,
+          total: res.data?.total || 0
+        })
+      }
+    } finally {
+      setLoading(false)
+    }
+  }, [formData])
+
+  useEffect(() => {
+    getListFu()
+  }, [getListFu])
+
+  const getDetailListFu = useCallback(async () => {
+    if (!detailInfo) return
+
+    const inlineList = getInlineDetailList(detailInfo.raw)
+    if (inlineList.length) {
+      const normalizedList = inlineList.map((item: any, index: number) =>
+        normalizeDetail(item, index)
+      )
+      const filteredList = filterInlineDetails(normalizedList, detailQuery)
+      const start = (detailQuery.pageNum - 1) * detailQuery.pageSize
+      const end = start + detailQuery.pageSize
+      setDetailTableInfo({
+        list: filteredList.slice(start, end),
+        total: filteredList.length
+      })
+      return
+    }
+
+    setDetailLoading(true)
+    try {
+      const payload = buildDetailPayload(detailInfo, detailQuery)
+      const tryPaths = detailApiPathRef.current
+        ? [detailApiPathRef.current]
+        : [...detailApiCandidates]
+
+      for (const path of tryPaths) {
+        const res = await silentPost(path, payload)
+        if (res?.data) {
+          detailApiPathRef.current = path
+          const records = (res.data.records || res.data.list || res.data.rows || []).map(
+            (item: any, index: number) => normalizeDetail(item, index)
+          )
+
+          setDetailTableInfo({
+            list: records,
+            total:
+              res.data.total ||
+              res.data.count ||
+              res.data.totalCount ||
+              res.data.recordTotal ||
+              records.length
+          })
+          return
+        }
+      }
+
+      setDetailTableInfo({ list: [], total: 0 })
+    } finally {
+      setDetailLoading(false)
+    }
+  }, [detailInfo, detailQuery])
+
+  useEffect(() => {
+    getDetailListFu()
+  }, [getDetailListFu])
+
+  const detailHeader = useMemo(() => {
+    if (!detailInfo) return null
+
+    return (
+      <div className={styles.A1ImportDetailHeader}>
+        <div className={styles.A1ImportDetailFile} title={detailInfo.fileName}>
+          文件名称:{detailInfo.fileName}
+        </div>
+        <div>导入数据条数:{detailInfo.importCount}</div>
+        <div>校验成功条数:{detailInfo.successCount}</div>
+        <div>校验失败条数:{detailInfo.failCount}</div>
+      </div>
+    )
+  }, [detailInfo])
+
+  const openDetailModal = useCallback((item: ImportRecordType) => {
+    detailApiPathRef.current = ''
+    setDetailInput({ keyword: '', result: '' })
+    setDetailQuery({
+      pageNum: 1,
+      pageSize: 10,
+      keyword: '',
+      result: ''
+    })
+    setDetailTableInfo({ list: [], total: 0 })
+    setDetailInfo(item)
+  }, [])
+
+  const closeDetailModal = useCallback(() => {
+    setDetailInfo(null)
+    setDetailTableInfo({ list: [], total: 0 })
+  }, [])
+
+  return (
+    <div className={styles.ImportData}>
+      <div className='A1ImportTop'>
+        <div className='A1ImportBack' onClick={closeFu}>
+          <ArrowLeftOutlined />
+          返回上一层
+        </div>
+        <div className='A1ImportRight'>
+          <p className='tips'>仅支持xlsx格式,文件不得大于10M,单次最多上传1000条</p>
+          <AAbtn
+            txt={1}
+            onClick={() => downloadFileByUrl('./myData/xlsx/盘点基准-一普文物信息.xlsx')}
+            tit='下载模板'
+          />
+          <UpBtn
+            tit='上传藏品数据'
+            url='cms/importLedger/uploadExcel'
+            width={140}
+            backFu={() => setFormData(prev => ({ ...prev, pageNum: 1 }))}
+          />
+        </div>
+      </div>
+
+      <div className='A1ImportTable'>
+        <MyTable
+          classKey='A1ImportData'
+          yHeight={640}
+          scrollX={160}
+          loading={loading}
+          list={tableInfo.list}
+          total={tableInfo.total}
+          pageNum={formData.pageNum}
+          pageSize={formData.pageSize}
+          rowKey='id'
+          columnsTemp={[
+            ['index', '序号', 90],
+            ['txt', '文件名称', 'fileName', 260],
+            ['txt', '导入数据(条数)', 'importCount', 150],
+            ['txt', '成功数据(条数)', 'successCount', 150],
+            ['txt', '失败数据(条数)', 'failCount', 150],
+            ['txt', '导入日期', 'importDate', 200],
+            ['txt', '导入用户', 'importUser', 150]
+          ]}
+          lastBtn={[
+            {
+              title: '操作',
+              fixed: 'right',
+              width: 100,
+              render: (item: ImportRecordType) => (
+                <Button size='small' type='text' onClick={() => openDetailModal(item)}>
+                  查看
+                </Button>
+              )
+            }
+          ]}
+          onChange={(pageNum, pageSize) => setFormData({ pageNum, pageSize })}
+        />
+      </div>
+
+      <Modal
+        title='上传数据校验结果'
+        open={!!detailInfo}
+        footer={null}
+        onCancel={closeDetailModal}
+        width={1180}
+        wrapClassName='A1ImportModalWrap'
+        destroyOnClose
+      >
+        {detailHeader}
+
+        <div className={styles.A1ImportSearch}>
+          <Input
+            allowClear
+            maxLength={30}
+            placeholder='搜索藏品登记号、藏品名称、不超过30个字。'
+            value={detailInput.keyword}
+            onChange={e => setDetailInput(prev => ({ ...prev, keyword: e.target.value }))}
+            onPressEnter={() =>
+              setDetailQuery(prev => ({
+                ...prev,
+                pageNum: 1,
+                keyword: detailInput.keyword.trim(),
+                result: detailInput.result
+              }))
+            }
+          />
+          <Select
+            allowClear
+            placeholder='校验结果'
+            value={detailInput.result || null}
+            options={[
+              { label: '成功', value: '成功' },
+              { label: '失败', value: '失败' }
+            ]}
+            onChange={value => setDetailInput(prev => ({ ...prev, result: value || '' }))}
+          />
+          <AAbtn
+            txt={1}
+            onClick={() =>
+              setDetailQuery({
+                pageNum: 1,
+                pageSize: detailQuery.pageSize,
+                keyword: detailInput.keyword.trim(),
+                result: detailInput.result
+              })
+            }
+            tit='查询'
+          />
+          <AAbtn
+            txt={2}
+            onClick={() => {
+              setDetailInput({ keyword: '', result: '' })
+              setDetailQuery(prev => ({
+                ...prev,
+                pageNum: 1,
+                keyword: '',
+                result: ''
+              }))
+            }}
+          />
+        </div>
+
+        <div className={styles.A1ImportDetailTable}>
+          <MyTable
+            classKey='A1ImportDetail'
+            yHeight={440}
+            scrollX={160}
+            loading={detailLoading}
+            list={detailTableInfo.list}
+            total={detailTableInfo.total}
+            pageNum={detailQuery.pageNum}
+            pageSize={detailQuery.pageSize}
+            rowKey='id'
+            columnsTemp={[
+              ['txt', '行数', 'rowNum', 100],
+              ['txt', '藏品登记号', 'num', 220],
+              ['txt', '藏品名称', 'name', 220],
+              ['txt', '校验结果', 'result', 120],
+              ['txt', '失败原因', 'failReason', 360]
+            ]}
+            onChange={(pageNum, pageSize) =>
+              setDetailQuery(prev => ({
+                ...prev,
+                pageNum,
+                pageSize
+              }))
+            }
+          />
+        </div>
+      </Modal>
+    </div>
+  )
+}
+
+const MemoImportData = React.memo(ImportData)
+
+export default MemoImportData

+ 28 - 37
src/pages/A1check/A1ledger/index.tsx

@@ -1,40 +1,19 @@
-import React, { useCallback, useMemo, useRef } from 'react'
+import React, { useCallback, useMemo, useRef, useState } from 'react'
 import styles from './index.module.scss'
 import TopSearch from '@/pages/ZcomPage/TopSearch'
 import { A1tableType, A1topArr } from './data'
 import AAbtn from '@/pages/ZcomPage/AAbtn'
 import { MessageFu } from '@/utils/message'
 import { Button } from 'antd'
-import { downloadFileByUrl, openLink } from '@/utils/history'
+import { openLink } from '@/utils/history'
 import { A1_APIgetList, A1_APIreset } from '@/store/action/A1check/A1ledger'
 import { useSelector } from 'react-redux'
 import { RootState } from '@/store'
-import UpBtn from '@/pages/ZcomPage/UpBtn'
+import ImportData from './ImportData'
 
 function A1ledger() {
-  // --------------右侧按钮
-  const rightBtn = useMemo(() => {
-    return (
-      <>
-        <AAbtn txt={1} onClick={() => MessageFu.warning('功能开发中')} tit='新增藏品' />
-        <AAbtn
-          txt={1}
-          onClick={() => downloadFileByUrl('./myData/xlsx/盘点基准-一普文物信息.xlsx')}
-          tit='下载模板'
-        />
-        <UpBtn
-          tit='导入数据'
-          url='cms/importLedger/uploadExcel'
-          backFu={() => topDomRef.current.clickReset()}
-        />
-
-        <AAbtn txt={1} onClick={() => topDomRef.current.clickSearch()} tit='查询' />
-        <AAbtn txt={2} onClick={() => topDomRef.current.clickReset()} />
-      </>
-    )
-  }, [])
-
   const topDomRef = useRef<any>(null)
+  const [showImportData, setShowImportData] = useState(false)
 
   const tableInfo = useSelector((state: RootState) => state.A1ledger.tableInfo)
 
@@ -46,6 +25,18 @@ function A1ledger() {
     }
   }, [])
 
+  const rightBtn = useMemo(
+    () => (
+      <>
+        <AAbtn txt={1} onClick={() => MessageFu.warning('功能开发中')} tit='新增藏品' />
+        <AAbtn txt={1} onClick={() => setShowImportData(true)} tit='导入数据' />
+        <AAbtn txt={1} onClick={() => topDomRef.current.clickSearch()} tit='查询' />
+        <AAbtn txt={2} onClick={() => topDomRef.current.clickReset()} />
+      </>
+    ),
+    []
+  )
+
   return (
     <div className={styles.A1ledger}>
       <div className='pageTitle'>
@@ -63,7 +54,7 @@ function A1ledger() {
         ref={topDomRef}
         leftArr={A1topArr}
         rightBtn={rightBtn}
-        waiWidth={490}
+        waiWidth={390}
         sonWidth='13.8%'
         getListAPI={A1_APIgetList}
         tableInfo={tableInfo}
@@ -88,20 +79,20 @@ function A1ledger() {
             title: '操作',
             fixed: 'right',
             width: 100,
-            render: (item: A1tableType) => {
-              return (
-                <Button
-                  size='small'
-                  type='text'
-                  onClick={() => openLink(`/goodsLook/${item.numName || null}/${item.num || null}`)}
-                >
-                  查看
-                </Button>
-              )
-            }
+            render: (item: A1tableType) => (
+              <Button
+                size='small'
+                type='text'
+                onClick={() => openLink(`/goodsLook/${item.numName || null}/${item.num || null}`)}
+              >
+                查看
+              </Button>
+            )
           }
         ]}
       />
+
+      {showImportData ? <ImportData closeFu={() => setShowImportData(false)} /> : null}
     </div>
   )
 }

+ 94 - 0
src/pages/A1check/A4collect/data.ts

@@ -0,0 +1,94 @@
+import { resDictStr } from '@/utils/select'
+
+export const A4topArr = [
+  {
+    key: 'numName',
+    placeholder: '编号类型',
+    type: 'Select',
+    options: resDictStr('编号类型')
+  },
+  {
+    key: 'num',
+    placeholder: '请输入编号',
+    type: 'Input'
+  },
+  {
+    key: 'name',
+    placeholder: '请搜索名称或原名',
+    type: 'Input'
+  },
+  {
+    key: 'level',
+    placeholder: '级别',
+    type: 'Select',
+    options: resDictStr('文物级别')
+  },
+  {
+    key: 'type',
+    placeholder: '类别',
+    type: 'Select',
+    options: resDictStr('类别')
+  },
+  {
+    key: 'status',
+    placeholder: '盘点状态',
+    type: 'Select',
+    options: resDictStr('盘点状态')
+  },
+  {
+    key: 'conform',
+    placeholder: '相符情况',
+    type: 'Select',
+    options: resDictStr('相符情况')
+  },
+  {
+    key: 'unInfo',
+    placeholder: '不符情形',
+    type: 'Select',
+    options: resDictStr('不符情形')
+  },
+  {
+    key: 'reasonInfo',
+    placeholder: '不符原因',
+    type: 'Select',
+    options: resDictStr('不符原因')
+  },
+  {
+    key: 'effect',
+    placeholder: '生效状态',
+    type: 'Select',
+    options: resDictStr('生效状态')
+  }
+]
+
+export const A4columnsTemp = [
+  ['index', '序号'],
+  ['txt', '省、自治区、直辖市', 'province'],
+  ['txt', '收藏单位', 'collectionUnit'],
+  ['txt', '藏品总登记账登记的馆藏文物总数(件/套)', 'totalCount'],
+  ['txt', '账物相符馆藏文件总数(件)', 'conformCount'],
+  ['txt', '账物不符差异总数(件)', 'diffCount'],
+  ['txt', '记录错误', 'recordError'],
+  ['txt', '损毁', 'damageCount'],
+  ['txt', '被盗', 'stolenCount'],
+  ['txt', '丢失', 'lostCount'],
+  ['txt', '长期出借 无法确认', 'longTermLoanUnknownCount'],
+  ['txt', '其他', 'otherCount'],
+  ['txt', '备注', 'remark']
+]
+
+export const A4widthSet = {
+  index: 90,
+  province: 160,
+  collectionUnit: 180,
+  totalCount: 220,
+  conformCount: 180,
+  diffCount: 180,
+  recordError: 120,
+  damageCount: 100,
+  stolenCount: 100,
+  lostCount: 100,
+  longTermLoanUnknownCount: 160,
+  otherCount: 100,
+  remark: 180
+}

+ 67 - 0
src/pages/A1check/A4collect/index.module.scss

@@ -1,4 +1,71 @@
 .A4collect {
+  width: 100%;
+  height: 100%;
+
   :global {
+    .pageTitle {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 15px;
+    }
+
+    .A4content {
+      background-color: #fcf9f5;
+      border-radius: 10px;
+      padding: 15px 24px 0;
+      position: relative;
+      height: calc(100% - 55px);
+    }
+
+    .A4searchRow {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 14px 12px;
+    }
+
+    .A4searchItem {
+      width: calc(20% - 10px);
+      min-width: 180px;
+
+      .ant-select,
+      .ant-input-affix-wrapper,
+      .ant-select {
+        width: 100%;
+      }
+
+      .ant-select {
+        height: 40px;
+        border: none;
+
+        .ant-select-selector {
+          background-color: transparent;
+          background-size: 100% 100%;
+          background-image: url('../../../assets/layImg/home_input_bg.png');
+        }
+      }
+
+      .ant-input-affix-wrapper {
+        width: 100%;
+        height: 40px;
+        border: none;
+        background-color: transparent;
+        background-size: 100% 100%;
+        background-image: url('../../../assets/layImg/home_input_bg.png');
+      }
+    }
+
+    .A4actionRow {
+      display: flex;
+      justify-content: flex-end;
+      gap: 10px;
+      margin: 16px 0 12px;
+    }
+
+    .A4table {
+      .ant-table-cell {
+        padding: 10px 8px !important;
+      }
+    }
   }
 }

+ 335 - 2
src/pages/A1check/A4collect/index.tsx

@@ -1,9 +1,342 @@
-import React from 'react'
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
 import styles from './index.module.scss'
+import { A4columnsTemp, A4topArr, A4widthSet } from './data'
+import AAbtn from '@/pages/ZcomPage/AAbtn'
+import { Input, Select, Button } from 'antd'
+import MyTable from '@/components/MyTable'
+import ExportJsonExcel from 'js-export-excel'
+import dayjs from 'dayjs'
+import { MessageFu } from '@/utils/message'
+import { getTokenInfo } from '@/utils/storage'
+import { baseUrlTemp, envFlag } from '@/utils/http'
+
+type FormStateType = {
+  pageNum: number
+  pageSize: number
+  numName?: string
+  num?: string
+  name?: string
+  level?: string
+  type?: string
+  status?: string
+  conform?: string
+  unInfo?: string
+  reasonInfo?: string
+  effect?: string
+}
+
+type CollectRowType = {
+  id: string | number
+  province: string
+  collectionUnit: string
+  totalCount: number | string
+  conformCount: number | string
+  diffCount: number | string
+  recordError: number | string
+  damageCount: number | string
+  stolenCount: number | string
+  lostCount: number | string
+  longTermLoanUnknownCount: number | string
+  otherCount: number | string
+  remark: string
+  raw: any
+}
+
+const requestBaseURL = envFlag ? '/api/' : `${baseUrlTemp}/api/`
+const emptyTxt = '(空)'
+const apiCandidates = [
+  'cms/checkMatch/collectPage',
+  'cms/checkMatch/summaryPage',
+  'cms/checkMatch/collectList',
+  'cms/checkMatch/summaryList',
+  'cms/checkMatch/statPage'
+]
+
+const pickValue = (item: any, keys: string[], defaultValue: any = emptyTxt) => {
+  for (const key of keys) {
+    const value = item?.[key]
+    if (value === 0 || value) return value
+  }
+  return defaultValue
+}
+
+const normalizeRow = (item: any, index: number): CollectRowType => ({
+  id: pickValue(item, ['id', 'summaryId', 'collectId', 'statId'], `collect-${index}`),
+  province: pickValue(item, ['province', 'provinceName', 'areaName', 'regionName']),
+  collectionUnit: pickValue(item, [
+    'collectionUnit',
+    'collectionName',
+    'museumName',
+    'unitName',
+    'deptName'
+  ]),
+  totalCount: pickValue(
+    item,
+    ['totalCount', 'registerCount', 'goodsTotalCount', 'pcsRegister', 'relicTotalCount'],
+    0
+  ),
+  conformCount: pickValue(item, ['conformCount', 'matchCount', 'pcsConform'], 0),
+  diffCount: pickValue(item, ['diffCount', 'unConformCount', 'pcsUnConform'], 0),
+  recordError: pickValue(item, ['recordError', 'recordErrorCount'], 0),
+  damageCount: pickValue(item, ['damageCount', 'damageNum', 'tornCount'], 0),
+  stolenCount: pickValue(item, ['stolenCount', 'stolenNum'], 0),
+  lostCount: pickValue(item, ['lostCount', 'lostNum'], 0),
+  longTermLoanUnknownCount: pickValue(
+    item,
+    [
+      'longTermLoanUnknownCount',
+      'loanUnknownCount',
+      'longTermLoanCount',
+      'loanCount',
+      'lendCount',
+      'borrowOutCount',
+      'unableConfirmCount',
+      'unknownCount',
+      'unConfirmCount'
+    ],
+    0
+  ),
+  otherCount: pickValue(item, ['otherCount', 'otherNum'], 0),
+  remark: pickValue(item, ['remark', 'memo', 'comment']),
+  raw: item
+})
+
+const silentPost = async (url: string, data: any) => {
+  const { token } = getTokenInfo()
+  try {
+    const response = await fetch(`${requestBaseURL}${url}`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        ...(token ? { token } : {})
+      },
+      body: JSON.stringify(data)
+    })
+
+    if (!response.ok) return null
+
+    const result = await response.json().catch(() => null)
+    if (!result || result.code !== 0) return null
+    return result
+  } catch (error) {
+    return null
+  }
+}
+
 function A4collect() {
+  const [formData, setFormData] = useState<FormStateType>({
+    pageNum: 1,
+    pageSize: 10
+  })
+  const [searchForm, setSearchForm] = useState<FormStateType>({
+    pageNum: 1,
+    pageSize: 10
+  })
+  const [tableInfo, setTableInfo] = useState({ list: [] as CollectRowType[], total: 0 })
+  const [loading, setLoading] = useState(false)
+  const [apiPath, setApiPath] = useState('')
+
+  const getListFu = useCallback(async () => {
+    setLoading(true)
+    try {
+      const tryPaths = apiPath ? [apiPath] : [...apiCandidates]
+
+      for (const path of tryPaths) {
+        const res = await silentPost(path, formData)
+        if (res?.data) {
+          const records = (res.data.records || res.data.list || res.data.rows || []).map(
+            (item: any, index: number) => normalizeRow(item, index)
+          )
+
+          setApiPath(path)
+          setTableInfo({
+            list: records,
+            total:
+              res.data.total ||
+              res.data.count ||
+              res.data.totalCount ||
+              res.data.recordTotal ||
+              records.length
+          })
+          return
+        }
+      }
+
+      setTableInfo({ list: [], total: 0 })
+    } finally {
+      setLoading(false)
+    }
+  }, [apiPath, formData])
+
+  useEffect(() => {
+    getListFu()
+  }, [getListFu])
+
+  const exportData = useCallback(async () => {
+    const exportPayload = { ...formData, pageNum: 1, pageSize: 999999 }
+    const tryPaths = apiPath ? [apiPath] : [...apiCandidates]
+
+    for (const path of tryPaths) {
+      const res = await silentPost(path, exportPayload)
+      if (res?.data) {
+        const records = (res.data.records || res.data.list || res.data.rows || []).map(
+          (item: any, index: number) => normalizeRow(item, index)
+        )
+
+        if (!records.length) {
+          MessageFu.warning('当前搜索条件无数据')
+          return
+        }
+
+        const exportList = records.map((item: CollectRowType, index: number) => ({
+          序号: index + 1,
+          省自治区直辖市: item.province,
+          收藏单位: item.collectionUnit,
+          藏品总登记账登记的馆藏文物总数件套: item.totalCount,
+          账物相符馆藏文件总数件: item.conformCount,
+          账物不符差异总数件: item.diffCount,
+          记录错误: item.recordError,
+          损毁: item.damageCount,
+          被盗: item.stolenCount,
+          丢失: item.lostCount,
+          长期出借无法确认: item.longTermLoanUnknownCount,
+          其他: item.otherCount,
+          备注: item.remark
+        }))
+
+        const option = {
+          fileName: '各馆盘库结果汇总' + dayjs().format('YYYY-MM-DD HH:mm'),
+          datas: [
+            {
+              sheetData: exportList,
+              sheetName: '各馆盘库结果汇总',
+              sheetFilter: Object.keys(exportList[0]),
+              sheetHeader: [
+                '序号',
+                '省、自治区、直辖市',
+                '收藏单位',
+                '藏品总登记账登记的馆藏文物总数(件/套)',
+                '账物相符馆藏文件总数(件)',
+                '账物不符差异总数(件)',
+                '记录错误',
+                '损毁',
+                '被盗',
+                '丢失',
+                '长期出借',
+                '无法确认',
+                '其他',
+                '备注'
+              ]
+            }
+          ]
+        }
+
+        new ExportJsonExcel(option).saveExcel()
+        return
+      }
+    }
+
+    MessageFu.warning('暂无可导出的汇总数据')
+  }, [apiPath, formData])
+
+  const rightTopBtn = useMemo(
+    () => <AAbtn txt={1} onClick={exportData} tit='导出数据' width={110} />,
+    [exportData]
+  )
+
   return (
     <div className={styles.A4collect}>
-      <h1>A4collect</h1>
+      <div className='pageTitle'>各馆盘库结果汇总</div>
+
+      <div className='A4content'>
+        <div className='A4searchRow'>
+          {A4topArr.map(item => (
+            <div className='A4searchItem' key={item.key}>
+              {item.type === 'Select' ? (
+                <Select
+                  allowClear
+                  placeholder={item.placeholder}
+                  options={item.options}
+                  value={(searchForm as any)[item.key] || null}
+                  onChange={value =>
+                    setSearchForm(prev => ({ ...prev, [item.key]: value || undefined }))
+                  }
+                />
+              ) : (
+                <Input
+                  allowClear
+                  placeholder={item.placeholder}
+                  value={(searchForm as any)[item.key] || ''}
+                  onChange={e =>
+                    setSearchForm(prev => ({
+                      ...prev,
+                      [item.key]: e.target.value.trim() || undefined
+                    }))
+                  }
+                />
+              )}
+            </div>
+          ))}
+        </div>
+
+        <div className='A4actionRow'>
+          <AAbtn
+            txt={1}
+            onClick={() =>
+              setFormData({
+                ...searchForm,
+                pageNum: 1,
+                pageSize: formData.pageSize
+              })
+            }
+            tit='查询'
+          />
+          <AAbtn
+            txt={2}
+            onClick={() => {
+              const resetForm = { pageNum: 1, pageSize: formData.pageSize }
+              setSearchForm(resetForm)
+              setFormData(resetForm)
+            }}
+          />
+          {rightTopBtn}
+        </div>
+
+        <div className='A4table'>
+          <MyTable
+            classKey='A4collect'
+            yHeight={610}
+            scrollX={160}
+            loading={loading}
+            list={tableInfo.list}
+            total={tableInfo.total}
+            pageNum={formData.pageNum}
+            pageSize={formData.pageSize}
+            rowKey='id'
+            columnsTemp={A4columnsTemp}
+            widthSet={A4widthSet}
+            lastBtn={[
+              {
+                title: '操作',
+                fixed: 'right',
+                width: 100,
+                render: () => (
+                  <Button size='small' type='text' onClick={() => MessageFu.warning('功能开发中')}>
+                    查看
+                  </Button>
+                )
+              }
+            ]}
+            onChange={(pageNum, pageSize) =>
+              setFormData(prev => ({
+                ...prev,
+                pageNum,
+                pageSize
+              }))
+            }
+          />
+        </div>
+      </div>
     </div>
   )
 }

+ 12 - 0
src/setupProxy.js

@@ -0,0 +1,12 @@
+const { createProxyMiddleware } = require('http-proxy-middleware')
+
+module.exports = function (app) {
+  app.use(
+    '/api',
+    createProxyMiddleware({
+      target: 'https://sit-ciwu.4dage.com',
+      changeOrigin: true,
+      secure: false
+    })
+  )
+}

+ 9 - 9
src/utils/http.ts

@@ -7,27 +7,27 @@ import history, { loginOutFu } from './history'
 
 export const envFlag = process.env.NODE_ENV === 'development'
 
-// const baseUrlTemp = baseUrlTempOne // 测试环境
-export const baseUrlTemp = 'http://192.168.20.61:8096' // 线下环境
+export const baseUrlTemp = baseUrlTempOne // 测试环境
+// export const baseUrlTemp = 'http://192.168.20.61:8096' // 线下环境
 
-const baseFlag = baseUrlTemp.includes('https://') || !envFlag
+// 静态资源和文件地址
+export const baseURL = baseUrlTemp
 
-// 请求基地址
-export const baseURL = envFlag ? `${baseUrlTemp}${baseFlag ? '' : '/api/'}` : baseUrlTemp
+// 开发环境通过本地代理转发 /api,避免浏览器直接跨域
+const requestBaseURL = envFlag ? '/api/' : `${baseUrlTemp}/api/`
 
-// 处理  类型“AxiosResponse<any, any>”上不存在属性“code”
+// 处理 类型“AxiosResponse<any, any>”上不存在属性“code”
 declare module 'axios' {
   interface AxiosResponse {
     code: number
     msg: string
     timestamp: string
-    // 这里追加你的参数
   }
 }
 
 // 创建 axios 实例
 const http = axios.create({
-  baseURL: `${baseURL}${baseFlag ? '/api/' : ''}`,
+  baseURL: requestBaseURL,
   timeout: 30000
 })
 
@@ -85,7 +85,7 @@ http.interceptors.response.use(
     timeId = window.setTimeout(() => {
       axajInd = 0
       domShowFu('#AsyncSpinLoding', false)
-      // 如果因为网络原因,response没有,给提示消息
+      // 如果因为网络原因,response 没有,给提示消息
       if (!err.response) {
         if (store.getState().A0Layout.closeUpFile.state) MessageFu.warning('取消上传!')
         else {