shaogen1995 пре 1 месец
родитељ
комит
c4c8af4061

+ 2 - 1
package.json

@@ -24,6 +24,7 @@
     "file-saver": "^2.0.5",
     "js-base64": "^3.7.3",
     "js-export-excel": "^1.1.4",
+    "jszip": "^3.10.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-redux": "^8.0.4",
@@ -69,4 +70,4 @@
     "react-app-rewired": "^2.2.1"
   },
   "homepage": "."
-}
+}

+ 5 - 0
src/components/MyTable/index.tsx

@@ -105,6 +105,11 @@ function MyTable({
             />
           </div>
         ),
+        length: (item: any) => {
+          const arr = item[v[2]] || []
+
+          return arr.length
+        },
 
         time: (item: any) => {
           let txt = isNull

+ 6 - 3
src/components/ZupTypes/index.tsx

@@ -26,7 +26,10 @@ export type FileListType = {
   originalUrl: string
   compressedUrl: string
   _id: string
-  type: 'model' | 'img' | 'audio' | 'video'
+  type: 'model' | 'img' | 'audio' | 'video' | 'doc'
+  // ----------
+  remark: string
+  state: boolean
 }
 
 type Props = {
@@ -351,7 +354,7 @@ function ZupTypes(
   // ------------让父组件调用的 回显
   const setFileComFileFu = useCallback(
     (info: any) => {
-      const obj = {
+      const obj: any = {
         model: {} as FileListType,
         img: [] as FileListType[],
         audio: {} as FileListType,
@@ -368,7 +371,7 @@ function ZupTypes(
         data.forEach(v => {
           if (v.type === 'img') {
             obj.img.push(v)
-          } else obj[v.type!] = v
+          } else obj[v.type] = v
         })
         setFileList(obj)
       }

+ 1 - 1
src/pages/A2video/index.tsx

@@ -99,7 +99,7 @@ function A2video() {
         <div className='A2topll'>
           <Input
             key={inputKey}
-            maxLength={10}
+            maxLength={30}
             showCount
             style={{ width: 300 }}
             placeholder='请输入标题'

+ 1 - 1
src/pages/A3goods/index.tsx

@@ -102,7 +102,7 @@ function A3goods() {
         <div className='A3topll'>
           <Input
             key={inputKey}
-            maxLength={10}
+            maxLength={30}
             showCount
             style={{ width: 300 }}
             placeholder='请输入标题'

+ 19 - 3
src/pages/A4advanced/A4add/index.tsx

@@ -9,6 +9,7 @@ import { A4typeSelectType } from '../data'
 import ZupOne from '@/components/ZupOne'
 import ZRichTextOne from '@/components/ZRichTextOne'
 import MyPopconfirm from '@/components/MyPopconfirm'
+import TextArea from 'antd/es/input/TextArea'
 
 type Props = {
   txt: AddTxtType
@@ -19,6 +20,8 @@ type Props = {
 }
 
 function A4add({ txt, _id, closeFu, addTableFu, editTableFu }: Props) {
+  const [type, setType] = useState('')
+
   const FormBoxRef = useRef<FormInstance>(null)
   // 封面图的ref
   const ZupThumbRef = useRef<any>(null)
@@ -29,6 +32,8 @@ function A4add({ txt, _id, closeFu, addTableFu, editTableFu }: Props) {
     if (res.code === 0) {
       const info = res.data
 
+      setType(info.type)
+
       if (info.releaseDate) info.releaseDate = dayjs(info.releaseDate)
 
       FormBoxRef.current?.setFieldsValue(info)
@@ -70,7 +75,7 @@ function A4add({ txt, _id, closeFu, addTableFu, editTableFu }: Props) {
 
       // 富文本
       const rtf = ZRichTextRef.current?.fatherBtnOkFu() || { flag: true }
-      if (rtf.flag) return MessageFu.warning('请输入内容')
+      if (rtf.flag && type !== '轮播图') return MessageFu.warning('请输入内容')
 
       const obj = {
         ...values,
@@ -91,7 +96,7 @@ function A4add({ txt, _id, closeFu, addTableFu, editTableFu }: Props) {
       }
       // console.log("通过校验,点击确定");
     },
-    [txt, _id, addTableFu, editTableFu, closeFu]
+    [type, txt, _id, addTableFu, editTableFu, closeFu]
   )
 
   return (
@@ -124,6 +129,8 @@ function A4add({ txt, _id, closeFu, addTableFu, editTableFu }: Props) {
 
           <Form.Item label='类别' name='type' rules={[{ required: true, message: '请选择类别' }]}>
             <Select
+              value={type}
+              onChange={e => setType(e)}
               disabled={txt === '查看'}
               style={{ width: 150 }}
               options={A4typeSelectType}
@@ -151,6 +158,15 @@ function A4add({ txt, _id, closeFu, addTableFu, editTableFu }: Props) {
             </div>
           </div>
 
+          <Form.Item label='简介' name='intro'>
+            <TextArea
+              readOnly={txt === '查看'}
+              maxLength={500}
+              showCount
+              placeholder='请输入内容'
+            />
+          </Form.Item>
+
           <div className='sortForm'>
             <Form.Item
               label='排序值'
@@ -167,7 +183,7 @@ function A4add({ txt, _id, closeFu, addTableFu, editTableFu }: Props) {
 
           <div className='formBox'>
             <div className='formBoxll'>
-              <span>* </span>内容:
+              <span>{type === '轮播图' ? '' : '* '}</span>内容:
             </div>
             <div className='formBoxrr'>
               <ZRichTextOne

+ 1 - 1
src/pages/A4advanced/index.tsx

@@ -100,7 +100,7 @@ function A4advanced() {
         <div className='A4topll'>
           <Input
             key={inputKey}
-            maxLength={10}
+            maxLength={30}
             showCount
             style={{ width: 300 }}
             placeholder='请输入标题'

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

@@ -9,7 +9,7 @@ import { Button, Input, Select } from 'antd'
 import MyPopconfirm from '@/components/MyPopconfirm'
 import { AddTxtType, fromDataBase } from '../A2video/data'
 import MyTable from '@/components/MyTable'
-import { A4tableC } from '@/utils/tableData'
+import { A5tableC } from '@/utils/tableData'
 import A5add from './A5add'
 function A5share() {
   const dispatch = useDispatch()
@@ -100,7 +100,7 @@ function A5share() {
         <div className='A5topll'>
           <Input
             key={inputKey}
-            maxLength={10}
+            maxLength={30}
             showCount
             style={{ width: 300 }}
             placeholder='请输入标题'
@@ -129,7 +129,7 @@ function A5share() {
       <MyTable
         yHeight={650}
         list={tableInfo.list}
-        columnsTemp={A4tableC}
+        columnsTemp={A5tableC}
         lastBtn={tableLastBtn}
         pageNum={fromData.pageNum}
         pageSize={fromData.pageSize}

+ 65 - 0
src/pages/B1audit/B1btn/index.module.scss

@@ -0,0 +1,65 @@
+.B1btn {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  border-radius: 10px;
+
+  :global {
+    .B1Bmain {
+      padding: 20px;
+      height: calc(100% - 70px);
+      overflow-y: auto;
+
+      .B1BtopBox {
+        font-size: 18px;
+        display: flex;
+        flex-wrap: wrap;
+        .B1Btop {
+          width: 50%;
+          display: flex;
+          margin-bottom: 20px;
+          .B1Btopll {
+            width: 100px;
+            font-weight: 700;
+            text-align: right;
+          }
+          .B1Btoprr {
+            width: calc(100% - 100px);
+            word-wrap: break-word;
+          }
+        }
+        .B1Btop2 {
+          width: 100%;
+        }
+      }
+
+      .B1Btit {
+        color: var(--themeColor);
+        margin: 0 0 15px 0;
+        font-size: 20px;
+        font-weight: 700;
+      }
+      textarea {
+        min-height: 30px !important;
+      }
+      .ant-checkbox-inner {
+        width: 24px;
+        height: 24px;
+      }
+    }
+
+    .B1Bbtn {
+      height: 70px;
+      display: flex;
+      align-items: center;
+      padding-left: 100px;
+      border-top: 2px solid #ccc;
+      .ant-btn {
+        margin-right: 30px;
+      }
+    }
+  }
+}

+ 215 - 0
src/pages/B1audit/B1btn/index.tsx

@@ -0,0 +1,215 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
+import styles from './index.module.scss'
+import { B1_APIaudit, B1_APIgetInfo } from '@/store/action/B1audit'
+import { b1BtypeObj, B1listType } from '../data'
+import { Button, Table } from 'antd'
+import { FileListType } from '@/components/ZupTypes'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import TextArea from 'antd/es/input/TextArea'
+import { baseURL } from '@/utils/http'
+import store from '@/store'
+import { MessageFu } from '@/utils/message'
+
+type Props = {
+  sId: string
+  isLook: boolean
+  closeFu: () => void
+  upTableFu: () => void
+}
+
+function B1btn({ sId, isLook, closeFu, upTableFu }: Props) {
+  const [info, setInfo] = useState({} as B1listType)
+
+  const getInfoFu = useCallback(async (_id: string) => {
+    const res = await B1_APIgetInfo(_id)
+    if (res.code === 0) {
+      const infoTemp: B1listType = res.data
+      setInfo(infoTemp)
+      setIds(infoTemp.fileDetails.filter(v => v.state).map(c => c._id))
+    }
+  }, [])
+
+  useEffect(() => {
+    getInfoFu(sId)
+  }, [getInfoFu, sId])
+
+  const [ids, setIds] = useState<React.Key[]>([])
+
+  const rowSelection = useMemo(() => {
+    return {
+      selectedRowKeys: ids,
+      onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => {
+        setIds(selectedRowKeys)
+      },
+      getCheckboxProps: (record: any) => ({
+        disabled: isLook,
+        name: record.name
+      })
+    }
+  }, [ids, isLook])
+
+  const columns = useMemo(() => {
+    return [
+      {
+        title: '文件名',
+        render: (item: FileListType) => item.originalName || '(空)'
+      },
+      {
+        title: '类型',
+        render: (item: FileListType) => Reflect.get(b1BtypeObj, item.type) || '(空)'
+      },
+      {
+        title: '备注',
+        width: 600,
+        render: (item: FileListType) => (
+          <TextArea
+            readOnly={isLook}
+            maxLength={200}
+            autoSize
+            value={item.remark || ''}
+            placeholder='请输入'
+            onChange={e =>
+              setInfo({
+                ...info,
+                fileDetails: info.fileDetails.map(v => ({
+                  ...v,
+                  remark: v._id === item._id ? e.target.value : v.remark
+                }))
+              })
+            }
+          />
+        )
+      },
+      {
+        title: '操作',
+        width: 150,
+        render: (item: FileListType) => {
+          const lastNameArr = item.originalName.split('.')
+          let laseName = lastNameArr[lastNameArr.length - 1]
+          laseName = laseName.toLocaleLowerCase()
+          return (
+            <>
+              {['img', 'video'].includes(item.type) ? (
+                <Button
+                  size='small'
+                  type='text'
+                  onClick={() => {
+                    if (item.type === 'img') {
+                      store.dispatch({
+                        type: 'layout/lookBigImg',
+                        payload: { url: baseURL + item.originalUrl, show: true }
+                      })
+                    } else if (item.type === 'video') {
+                      store.dispatch({
+                        type: 'layout/lookDom',
+                        payload: { src: item.originalUrl, type: 'video' }
+                      })
+                    }
+                  }}
+                >
+                  查看
+                </Button>
+              ) : null}
+
+              {item.type === 'doc' && laseName === 'pdf' ? (
+                <Button
+                  size='small'
+                  type='text'
+                  onClick={() => {
+                    window.open(baseURL + item.originalUrl)
+                  }}
+                >
+                  查看
+                </Button>
+              ) : null}
+
+              <Button size='small' type='text'>
+                <a href={baseURL + item.originalUrl} download target='_blank' rel='noreferrer'>
+                  下载
+                </a>
+              </Button>
+            </>
+          )
+        }
+      }
+    ]
+  }, [info, isLook])
+
+  // 点击提交
+  const btnOk = useCallback(async () => {
+    if (ids.length <= 0) return MessageFu.warning('请至少选择一个文件')
+
+    const auditArr: any[] = []
+
+    ids.forEach(v => {
+      let remark = ''
+
+      const remarkObj = info.fileDetails.find(c => c._id === v)
+
+      if (remarkObj) remark = remarkObj.remark
+
+      auditArr.push({ _id: v, remark })
+    })
+
+    const obj = {
+      _id: sId,
+      auditArr
+    }
+
+    const res = await B1_APIaudit(obj)
+
+    if (res.code === 0) {
+      MessageFu.success('审核成功')
+      closeFu()
+      upTableFu()
+    }
+  }, [closeFu, ids, info.fileDetails, sId, upTableFu])
+
+  return (
+    <div className={styles.B1btn}>
+      <div className='B1Bmain'>
+        <div className='B1BtopBox'>
+          <div className='B1Btop'>
+            <div className='B1Btopll'>标题:</div>
+            <div className='B1Btoprr'>{info.title}</div>
+          </div>
+          <div className='B1Btop'>
+            <div className='B1Btopll'>联系方式:</div>
+            <div className='B1Btoprr'>{info.title}</div>
+          </div>
+          <div className='B1Btop B1Btop2'>
+            <div className='B1Btopll'>概述:</div>
+            <div className='B1Btoprr'>{info.description}</div>
+          </div>
+        </div>
+        <div className='B1Btit'>文件</div>
+        {/* 表格主体 */}
+        <Table
+          rowSelection={{ type: 'checkbox', ...rowSelection }}
+          rowKey='_id'
+          dataSource={info.fileDetails || []}
+          columns={columns}
+          pagination={false}
+        />
+      </div>
+
+      {/* 按钮 */}
+      <div className='B1Bbtn'>
+        {isLook ? (
+          <Button onClick={closeFu}>返回</Button>
+        ) : (
+          <>
+            <Button type='primary' onClick={btnOk}>
+              提交
+            </Button>
+            <MyPopconfirm txtK='取消' onConfirm={closeFu} />
+          </>
+        )}
+      </div>
+    </div>
+  )
+}
+
+const MemoB1btn = React.memo(B1btn)
+
+export default MemoB1btn

+ 20 - 0
src/pages/B1audit/data.ts

@@ -0,0 +1,20 @@
+import { FileListType } from '@/components/ZupTypes'
+
+export type B1listType = {
+  _id: string
+  title: string
+  phone: string
+  description: string
+  fileIds: string[]
+  fileDetails: FileListType[]
+  state: boolean
+  createTime: string
+  updateTime: string
+  __v: number
+}
+
+export const b1BtypeObj = {
+  img: '图片',
+  video: '视频',
+  doc: '文档'
+}

+ 12 - 0
src/pages/B1audit/index.module.scss

@@ -1,4 +1,16 @@
 .B1audit {
+  background-color: #fff;
+  border-radius: 10px;
+  padding: 20px;
+  position: relative;
   :global {
+    .B1top {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 20px;
+    }
+    .ant-table-cell {
+      padding: 8px !important;
+    }
   }
 }

+ 142 - 2
src/pages/B1audit/index.tsx

@@ -1,9 +1,149 @@
-import React from 'react'
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
 import styles from './index.module.scss'
+import { useDispatch, useSelector } from 'react-redux'
+import { fromDataBase } from '../A2video/data'
+import { B1_APIdel, B1_APIgetList } from '@/store/action/B1audit'
+import { RootState } from '@/store'
+import { MessageFu } from '@/utils/message'
+import { B1listType } from './data'
+import { Button, Input } from 'antd'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import MyTable from '@/components/MyTable'
+import { B1tableC } from '@/utils/tableData'
+import B1btn from './B1btn'
 function B1audit() {
+  const dispatch = useDispatch()
+
+  // 顶部筛选
+  const [fromData, setFromData] = useState(fromDataBase)
+
+  // 封装发送请求的函数
+  const getListFu = useCallback(async () => {
+    dispatch(B1_APIgetList(fromData))
+  }, [dispatch, fromData])
+
+  useEffect(() => {
+    getListFu()
+  }, [getListFu])
+
+  const timeRef = useRef(-1)
+  // 输入框的输入
+  const txtChangeFu = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>, key: 'searchKey') => {
+      clearTimeout(timeRef.current)
+      timeRef.current = window.setTimeout(() => {
+        setFromData({
+          ...fromData,
+          [key]: e.target.value,
+          pageNum: 1
+        })
+      }, 500)
+    },
+    [fromData]
+  )
+  // 点击重置
+  const [inputKey, setInputKey] = useState(1)
+  const resetSelectFu = useCallback(() => {
+    setInputKey(Date.now())
+    setFromData({ ...fromDataBase })
+  }, [])
+
+  // 从仓库中获取表格数据
+  const tableInfo = useSelector((state: RootState) => state.B1audit.tableInfo)
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: string) => {
+      const res: any = await B1_APIdel(id)
+      if (res.code === 0) {
+        MessageFu.success('删除成功')
+        getListFu()
+      }
+    },
+    [getListFu]
+  )
+
+  const isLook = useRef(false)
+
+  const tableLastBtn = useMemo(() => {
+    return [
+      {
+        title: '操作',
+        render: (item: B1listType) => (
+          <>
+            {item.state ? (
+              <Button
+                size='small'
+                type='text'
+                onClick={() => {
+                  isLook.current = true
+                  setAuditId(item._id)
+                }}
+              >
+                查看
+              </Button>
+            ) : (
+              <Button
+                size='small'
+                type='text'
+                onClick={() => {
+                  isLook.current = false
+                  setAuditId(item._id)
+                }}
+              >
+                审核
+              </Button>
+            )}
+
+            <MyPopconfirm txtK='删除' onConfirm={() => delTableFu(item._id)} />
+          </>
+        )
+      }
+    ]
+  }, [delTableFu])
+
+  const [aduitId, setAuditId] = useState('')
+
   return (
     <div className={styles.B1audit}>
-      <h1>B1audit</h1>
+      <div className='pageTitle'>素材审核{aduitId ? ' - 审核' : ''}</div>
+      <div className='B1top'>
+        <div className='B1topll'>
+          <Input
+            key={inputKey}
+            maxLength={30}
+            showCount
+            style={{ width: 300 }}
+            placeholder='请输入标题'
+            allowClear
+            onChange={e => txtChangeFu(e, 'searchKey')}
+          />
+        </div>
+        <div className='B1toprr'>
+          <Button onClick={resetSelectFu}>重置</Button>
+        </div>
+      </div>
+
+      {/* 表格主体 */}
+      <MyTable
+        yHeight={650}
+        list={tableInfo.list}
+        columnsTemp={B1tableC}
+        lastBtn={tableLastBtn}
+        pageNum={fromData.pageNum}
+        pageSize={fromData.pageSize}
+        total={tableInfo.total}
+        onChange={(pageNum, pageSize) => setFromData({ ...fromData, pageNum, pageSize })}
+      />
+
+      {aduitId ? (
+        <B1btn
+          sId={aduitId}
+          isLook={isLook.current}
+          closeFu={() => setAuditId('')}
+          upTableFu={getListFu}
+        />
+      ) : null}
     </div>
   )
 }

+ 90 - 0
src/pages/B2library/data.ts

@@ -0,0 +1,90 @@
+import JSZip from 'jszip'
+import { saveAs } from 'file-saver'
+import { MessageFu } from '@/utils/message'
+import { domShowFu } from '@/utils/domShow'
+
+export const B2typeSelectType = [
+  { value: 'img', label: '图片' },
+  { value: 'video', label: '视频' },
+  { value: 'doc', label: '文档' }
+]
+
+/**
+ * 批量下载文件并压缩为ZIP包
+ * @param {Array} fileList - 文件地址列表,支持字符串或对象
+ * @param {String} zipName - 生成的ZIP包名称(可选,默认为"download.zip")
+ */
+export async function downloadFilesAsZip(fileList: any[], zipName: string, back: () => void) {
+  // 参数校验
+  if (!fileList || !Array.isArray(fileList) || fileList.length === 0) {
+    MessageFu.warning('至少选择一个文件')
+    return
+  }
+  MessageFu.info('正在下载中,请等待')
+  domShowFu('#AsyncSpinLoding', true)
+
+  const zip = new JSZip()
+  const promises: any[] = [] // 用于存储所有文件下载的Promise
+
+  fileList.forEach((fileItem, index) => {
+    // 统一处理文件地址和自定义文件名
+    let fileUrl = ''
+    let fileName = ''
+    if (typeof fileItem === 'string') {
+      fileUrl = fileItem
+      // 从URL中提取文件名,如果失败则使用索引
+      fileName = fileUrl.split('/').pop() || `file_${index}`
+    } else if (fileItem.url) {
+      fileUrl = fileItem.url
+      // 优先使用对象中指定的文件名
+      fileName = fileItem.name || fileUrl.split('/').pop() || `file_${index}`
+    } else {
+      MessageFu.warning(`文件项格式错误,已跳过:${fileItem}`)
+      return
+    }
+
+    // 对每个文件发起异步请求
+    const promise = fetch(fileUrl)
+      .then(response => {
+        if (!response.ok) {
+          MessageFu.error(`网络响应异常: ${response.status}`)
+          domShowFu('#AsyncSpinLoding', false)
+          throw new Error(`网络响应异常: ${response.status}`)
+        }
+        return response.blob() // 将响应转换为Blob对象
+      })
+      .then(blob => {
+        // 将Blob对象添加到ZIP压缩包中
+        zip.file(fileName, blob)
+      })
+      .catch(error => {
+        MessageFu.error(`下载文件失败 "${fileName}":${error}`)
+        domShowFu('#AsyncSpinLoding', false)
+        // 可选:即使某个文件失败,也继续处理其他文件
+        // 这里可以选择将错误信息记录到ZIP中的一个文本文件里
+      })
+
+    promises.push(promise)
+  })
+
+  try {
+    // 等待所有文件下载完成
+    await Promise.all(promises)
+
+    // 生成ZIP文件
+    const zipBlob = await zip.generateAsync({
+      type: 'blob',
+      compression: 'DEFLATE', // 使用DEFLATE算法进行压缩
+      compressionOptions: { level: 6 } // 压缩级别,1-9,6是较好的平衡点
+    })
+
+    // 使用FileSaver触发下载
+    saveAs(zipBlob, zipName)
+    MessageFu.success('压缩包下载成功')
+    back()
+    domShowFu('#AsyncSpinLoding', false)
+  } catch (error) {
+    MessageFu.error(`生成或下载压缩包过程中出错:${error}`)
+    domShowFu('#AsyncSpinLoding', false)
+  }
+}

+ 52 - 0
src/pages/B2library/index.module.scss

@@ -1,4 +1,56 @@
 .B2library {
+  background-color: #fff;
+  border-radius: 10px;
+  padding: 20px;
+  position: relative;
   :global {
+    .B2top {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 20px;
+    }
+    .ant-table-cell {
+      padding: 8px !important;
+    }
+    .ant-checkbox-inner {
+      width: 24px !important;
+      height: 24px !important;
+    }
+    .B2lookVideo {
+      display: flex;
+      justify-content: center;
+      position: relative;
+
+      .B2lookBox {
+        cursor: pointer;
+        transition: opacity 0.3s;
+        opacity: 0;
+        z-index: 10;
+        position: absolute;
+        top: 0;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 60px;
+        height: 60px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        font-size: 18px;
+        color: #fff;
+        background-color: rgba(0, 0, 0, 0.6);
+
+        & > div {
+          font-size: 14px;
+        }
+        &:hover {
+          opacity: 1;
+        }
+      }
+      video {
+        width: 60px;
+        height: 60px;
+        object-fit: cover;
+      }
+    }
   }
 }

+ 234 - 2
src/pages/B2library/index.tsx

@@ -1,9 +1,241 @@
-import React from 'react'
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
 import styles from './index.module.scss'
+import { useDispatch, useSelector } from 'react-redux'
+import { fromDataBase } from '../A2video/data'
+import { B2_APIdel, B2_APIgetList } from '@/store/action/B2library'
+import store, { RootState } from '@/store'
+import { MessageFu } from '@/utils/message'
+import { FileListType } from '@/components/ZupTypes'
+import { Button, Checkbox, Input, Select } from 'antd'
+import { baseURL } from '@/utils/http'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import { B2typeSelectType, downloadFilesAsZip } from './data'
+import MyTable from '@/components/MyTable'
+import { B2tableC } from '@/utils/tableData'
+import ImageLazy from '@/components/ImageLazy'
+import { EyeOutlined } from '@ant-design/icons'
+
 function B2library() {
+  const dispatch = useDispatch()
+
+  // 顶部筛选
+  const [fromData, setFromData] = useState(fromDataBase)
+
+  // 封装发送请求的函数
+  const getListFu = useCallback(async () => {
+    dispatch(B2_APIgetList(fromData))
+  }, [dispatch, fromData])
+
+  useEffect(() => {
+    getListFu()
+  }, [getListFu])
+
+  const timeRef = useRef(-1)
+  // 输入框的输入
+  const txtChangeFu = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>, key: 'searchKey') => {
+      clearTimeout(timeRef.current)
+      timeRef.current = window.setTimeout(() => {
+        setFromData({
+          ...fromData,
+          [key]: e.target.value,
+          pageNum: 1
+        })
+      }, 500)
+    },
+    [fromData]
+  )
+  // 点击重置
+  const [inputKey, setInputKey] = useState(1)
+  const resetSelectFu = useCallback(() => {
+    setInputKey(Date.now())
+    setFromData({ ...fromDataBase })
+  }, [])
+
+  // 从仓库中获取表格数据
+  const tableInfo = useSelector((state: RootState) => state.B2library.tableInfo)
+
+  const [checkArr, setCheckArr] = useState<FileListType[]>([])
+
+  const checkFlag = useMemo(() => {
+    return tableInfo.list.length <= checkArr.length
+  }, [checkArr.length, tableInfo.list.length])
+
+  // 全选和反选
+  const onChangeAll = useCallback(() => {
+    if (checkFlag) setCheckArr([])
+    else setCheckArr([...tableInfo.list])
+  }, [checkFlag, tableInfo.list])
+
+  // 单个选择
+  const checkOneFu = useCallback(
+    (item: FileListType) => {
+      if (checkArr.map(v => v._id).includes(item._id))
+        setCheckArr(checkArr.filter(v => v._id !== item._id))
+      else setCheckArr([...checkArr, item])
+    },
+    [checkArr]
+  )
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: string) => {
+      const res: any = await B2_APIdel(id)
+      if (res.code === 0) {
+        MessageFu.success('删除成功')
+        if (checkArr.map(v => v._id).includes(id)) setCheckArr(checkArr.filter(v => v._id !== id))
+        getListFu()
+      }
+    },
+    [checkArr, getListFu]
+  )
+
+  const tableLastBtn = useMemo(() => {
+    return [
+      {
+        title: '查看',
+        render: (item: FileListType) => {
+          if (item.type === 'img') {
+            return (
+              <div className='tableImgAuto'>
+                <ImageLazy
+                  width={60}
+                  height={60}
+                  src={item.compressedUrl || item.originalUrl}
+                  srcBig={item.originalUrl || item.compressedUrl}
+                />
+              </div>
+            )
+          } else if (item.type === 'video') {
+            return (
+              <div className='B2lookVideo'>
+                <div
+                  className='B2lookBox'
+                  onClick={() =>
+                    store.dispatch({
+                      type: 'layout/lookDom',
+                      payload: { src: item.originalUrl, type: 'video' }
+                    })
+                  }
+                >
+                  <EyeOutlined rev={undefined} />
+                  &nbsp;
+                  <div>预览</div>
+                </div>
+                <video src={baseURL + item.originalUrl} />
+              </div>
+            )
+          } else {
+            const lastNameArr = item.originalName.split('.')
+            let laseName = lastNameArr[lastNameArr.length - 1]
+            laseName = laseName.toLocaleLowerCase()
+            if (laseName === 'pdf') {
+              return (
+                <Button
+                  size='small'
+                  type='text'
+                  onClick={() => {
+                    window.open(baseURL + item.originalUrl)
+                  }}
+                >
+                  查看
+                </Button>
+              )
+            } else return <div className='tableImgAuto'>-</div>
+          }
+        }
+      },
+      {
+        title: (
+          <div>
+            <Checkbox checked={checkFlag} onChange={() => onChangeAll()}>
+              {checkFlag ? '反选' : '全选'}
+            </Checkbox>
+          </div>
+        ),
+        render: (item: FileListType) => (
+          <Checkbox
+            checked={checkArr.map(v => v._id).includes(item._id)}
+            onChange={() => checkOneFu(item)}
+          >
+            <span style={{ opacity: 0 }}>选择</span>{' '}
+          </Checkbox>
+        )
+      },
+      {
+        title: '操作',
+        render: (item: FileListType) => (
+          <>
+            <Button size='small' type='text'>
+              <a href={baseURL + item.originalUrl} download target='_blank' rel='noreferrer'>
+                下载
+              </a>
+            </Button>
+            <MyPopconfirm txtK='删除' onConfirm={() => delTableFu(item._id)} />
+          </>
+        )
+      }
+    ]
+  }, [checkArr, checkFlag, checkOneFu, delTableFu, onChangeAll])
+
   return (
     <div className={styles.B2library}>
-      <h1>B2library</h1>
+      <div className='pageTitle'>素材库</div>
+
+      <div className='B2top'>
+        <div className='B2topll'>
+          <Input
+            key={inputKey}
+            maxLength={30}
+            showCount
+            style={{ width: 300 }}
+            placeholder='请输入标题'
+            allowClear
+            onChange={e => txtChangeFu(e, 'searchKey')}
+          />
+          &emsp;
+          <Select
+            allowClear
+            style={{ width: 150 }}
+            placeholder='类别'
+            options={B2typeSelectType}
+            value={fromData.type || null}
+            onChange={e => setFromData({ ...fromData, type: e, pageNum: 1 })}
+          />
+        </div>
+        <div className='B2toprr'>
+          <Button onClick={resetSelectFu}>重置</Button>&emsp;
+          <Button
+            type='primary'
+            onClick={() =>
+              downloadFilesAsZip(
+                checkArr.map(v => ({
+                  url: baseURL + v.originalUrl,
+                  name: v.originalName
+                })),
+                'download.zip',
+                () => {
+                  setCheckArr([])
+                }
+              )
+            }
+          >
+            批量下载
+          </Button>
+        </div>
+      </div>
+
+      {/* 表格主体 */}
+      <MyTable
+        yHeight={650}
+        list={tableInfo.list}
+        columnsTemp={B2tableC}
+        lastBtn={tableLastBtn}
+        pageNum={fromData.pageNum}
+        pageSize={fromData.pageSize}
+        total={tableInfo.total}
+        onChange={(pageNum, pageSize) => setFromData({ ...fromData, pageNum, pageSize })}
+      />
     </div>
   )
 }

+ 40 - 0
src/store/action/B1audit.ts

@@ -0,0 +1,40 @@
+import http from '@/utils/http'
+import { AppDispatch } from '..'
+
+/**
+ * 素材审核-获取列表
+ */
+
+export const B1_APIgetList = (data: any): any => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post('audit/getAuditList', data)
+    if (res.code === 0) {
+      const obj = {
+        list: res.data.list,
+        total: res.data.total
+      }
+      dispatch({ type: 'B1/getList', payload: obj })
+    }
+  }
+}
+
+/**
+ * 素材审核-删除数据
+ */
+export const B1_APIdel = (_id: string) => {
+  return http.get(`audit/delAudit/${_id}`)
+}
+
+/**
+ * 素材审核-通过id获取详情
+ */
+export const B1_APIgetInfo = (_id: string) => {
+  return http.get(`audit/getAuditInfo/${_id}`)
+}
+
+/**
+ * 素材审核-审核
+ */
+export const B1_APIaudit = (data: any) => {
+  return http.post(`audit/saveAudit`, data)
+}

+ 33 - 0
src/store/action/B2library.ts

@@ -0,0 +1,33 @@
+import http from '@/utils/http'
+import { AppDispatch } from '..'
+
+/**
+ * 素材库-获取列表
+ */
+
+export const B2_APIgetList = (data: any): any => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post('audit/getLibraryList', data)
+    if (res.code === 0) {
+      const obj = {
+        list: res.data.list,
+        total: res.data.total
+      }
+      dispatch({ type: 'B2/getList', payload: obj })
+    }
+  }
+}
+
+/**
+ * 素材库-删除数据
+ */
+export const B2_APIdel = (_id: string) => {
+  return http.get(`audit/delLibrary/${_id}`)
+}
+
+/**
+ * 素材库-批量下载
+ */
+export const B2_APIdown = (_ids: string[]) => {
+  return http.post(`audit/libraryDown`, { _ids })
+}

+ 28 - 0
src/store/reducer/B1audit.ts

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

+ 28 - 0
src/store/reducer/B2library.ts

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

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

@@ -7,6 +7,8 @@ import A2video from './A2video'
 import A3goods from './A3goods'
 import A4advanced from './A4advanced'
 import A5share from './A5share'
+import B1audit from './B1audit'
+import B2library from './B2library'
 import Z1user from './Z1user'
 import Z2log from './Z2log'
 
@@ -17,6 +19,8 @@ const rootReducer = combineReducers({
   A3goods,
   A4advanced,
   A5share,
+  B1audit,
+  B2library,
   Z1user,
   Z2log
 })

+ 29 - 0
src/utils/tableData.ts

@@ -14,6 +14,8 @@
 //     ["text", "创建日期",'description', 50,A],
 //   ];
 
+import { b1BtypeObj } from '@/pages/B1audit/data'
+
 export const A2tableFuC = (val: string) => {
   return [
     ['txt', `${val}标题`, 'name'],
@@ -29,9 +31,36 @@ export const A4tableC = [
   ['img', '封面', 'coverSmall'],
   ['txt', `类别`, 'type'],
   ['txt', '发布日期', 'releaseDate'],
+  ['text', '简介', 'intro', 50],
+  ['txt', '排序值', 'sort']
+]
+
+export const A5tableC = [
+  ['txt', `标题`, 'name'],
+  ['img', '封面', 'coverSmall'],
+  ['txt', `类别`, 'type'],
+  ['txt', '发布日期', 'releaseDate'],
   ['txt', '排序值', 'sort']
 ]
 
+export const B1tableC = [
+  ['txt', `标题`, 'title'],
+  ['txt', '联系方式', 'phone'],
+  ['text', '概述', 'description', 50],
+  ['length', `文件数量`, 'fileIds'],
+  ['txt', '上传时间', 'createTime'],
+  ['txtChange', '状态', 'state', { true: '已审核', false: '未审核' }]
+]
+
+export const B2tableC = [
+  ['txt', `标题`, 'title'],
+  ['txt', '联系方式', 'phone'],
+  ['txtChange', `类别`, 'type', b1BtypeObj],
+  ['txt', `文件名`, 'originalName'],
+  ['text', '备注', 'remark', 50],
+  ['txt', '审核人', 'operator']
+]
+
 export const Z1tableC = [
   ['txt', '用户名', 'userName'],
   ['txtChange', '角色', 'isAdmin', { 0: '普通成员', 1: '管理员' }],

+ 28 - 1
yarn.lock

@@ -5879,6 +5879,11 @@ ignore@^5.2.0:
   resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
   integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
 
+immediate@~3.0.5:
+  version "3.0.6"
+  resolved "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+  integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
+
 immer@^9.0.7:
   version "9.0.21"
   resolved "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
@@ -7028,6 +7033,16 @@ jsonpointer@^5.0.0:
     object.assign "^4.1.4"
     object.values "^1.1.6"
 
+jszip@^3.10.1:
+  version "3.10.1"
+  resolved "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
+  integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
+  dependencies:
+    lie "~3.3.0"
+    pako "~1.0.2"
+    readable-stream "~2.3.6"
+    setimmediate "^1.0.5"
+
 keyv@^4.5.3:
   version "4.5.4"
   resolved "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -7091,6 +7106,13 @@ levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+lie@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
+  integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
+  dependencies:
+    immediate "~3.0.5"
+
 lilconfig@^2.0.3, lilconfig@^2.1.0:
   version "2.1.0"
   resolved "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
@@ -7712,6 +7734,11 @@ p-try@^2.0.0:
   resolved "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
+pako@~1.0.2:
+  version "1.0.11"
+  resolved "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
 param-case@^3.0.4:
   version "3.0.4"
   resolved "https://registry.npmmirror.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
@@ -9141,7 +9168,7 @@ read-cache@^1.0.0:
   dependencies:
     pify "^2.3.0"
 
-readable-stream@^2.0.1:
+readable-stream@^2.0.1, readable-stream@~2.3.6:
   version "2.3.8"
   resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
   integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==