|
|
@@ -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
|