wjx há 7 meses atrás
pai
commit
f5d32237b2

+ 0 - 2
src/components/HeaderDropdown/index.tsx

@@ -4,11 +4,9 @@ import React from 'react';
 import classNames from 'classnames';
 import styles from './index.less';
 
-declare type OverlayFunc = () => React.ReactNode;
 
 export interface HeaderDropdownProps extends Omit<DropDownProps, 'overlay'> {
   overlayClassName?: string;
-  overlay: React.ReactNode | OverlayFunc | any;
   placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
 }
 

+ 1 - 8
src/components/RightContent/AvatarDropdown.tsx

@@ -136,13 +136,6 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
   items.push({ label: '修改密码', key: 'edit', icon: <EditOutlined /> })
   items.push({ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> })
 
-  const menuHeaderDropdown = (
-    <Menu
-      items={items}
-      className={styles.menu} selectedKeys={[]} onClick={onMenuClick}
-    />
-  );
-
   // 切换公司
   const setCompanyHandle = (companyId: number) => {
     selectCompany.run(companyId).then((res: any) => {
@@ -153,7 +146,7 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
   }
   return (
     <>
-      <HeaderDropdown overlay={menuHeaderDropdown}>
+      <HeaderDropdown menu={{ items, onClick: onMenuClick }}>
         <span className={`${styles.action} ${styles.account}`} onClick={() => {
           let AdminToken: any = sessionStorage.getItem('Admin-Token')
           copy(AdminToken)

+ 2 - 1
src/pages/launchSystemV3/adMonitorListV3/adPlanList.tsx

@@ -355,7 +355,6 @@ const AdPlanList: React.FC<{ userId: string }> = (props) => {
                         <Table.Summary.Row className='s_summary'>
                             {tableField.map((item, index) => {
                                 let data = totalData[item.dataIndex]
-                                let value = (data === 0 || data) ? countDecimals(data) > 2 ? data.toFixed(2) : data : '--'
                                 if (item.dataIndex === 'zj') {
                                     return <Table.Summary.Cell index={index} key={item.dataIndex} align="center"><strong>总计</strong></Table.Summary.Cell>
                                 } else if (['ctr_total', 'mp_follow_rate_total',
@@ -365,6 +364,7 @@ const AdPlanList: React.FC<{ userId: string }> = (props) => {
                                     'income_roi124h_pla_total', 'income_roi124h_total', 'income_roi1_total', 'income_roi3_total', 'income_roi7_total', 'income_roi14_total', 'ad_monetization_roi_total',
                                     'mini_game_income_roi1_total', 'minigame3d_income_roi_total', 'minigame7d_income_roi_total', 'mini_game_ad_monetization_roi_total'
                                 ].includes(item.dataIndex)) {
+                                    let value = (data === 0 || data) ? data : '--'
                                     return <Table.Summary.Cell index={index} key={item.dataIndex} align="center">
                                         <Space size={4}>
                                             <strong>
@@ -374,6 +374,7 @@ const AdPlanList: React.FC<{ userId: string }> = (props) => {
                                         </Space>
                                     </Table.Summary.Cell>
                                 } else {
+                                    let value = (data === 0 || data) ? countDecimals(data) > 2 ? Math.floor(data * 100) / 100 : data : '--'
                                     return <Table.Summary.Cell index={index} key={item.dataIndex} align="center">
                                         <Space size={4}>
                                             <strong><Statistic value={value} /></strong>

+ 1 - 1
src/pages/launchSystemV3/adMonitorListV3/tablePlanListConfig.tsx

@@ -409,7 +409,7 @@ function tablePlanConfig(
             width: 110,
             sorter: true,
             render: (a: any, b: any) => {
-                return <StatisticNull data={b} field='cpc_total' />
+                return <StatisticNull data={b} precision={2} field='cpc_total' />
             }
         },
         {

+ 2 - 1
src/pages/launchSystemV3/material/cloudNew/folder.tsx

@@ -18,7 +18,7 @@ interface Props {
 const Folder = forwardRef(({ loading, onLoadData }: Props, ref: Ref<FolderRef>) => {
 
     /******************************/
-    const { treeData, setSelectedKeys, selectedKeys, setBreadcrumdData, findParentKeys, loadedKeys, setBatchFolderVisible, handleUpdateFolder, expandedKeys, setExpandedKeys } = useContext(DispatchCloudNew)!;
+    const { treeData, setSelectedKeys, selectedKeys, setMaterialParams, setBreadcrumdData, findParentKeys, loadedKeys, setBatchFolderVisible, handleUpdateFolder, expandedKeys, setExpandedKeys } = useContext(DispatchCloudNew)!;
 
     /******************************/
 
@@ -44,6 +44,7 @@ const Folder = forwardRef(({ loading, onLoadData }: Props, ref: Ref<FolderRef>)
             handleUpdateFolder(keys[0] as string)
         setBatchFolderVisible(false)
         setSelectedKeys(keys);
+        setMaterialParams(data => ({ ...data, pageNum: 1 }))
     };
 
     const onExpand = (expandedKeysValue: React.Key[]) => {

+ 59 - 5
src/pages/launchSystemV3/material/cloudNew/index.less

@@ -1,7 +1,18 @@
-.cloudNew {
+.cloudNew_layout {
     display: flex;
     min-width: 1200px;
     height: calc(100vh - 98px);
+    flex-direction: column;
+    gap: 6px;
+}
+
+.cloudNew_search {}
+
+.cloudNew {
+    display: flex;
+    flex: 1 0;
+    overflow: hidden;
+    height: 100%;
 
     .folder {
         width: 238px;
@@ -76,9 +87,14 @@
         }
 
         .content_col {
-            // min-width: 240px;
-            // max-width: 300px;
-            // width: 20%;
+            user-select: none;
+            /* 对大多数浏览器有效 */
+            -webkit-user-select: none;
+            /* Safari */
+            -moz-user-select: none;
+            /* Firefox */
+            -ms-user-select: none;
+            /* Internet Explorer/Edge */
         }
 
         .content_cover {
@@ -86,14 +102,52 @@
             background-color: #ececec;
             border-top-left-radius: 6px;
             border-top-right-radius: 6px;
-            text-align: center;
             position: relative;
+            display: flex;
+            justify-content: center;
+            align-items: center;
 
             .checkbox {
                 position: absolute;
                 top: 6px;
                 left: 10px;
             }
+
+            .coverImg {
+                max-width: 100%;
+                max-height: 100%;
+            }
+
+            .file_info {
+                position: absolute;
+                bottom: 5px;
+                padding: 0 10px;
+                display: flex;
+                justify-content: space-between;
+                width: 100%;
+
+                >div {
+                    background-color: rgba(0, 0, 0, 0.75);
+                    padding: 0 4px;
+                    border-radius: 4px;
+                    color: #fff;
+                    font-size: 12px;
+                }
+            }
+
+            .playr {
+                position: absolute;
+                width: 35px;
+                height: 35px;
+                top: 50%;
+                left: 50%;
+                transform: translate(-50%, -50%);
+
+                >img {
+                    width: 100%;
+                    height: 100%;
+                }
+            }
         }
 
         .body {

+ 45 - 32
src/pages/launchSystemV3/material/cloudNew/index.tsx

@@ -10,6 +10,7 @@ import { useAjax } from "@/Hook/useAjax";
 import ManageFolder from "./manageFolder";
 import { ExclamationCircleOutlined, FolderOpenOutlined, FolderOutlined } from "@ant-design/icons";
 import { updateTreeData } from "./const";
+import Search from "./search";
 
 export const DispatchCloudNew = React.createContext<CLOUDNEW.CloudNewReactContent | null>(null);
 
@@ -33,6 +34,8 @@ const CloudNew: React.FC = () => {
     const [folderCreateBy, setFolderCreateBy] = useState<number>()     // 获取最上层文件夹创建人
     const [batchFolderVisible, setBatchFolderVisible] = useState<boolean>(false) // 批量操作文件夹控制
     const [handleType, setHandleType] = useState<'folder' | 'file'>('file') // 操作类型
+    const [searchParams, setSearchParams] = useState<{ searchType: 'file' | 'folder', keyword?: string }>({ searchType: 'file' })
+    const [materialParams, setMaterialParams] = useState<CLOUDNEW.GetMaterialListProps>({ pageNum: 1, pageSize: 30 })
 
     const getFolderList = useAjax((params) => getFolderListApi(params))
     const delFolderAjax = useAjax((params) => delFolderApi(params))
@@ -132,13 +135,8 @@ const CloudNew: React.FC = () => {
         });
     }
 
-    return <Card
-        style={{ height: '100%' }}
-        bodyStyle={{ padding: 0, overflow: 'auto hidden', height: '100%' }}
-        className="cardResetCss buttonResetCss"
-        bordered
-    >
-        <div className={style.cloudNew}>
+    return <>
+        <div className={style.cloudNew_layout}>
             <DispatchCloudNew.Provider value={{
                 treeData, setTreeData,
                 selectedKeys, setSelectedKeys,
@@ -149,31 +147,46 @@ const CloudNew: React.FC = () => {
                 expandedKeys, setExpandedKeys,
                 folderCreateBy, setFolderCreateBy,
                 batchFolderVisible, setBatchFolderVisible,
-                handleType, setHandleType
+                handleType, setHandleType,
+                searchParams, setSearchParams,
+                materialParams, setMaterialParams
             }}>
-                {/* 文件夹 */}
-                <Folder
-                    loading={folderLoading}
-                    onLoadData={(treeNode: EventDataNode<DataNode>) => {
-                        return new Promise<void>(async (resolve) => {
-                            console.log('Trigger Select', treeNode);
-                            await handleUpdateFolder(treeNode.key as string)
-                            resolve()
-                        })
-                    }}
-                />
-                {/* 素材列表 */}
-                <Material
-                    onAddFolder={() => {
-                        setFolderVisible(true)
-                    }}
-                    onUpdateFolder={(data) => {
-                        setInitialValuesFolder(data)
-                        setFolderVisible(true)
-                    }}
-                    onDelFolder={delFolder}
-                    ref={refMaterial}
+                {/* 搜索 */}
+                <Search
+                    onSearch={(value) => (refMaterial.current as any)?.search(value)}
                 />
+                <Card
+                    style={{ height: '100%', flex: '1 0', overflow: 'hidden' }}
+                    bodyStyle={{ padding: 0, overflow: 'auto hidden', height: '100%' }}
+                    className="cardResetCss buttonResetCss"
+                    bordered
+                >
+                    <div className={style.cloudNew}>
+                        {/* 文件夹 */}
+                        <Folder
+                            loading={folderLoading}
+                            onLoadData={(treeNode: EventDataNode<DataNode>) => {
+                                return new Promise<void>(async (resolve) => {
+                                    console.log('Trigger Select', treeNode);
+                                    await handleUpdateFolder(treeNode.key as string)
+                                    resolve()
+                                })
+                            }}
+                        />
+                        {/* 素材列表 */}
+                        <Material
+                            onAddFolder={() => {
+                                setFolderVisible(true)
+                            }}
+                            onUpdateFolder={(data) => {
+                                setInitialValuesFolder(data)
+                                setFolderVisible(true)
+                            }}
+                            onDelFolder={delFolder}
+                            ref={refMaterial}
+                        />
+                    </div>
+                </Card>
             </DispatchCloudNew.Provider>
         </div>
 
@@ -198,8 +211,8 @@ const CloudNew: React.FC = () => {
                     }
                     setInitialValuesFolder(undefined)
                 } else {
+                    (refMaterial.current as any).folderRefresh()
                     if (selectedKeys?.[0]) {
-                        (refMaterial.current as any).folderRefresh()
                         handleUpdateFolder(selectedKeys[0] as string)
                     } else {
                         // handleGetFolder()
@@ -208,7 +221,7 @@ const CloudNew: React.FC = () => {
                 }
             }}
         />}
-    </Card>
+    </>
 }
 
 export default CloudNew

+ 211 - 36
src/pages/launchSystemV3/material/cloudNew/material.tsx

@@ -1,8 +1,8 @@
 import React, { forwardRef, Ref, useContext, useEffect, useImperativeHandle, useRef, useState } from "react"
 import style from './index.less'
-import { Breadcrumb, Button, Card, Checkbox, Dropdown, Pagination, Radio, Spin, Typography } from "antd"
+import { Breadcrumb, Button, Card, Checkbox, Dropdown, message, Modal, Pagination, Radio, Spin, Typography } from "antd"
 import { DispatchCloudNew } from "."
-import { getFolderListApi } from "@/services/adqV3/cloudNew"
+import { delMaterialApi, getFolderListApi, getMaterialListApi } from "@/services/adqV3/cloudNew"
 import { useAjax } from "@/Hook/useAjax"
 import { useSize } from "ahooks"
 import './global.less'
@@ -11,6 +11,10 @@ import { CheckboxValueType } from "antd/lib/checkbox/Group"
 import MoveFile from "./moveFile"
 import { ItemType } from "antd/lib/menu/hooks/useItems"
 import UploadFile from "./uploadFile"
+import UpdateCreate from "./updateCreate"
+import { formatSecondsToTime, getVideoImgUrl } from "@/utils/utils"
+import { ExclamationCircleOutlined } from "@ant-design/icons"
+import UpdateFile from "./updateFile"
 const { Text } = Typography;
 
 interface Props {
@@ -22,6 +26,10 @@ interface Props {
 
 interface MaterialRef {
     folderRefresh: () => void
+    search: (value: {
+        searchType: "file" | "folder";
+        keyword?: string;
+    }) => void
 }
 /**
  * 素材列表
@@ -31,7 +39,7 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
 
     /********************************/
     const { initialState } = useModel('@@initialState');
-    const { breadcrumdData, setSelectedKeys, setBreadcrumdData, selectedKeys, treeData, folderCreateBy, findParentKeys, loadedKeys, handleUpdateFolder, batchFolderVisible, setBatchFolderVisible, handleType, setHandleType } = useContext(DispatchCloudNew)!;
+    const { breadcrumdData, setSelectedKeys, setBreadcrumdData, setSearchParams, setMaterialParams, materialParams, searchParams, selectedKeys, treeData, folderCreateBy, findParentKeys, loadedKeys, handleUpdateFolder, batchFolderVisible, setBatchFolderVisible, handleType, setHandleType } = useContext(DispatchCloudNew)!;
     const ref = useRef<HTMLDivElement>(null);
     const size = useSize(ref);
     const [folderList, setFolderList] = useState<{ id: number, folderName: string, createBy: number, description?: string }[]>([])
@@ -41,16 +49,29 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
     const [indeterminateFolder, setIndeterminateFolder] = useState<boolean>(false);
     const [moveVisible, setMoveVisible] = useState<boolean>(false)
     const [moveType, setMoveType] = useState<'folder' | 'file'>('folder')
+    const [batchType, setBatchType] = useState<'folder' | 'file'>()
     const [checkedList, setCheckedList] = useState<CheckboxValueType[]>([])
     const [uploadVisible, setUploadVisible] = useState<boolean>(false)
+    const [updateOwnerData, setUpdateOwnerData] = useState<{ visible?: boolean, folderId: number }>({ folderId: 0 })
+    const [updateFileData, setUpdateFileData] = useState<{ visible?: boolean, initialValues: any }>({ visible: false, initialValues: {} })
 
     const getFolderList = useAjax((params) => getFolderListApi(params))
+    const delMaterial = useAjax((params) => delMaterialApi(params))
+    const getMaterialList = useAjax((params) => getMaterialListApi(params))
     /********************************/
 
     useImperativeHandle(ref1, () => ({
         // 刷新文件夹
         folderRefresh() {
             getFolder()
+        },
+        search(value) {
+            setSearchParams(value)
+            if (value.searchType === 'folder') {
+                getFolder(value.keyword)
+            } else {
+                setMaterialParams(data => ({ ...data, materialName: value.keyword, pageNum: 1 }))
+            }
         }
     }));
 
@@ -63,32 +84,51 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
     }, [size?.width])
 
     useEffect(() => {
-        if (isShowFolder()) {
+        cancelSelect()
+        if (isShowFolder() && !(searchParams.keyword && searchParams.searchType)) {
             // 文件夹
             getFolder()
         } else {
             // 文件
+            getFile()
         }
-    }, [selectedKeys, handleType])
+    }, [selectedKeys, handleType, materialParams, searchParams])
 
     /** 获取下级文件夹 */
-    const getFolder = () => {
+    const getFolder = (folderName?: string) => {
         let parentId: number | undefined;
         if (selectedKeys?.length) {
             const parentIdArr = (selectedKeys[0] as string).split('-')
             const parentIdArrLength = parentIdArr.length
             parentId = Number(parentIdArr[parentIdArrLength - 1])
         }
-        getFolderList.run({ parentId }).then(res => {
+        getFolderList.run({ parentId, folderName }).then(res => {
             setFolderList(() => res || [])
         })
     }
 
+    // 搜索素材
+    const getFile = () => {
+        let parentId: number | undefined;
+        if (selectedKeys?.length) {
+            const parentIdArr = (selectedKeys[0] as string).split('-')
+            const parentIdArrLength = parentIdArr.length
+            parentId = Number(parentIdArr[parentIdArrLength - 1])
+        }
+        getMaterialList.run({ ...materialParams, folderId: parentId })
+    }
+
     // 文件夹选择
     const onCheckboxChange = (checkedValues: CheckboxValueType[]) => {
+        let data: any[] = []
+        if (batchType === 'folder') {
+            data = folderList
+        } else {
+            data = getMaterialList?.data?.records || []
+        }
         setCheckedFolderList(checkedValues)
-        setIndeterminateFolder(!!checkedValues.length && checkedValues.length < folderList.length);
-        setCheckFolderAll(checkedValues.length === folderList.length);
+        setIndeterminateFolder(!!checkedValues.length && checkedValues.length < data.length);
+        setCheckFolderAll(checkedValues.length === data.length);
     };
 
     // 取消选择
@@ -109,6 +149,7 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
         return initialState?.currentUser?.userId?.toString() === createBy.toString()
     }
 
+    // 文件夹更多
     const getItems = (item: any) => {
         let data: ItemType[] = []
         if (isPermission(item.createBy)) {
@@ -124,7 +165,7 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
             } else if (isAdmin()) {
                 data.push({
                     label: '修改文件夹所属人', style: { fontSize: 12 }, key: 'createBy', onClick: () => {
-
+                        setUpdateOwnerData({ visible: true, folderId: item.id })
                     }
                 })
             }
@@ -133,10 +174,52 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
             if (isAdmin() && !selectedKeys?.[0]) {
                 data.push({
                     label: '修改文件夹所属人', style: { fontSize: 12 }, key: 'createBy', onClick: () => {
+                        setUpdateOwnerData({ visible: true, folderId: item.id })
+                    }
+                })
+            }
+        }
+        return data
+    }
+
+    // 删除素材
+    const delFile = (id: number, name: string) => {
+        Modal.confirm({
+            title: <strong>{`删除素材“${name}”`}</strong>,
+            icon: <ExclamationCircleOutlined />,
+            content: '是否确定删除该素材',
+            okText: '确认',
+            cancelText: '取消',
+            className: 'modalResetCss',
+            onOk: () => {
+                return new Promise((resolve: (value: unknown) => void) => {
+                    delMaterial.run(id).then(res => {
+                        if (res) {
+                            message.success('删除成功');
+                            getMaterialList.refresh()
+                            resolve('')
+                        }
+                    })
+                })
+            }
+        });
+    }
 
+    // 操作素材 更多
+    const getItemsFile = (item: any) => {
+        let data: ItemType[] = []
+        if (isPermission(item.createBy)) {
+            data.push({ label: '编辑', style: { fontSize: 12 }, key: 'edit', onClick: () => setUpdateFileData({ visible: true, initialValues: item }) })
+            if (!!selectedKeys?.[0]) {
+                data.push({
+                    label: '移动', style: { fontSize: 12 }, key: 'move', onClick: () => {
+                        setCheckedList([item.id])
+                        setMoveType('file')
+                        setMoveVisible(true)
                     }
                 })
             }
+            data.push({ label: <span style={{ color: 'red', fontSize: 12 }}>删除</span>, key: 'del', onClick: () => delFile(item.id, item?.materialName) })
         }
         return data
     }
@@ -152,36 +235,57 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
                 {!!selectedKeys?.[0] && <Checkbox
                     onChange={(e) => {
                         setHandleType(e.target.checked ? 'folder' : 'file')
+                        setBatchFolderVisible(false)
+                        cancelSelect()
                     }}
                     checked={handleType === 'folder'}
                 ><span style={{ fontSize: 12 }}>是否操作文件夹</span></Checkbox>}
                 {(!selectedKeys?.[0] || handleType === 'folder') && <Button onClick={() => onAddFolder?.()} disabled={folderCreateBy ? !isPermission(folderCreateBy) : false}>新建文件夹</Button>}
+                {!selectedKeys?.[0] && <>
+                    <Button type="primary" disabled={folderCreateBy ? !isPermission(folderCreateBy) : false} onClick={() => setUploadVisible(true)}>上传素材</Button>
+                    {(searchParams.keyword && searchParams.searchType) && <Button onClick={() => { setBatchFolderVisible(true); setBatchType('file') }} disabled={folderCreateBy ? !isPermission(folderCreateBy) : false}>批量操作素材</Button>}
+                </>}
                 {handleType === 'folder' ? <>
-                    {!!selectedKeys?.[0] && <Button onClick={() => setBatchFolderVisible(true)} disabled={folderCreateBy ? !isPermission(folderCreateBy) : false}>批量操作文件夹</Button>}
+                    {!!selectedKeys?.[0] && <Button onClick={() => { setBatchFolderVisible(true); setBatchType('folder') }} disabled={folderCreateBy ? !isPermission(folderCreateBy) : false}>批量操作文件夹</Button>}
                 </> : <>
-                    {!!selectedKeys?.[0] && <Button type="primary" disabled={folderCreateBy ? !isPermission(folderCreateBy) : false} onClick={() => setUploadVisible(true)}>上传素材</Button>}
+                    {!!selectedKeys?.[0] && <>
+                        <Button type="primary" disabled={folderCreateBy ? !isPermission(folderCreateBy) : false} onClick={() => setUploadVisible(true)}>上传素材</Button>
+                        <Button onClick={() => { setBatchFolderVisible(true); setBatchType('file') }} disabled={folderCreateBy ? !isPermission(folderCreateBy) : false}>批量操作素材</Button>
+                    </>}
                 </>}
             </div>
         </div>
         {batchFolderVisible ? <div className={style.operates}>
             <div className={style.left_bts}>
-                <Checkbox onChange={(e) => {
-                    setCheckedFolderList(e.target.checked ? folderList.map(item => item.id) : [])
-                    setIndeterminateFolder(false)
-                    setCheckFolderAll(e.target.checked)
-                }} indeterminate={indeterminateFolder} checked={checkFolderAll}>全选</Checkbox>
-                <span style={{ color: '#1890FF' }}>已选{checkedFolderList?.length || 0}个文件夹</span>
+                <Checkbox
+                    onChange={(e) => {
+                        setCheckedFolderList(
+                            e.target.checked ? batchType === 'folder' ?
+                                folderList.map(item => item.id) :
+                                getMaterialList?.data?.records?.filter((item: { createBy: any }) => isPermission(item.createBy)).map((item: { id: any }) => item.id) :
+                                []
+                        )
+                        setIndeterminateFolder(false)
+                        setCheckFolderAll(e.target.checked)
+                    }}
+                    disabled={batchType === 'folder' ? false : !getMaterialList?.data?.records?.some((item: { createBy: any }) => isPermission(item.createBy))}
+                    indeterminate={indeterminateFolder}
+                    checked={checkFolderAll}
+                >全选</Checkbox>
+                <span style={{ color: '#1890FF' }}>已选{checkedFolderList?.length || 0}个{batchType === 'file' ? '素材' : '文件夹'}</span>
             </div>
             <div className={style.left_bts}>
                 <Button disabled={checkedFolderList?.length === 0} onClick={() => {
                     setCheckedList(checkedFolderList)
                     setMoveVisible(true)
-                    setMoveType('folder')
-                }}>移动文件夹</Button>
+                    setMoveType(batchType as any)
+                }}>{batchType === 'folder' ? '移动文件夹' : '移动素材'}</Button>
                 <Button type="primary" onClick={() => {
                     setBatchFolderVisible(false)
                 }}>完成</Button>
             </div>
+        </div> : (searchParams.keyword && searchParams.searchType) ? <div className={style.operates}>
+            <span>搜索「{searchParams.searchType === 'file' ? '素材' : '文件夹'}」:{searchParams.keyword}</span>
         </div> : <div className={style.operates}>
             <Breadcrumb>
                 {breadcrumdData.map((item, index) => <Breadcrumb.Item key={item.key}>
@@ -196,17 +300,17 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
             </Breadcrumb>
         </div>}
         <div className={`${style.content} content_global`}>
-            <Spin spinning={getFolderList.loading}>
+            <Spin spinning={getFolderList.loading || getMaterialList.loading}>
                 <div className={style.content_scroll} ref={ref}>
                     <Checkbox.Group value={checkedFolderList} style={{ width: '100%' }} onChange={onCheckboxChange}>
                         <div className={style.content_scroll_div}>
-                            {isShowFolder() ? folderList.map((item, index) => <div key={index} className={style.content_row} style={{ width: rowNum ? (1 / rowNum * 100) + '%' : 200 }}>
+                            {isShowFolder() && !(searchParams.keyword && searchParams.searchType) ? folderList.map((item) => <div key={item.id} className={style.content_row} style={{ width: rowNum ? (1 / rowNum * 100) + '%' : 200 }}>
                                 <Card
                                     hoverable
                                     bodyStyle={{ padding: 0 }}
                                     className={`${style.content_col}`}
-                                    cover={<div style={{ height: 120 }} className={style.content_cover}>
-                                        {batchFolderVisible && <div className={style.checkbox}><Checkbox value={item.id} /></div>}
+                                    cover={<div className={style.content_cover} style={{ height: 120 }}>
+                                        {batchFolderVisible && <div className={style.checkbox}><Checkbox value={item.id} disabled={!isPermission(item.createBy)} /></div>}
                                         <img src={require('../../../../../public/file.png')} height={'100%'} alt="" />
                                     </div>}
                                     onDoubleClick={() => {
@@ -235,19 +339,59 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
                                         </Dropdown> : <a style={{ fontSize: 11 }}>无权限操作</a>}
                                     </div>
                                 </Card>
-                            </div>) : <>
-                                素材
-                            </>}
+                            </div>) : getMaterialList?.data?.records.map((item: any) => <div key={item.id} className={style.content_row} style={{ width: rowNum ? (1 / rowNum * 100) + '%' : 200 }}>
+                                <Card
+                                    hoverable
+                                    bodyStyle={{ padding: 0 }}
+                                    className={`${style.content_col}`}
+                                    cover={<div style={{ height: 120, padding: 0 }} className={style.content_cover}>
+                                        {batchFolderVisible && <div className={style.checkbox}><Checkbox value={item.id} disabled={!isPermission(item.createBy)} /></div>}
+                                        {item.materialType === 'video' && <div className={style.playr}>
+                                            <img src={require('../../../../../public/image/play.png')} alt="" />
+                                        </div>}
+                                        <div className={style.file_info}>
+                                            <div>{item.width}*{item.height}</div>
+                                            {item.materialType === 'video' && <div>{formatSecondsToTime(Math.floor(item.videoDuration))}</div>}
+                                        </div>
+                                        <img src={item.materialType === 'image' ? item.ossUrl : getVideoImgUrl(item.ossUrl)} className={style.coverImg} alt="" />
+                                    </div>}
+                                    onClick={() => {
+
+                                    }}
+                                >
+                                    <div className={style.body}>
+                                        <Text ellipsis>{item?.materialName}</Text>
+                                    </div>
+                                    <div className={style.actions}>
+                                        <div style={{ height: 22 }}></div>
+                                        {isPermission(item.createBy) ? <Dropdown menu={{
+                                            items: getItemsFile(item)
+                                        }}>
+                                            <a onClick={e => e.preventDefault()} style={{ fontSize: 11 }}>更多</a>
+                                        </Dropdown> : <a style={{ fontSize: 11 }}>无权限操作</a>}
+                                    </div>
+                                </Card>
+                            </div>)}
                         </div>
                     </Checkbox.Group>
                 </div>
             </Spin>
         </div>
-        {handleType === 'file' && <div className={style.fotter}>
-            <Pagination size="small" total={50} showSizeChanger showQuickJumper />
+        {((handleType === 'file' && !!selectedKeys?.[0]) || (!selectedKeys?.[0] && searchParams.keyword && searchParams.searchType)) && <div className={style.fotter}>
+            <Pagination
+                size="small"
+                total={getMaterialList?.data?.totalRow || 0}
+                showSizeChanger
+                showQuickJumper
+                pageSize={getMaterialList?.data?.pageSize || 30}
+                current={getMaterialList?.data?.pageNumber || 1}
+                onChange={(page: number, pageSize: number) => {
+                    setMaterialParams({ ...materialParams, pageNum: page, pageSize })
+                }}
+            />
         </div>}
 
-        {/* 移动文件 */}
+        {/* 移动文件 素材 */}
         {moveVisible && <MoveFile
             moveType={moveType}
             checkedList={checkedList}
@@ -260,25 +404,56 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
             onChange={(selectedKey: string) => {
                 getFolder()
                 setMoveVisible(false)
-                cancelSelect()
-                handleUpdateFolder(selectedKeys[0] as string)
-                if (loadedKeys.includes(selectedKey)) {
-                    handleUpdateFolder(selectedKey)
+                if (moveType === 'folder') {
+                    handleUpdateFolder(selectedKeys[0] as string)
+                    if (loadedKeys.includes(selectedKey)) {
+                        handleUpdateFolder(selectedKey)
+                    }
+                } else {
+                    getMaterialList.refresh()
                 }
+                cancelSelect()
                 setBatchFolderVisible(false)
             }}
         />}
-        
+
         {/* 上传文件 */}
         {uploadVisible && <UploadFile
+            folderId={breadcrumdData[breadcrumdData.length - 1].key}
+            userId={(initialState?.currentUser?.userId || 0) as number}
             visible={uploadVisible}
             onChange={() => {
-
+                setUploadVisible(false)
+                getMaterialList.refresh()
             }}
             onClose={() => {
                 setUploadVisible(false)
             }}
         />}
+
+        {/* 修改所属人 */}
+        {updateOwnerData.visible && <UpdateCreate
+            {...updateOwnerData}
+            onClose={() => {
+                setUpdateOwnerData({ visible: false, folderId: 0 })
+            }}
+            onChange={() => {
+                getFolder()
+                setUpdateOwnerData({ visible: false, folderId: 0 })
+            }}
+        />}
+
+        {/* 修改素材 */}
+        {updateFileData?.visible && <UpdateFile
+            {...updateFileData}
+            onClose={() => {
+                setUpdateFileData({ visible: false, initialValues: {} })
+            }}
+            onChange={() => {
+                getMaterialList.refresh()
+                setUpdateFileData({ visible: false, initialValues: {} })
+            }}
+        />}
     </div>
 })
 

+ 13 - 2
src/pages/launchSystemV3/material/cloudNew/moveFile.tsx

@@ -1,5 +1,5 @@
 import { useAjax } from "@/Hook/useAjax"
-import { getFolderListApi, moveFolderApi } from "@/services/adqV3/cloudNew"
+import { getFolderListApi, moveFolderApi, moveMaterialApi } from "@/services/adqV3/cloudNew"
 import { FolderOpenOutlined, FolderOutlined } from "@ant-design/icons"
 import { Card, message, Modal, Spin } from "antd"
 import Tree, { DataNode, EventDataNode } from "antd/lib/tree"
@@ -25,6 +25,7 @@ const MoveFile: React.FC<Props> = ({ moveType, checkedList, visible, userId, onC
 
     const getFolderList = useAjax((params) => getFolderListApi(params))
     const moveFolder = useAjax((params) => moveFolderApi(params))
+    const moveMaterial = useAjax((params) => moveMaterialApi(params))
     /**********************************/
 
     useEffect(() => {
@@ -74,6 +75,16 @@ const MoveFile: React.FC<Props> = ({ moveType, checkedList, visible, userId, onC
                         onChange?.(selectedKeys[0] as string)
                     }
                 })
+            } else {
+                const parentIdArr = (selectedKeys[0] as string).split('-')
+                const parentIdArrLength = parentIdArr.length
+                const parentId = Number(parentIdArr[parentIdArrLength - 1])
+                moveMaterial.run({ folderId: parentId, materialIds: checkedList }).then(res => {
+                    if (res) {
+                        message.success('移动成功')
+                        onChange?.(selectedKeys[0] as string)
+                    }
+                })
             }
         } else {
             message.error('请选择文件夹')
@@ -89,7 +100,7 @@ const MoveFile: React.FC<Props> = ({ moveType, checkedList, visible, userId, onC
         bodyStyle={{ backgroundColor: '#f1f4fc', maxHeight: 650, overflow: 'hidden', overflowY: 'auto', padding: '10px' }}
         maskClosable={false}
         okText="移动"
-        confirmLoading={moveFolder.loading}
+        confirmLoading={moveFolder.loading || moveMaterial.loading}
     >
         <Card className="cardResetCss" title={<strong style={{ fontSize: 12 }}>文件夹列表</strong>}>
             <Spin spinning={folderLoading}>

+ 38 - 0
src/pages/launchSystemV3/material/cloudNew/search.tsx

@@ -0,0 +1,38 @@
+import { Card, Input, Select } from "antd"
+import React, { useContext } from "react"
+import { DispatchCloudNew } from ".";
+
+interface Props {
+    onSearch?: (value: CLOUDNEW.SearchProps) => void
+}
+const Search: React.FC<Props> = ({ onSearch }) => {
+
+    /**********************************/
+    const { searchParams, setSearchParams } = useContext(DispatchCloudNew)!;
+    /**********************************/
+
+
+    return <Card
+        bodyStyle={{ padding: '5px 10px', overflow: 'auto hidden', display: 'flex', gap: 6, flexWrap: 'wrap' }}
+        className="cardResetCss buttonResetCss"
+        bordered
+    >
+        <Input.Group compact>
+            <Select value={searchParams.searchType} onChange={(e) => setSearchParams({ ...searchParams, searchType: e })} style={{ width: 100 }}>
+                {/* <Select.Option value="folder">搜文件夹</Select.Option> */}
+                <Select.Option value="file">搜素材</Select.Option>
+            </Select>
+            <Input.Search
+                enterButton
+                style={{ width: 220 }}
+                onSearch={(value) => {
+                    onSearch?.({ ...searchParams, keyword: value })
+                }}
+                allowClear
+                placeholder="请输入关键词"
+            />
+        </Input.Group>
+    </Card>
+}
+
+export default React.memo(Search)

+ 97 - 0
src/pages/launchSystemV3/material/cloudNew/updateCreate.tsx

@@ -0,0 +1,97 @@
+import { Button, Card, Form, message, Modal, Select, Space } from "antd"
+import React, { useEffect } from "react"
+import '../../tencentAdPutIn/index.less'
+import { useAjax } from "@/Hook/useAjax"
+import { updateOwnerApi } from "@/services/adqV3/cloudNew"
+import { getUserAllApi } from "@/services/operating/account"
+
+interface Props {
+    folderId: number
+    visible?: boolean
+    onClose?: () => void
+    onChange?: () => void
+}
+
+/**
+ * 修改文件所属人
+ * @param param0 
+ * @returns 
+ */
+const UpdateCreate: React.FC<Props> = ({ folderId, visible, onClose, onChange }) => {
+
+    /********************************/
+    const [form] = Form.useForm();
+
+    const updateOwner = useAjax((params) => updateOwnerApi(params))
+    const getUserAll = useAjax(() => getUserAllApi())
+    /********************************/
+
+    useEffect(() => {
+        getUserAll.run()
+    }, [])
+
+    const handleOk = (values: any) => {
+        console.log(values)
+        updateOwner.run({ ...values, folderId }).then(res => {
+            if (res) {
+                message.success('更新成功')
+                onChange?.()
+            }
+        })
+    }
+
+    return <Modal
+        title={<strong>修改文件所属人</strong>}
+        className="modalResetCss"
+        open={visible}
+        onCancel={onClose}
+        footer={null}
+        bodyStyle={{ padding: '0 0 40px', position: 'relative', borderRadius: '0 0 8px 8px' }}
+        maskClosable={false}
+        width={600}
+    >
+        <Form
+            form={form}
+            name="updateOwner"
+            labelAlign='left'
+            labelCol={{ span: 4 }}
+            layout="horizontal"
+            colon={false}
+            style={{ backgroundColor: '#f1f4fc', maxHeight: 650, overflow: 'hidden', overflowY: 'auto', padding: '10px', borderRadius: '0 0 8px 8px' }}
+            scrollToFirstError={{
+                behavior: 'smooth',
+                block: 'center'
+            }}
+            onFinishFailed={({ errorFields }) => {
+                message.error(errorFields?.[0]?.errors?.[0])
+            }}
+            onFinish={handleOk}
+            initialValues={{
+                isPublic: true
+            }}
+        >
+            <Card className="cardResetCss">
+                <Form.Item name="ownerId" label={<strong>所属人</strong>} rules={[{ required: true, message: '请选择所属人!' }]}>
+                    <Select
+                        placeholder="请选择所属人"
+                        showSearch
+                        filterOption={(input, option) =>
+                            ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                        }
+                        options={getUserAll?.data?.map((item: { nickname: any; userId: any }) => ({ label: item.nickname, value: item.userId }))}
+                    />
+                </Form.Item>
+            </Card>
+            <Form.Item className="submit_pull">
+                <Space>
+                    <Button onClick={onClose}>取消</Button>
+                    <Button type="primary" htmlType="submit" loading={updateOwner.loading} className="modalResetCss">
+                        确定
+                    </Button>
+                </Space>
+            </Form.Item>
+        </Form>
+    </Modal>
+}
+
+export default React.memo(UpdateCreate)

+ 84 - 0
src/pages/launchSystemV3/material/cloudNew/updateFile.tsx

@@ -0,0 +1,84 @@
+import { useAjax } from "@/Hook/useAjax"
+import { updateMaterialApi } from "@/services/adqV3/cloudNew"
+import { Button, Card, Form, Input, message, Modal, Space } from "antd"
+import React from "react"
+
+
+interface Props {
+    initialValues: any
+    visible?: boolean
+    onClose?: () => void
+    onChange?: () => void
+}
+
+/**
+ * 修改文件
+ * @returns 
+ */
+const UpdateFile: React.FC<Props> = ({ visible, onChange, onClose, initialValues }) => {
+
+    /******************************/
+    const [form] = Form.useForm();
+    
+    const updateMaterial = useAjax((params) => updateMaterialApi(params))
+    /******************************/
+
+    const handleOk = (values: any) => {
+        console.log(values)
+        updateMaterial.run({ ...values, id: initialValues.id }).then(res => {
+            if (res) {
+                message.success('修改成功')
+                onChange?.()
+            }
+        })
+    }
+
+    return <Modal
+        title={<strong>{'修改素材'}</strong>}
+        open={visible}
+        onCancel={onClose}
+        footer={null}
+        className="modalResetCss"
+        bodyStyle={{ padding: '0 0 40px', position: 'relative', borderRadius: '0 0 8px 8px' }}
+        maskClosable={false}
+        width={600}
+    >
+        <Form
+            form={form}
+            name="newFolder"
+            labelAlign='left'
+            labelCol={{ span: 4 }}
+            layout="horizontal"
+            colon={false}
+            style={{ backgroundColor: '#f1f4fc', maxHeight: 650, overflow: 'hidden', overflowY: 'auto', padding: '10px', borderRadius: '0 0 8px 8px' }}
+            scrollToFirstError={{
+                behavior: 'smooth',
+                block: 'center'
+            }}
+            onFinishFailed={({ errorFields }) => {
+                message.error(errorFields?.[0]?.errors?.[0])
+            }}
+            onFinish={handleOk}
+            initialValues={initialValues}
+        >
+            <Card className="cardResetCss">
+                <Form.Item name="materialName" label={<strong>素材名称</strong>} required={false} rules={[{ required: true, message: '请输入素材名称!' }]}>
+                    <Input placeholder="文件夹名称" />
+                </Form.Item>
+                <Form.Item name="description" label={<strong>备注</strong>}>
+                    <Input.TextArea placeholder="备注" />
+                </Form.Item>
+            </Card>
+            <Form.Item className="submit_pull">
+                <Space>
+                    <Button onClick={onClose}>取消</Button>
+                    <Button type="primary" htmlType="submit" loading={updateMaterial.loading} className="modalResetCss">
+                        确定
+                    </Button>
+                </Space>
+            </Form.Item>
+        </Form>
+    </Modal>
+}
+
+export default React.memo(UpdateFile)

+ 190 - 27
src/pages/launchSystemV3/material/cloudNew/uploadFile.tsx

@@ -1,4 +1,4 @@
-import { Button, Card, Form, Input, message, Modal, Radio, Select, Space, Upload } from "antd"
+import { Button, Card, Form, Input, message, Modal, Radio, Select, Space, TreeSelect, Upload } from "antd"
 import React, { useEffect, useMemo, useState } from "react"
 import '../../tencentAdPutIn/index.less'
 import { useAjax } from "@/Hook/useAjax"
@@ -7,8 +7,19 @@ import { RcFile } from "antd/lib/upload"
 import { UploadOutlined } from "@ant-design/icons"
 import style from './index.less'
 import CropperImg from "@/components/FileBoxAD/components/Cropper"
+import { compressAccurately } from "image-conversion"
+import { blobToBase64, dataURLtoFile, videoMessage } from "@/utils/compress"
+import { getImgSize } from "@/utils/utils"
+import request from "umi-request"
+import { getFileUrl } from "@/services/launchAdq/material"
+import getMD5 from "@/components/MD5"
+import { addMaterialApi, getFolderListApi } from "@/services/adqV3/cloudNew"
+import { DataNode } from "antd/lib/tree"
+import { updateTreeData } from "./const"
 
 interface Props {
+    folderId: number
+    userId: number,
     visible?: boolean
     onChange?: () => void
     onClose?: () => void
@@ -17,7 +28,7 @@ interface Props {
  * 上传素材
  * @returns 
  */
-const UploadFile: React.FC<Props> = ({ visible, onClose }) => {
+const UploadFile: React.FC<Props> = ({ visible, onClose, folderId, userId, onChange }) => {
 
     /**********************************/
     const [queryForm, setQueryForm] = useState<CLOUDNEW.AddMaterialProps>({ materialType: 'image' })
@@ -25,16 +36,134 @@ const UploadFile: React.FC<Props> = ({ visible, onClose }) => {
     const [fileUrl, setFileUrl] = useState<string>('')
     const [previewVisible, setPreviewVisible] = useState<boolean>(false)
     const [visibleCropper, setVisibleCropper] = useState<boolean>(false)
+    const [loading, setLoading] = useState<boolean>(false)
+    const [treeData, setTreeData] = useState<DataNode[]>([]);
 
     const getUserAll = useAjax(() => getUserAllApi())
+    const addMaterial = useAjax((params) => addMaterialApi(params))
+    const getFolderList = useAjax((params) => getFolderListApi(params))
+    //请求上传地址
+    const getFileUrlAjx = useAjax((params: { type: string, fileType: 'video' | 'image' }) => getFileUrl(params), { manual: true })
     /**********************************/
 
+    useEffect(() => {
+        getFolder()
+    }, [])
+
+    const getFolder = () => {
+        getFolderList.run({}).then(res => {
+            setTreeData(() => res?.map((item: { folderName: any; id: any; createBy: number }) => ({
+                title: item.folderName,
+                value: item.id,
+                key: item.id,
+                disabled: userId.toString() !== item.createBy.toString()
+            })) || [])
+        })
+    }
+
     useEffect(() => {
         getUserAll.run()
     }, [])
 
-    const handleOk = () => {
+    const handleOk = async () => {
+        if (!folderId && !queryForm?.folderId) {
+            message.error('当前不能上传素材,请选择文件夹')
+            return
+        }
+        if (queryForm?.file) {
+            let file = queryForm.file
+            let fileSize = 0
+            if (queryForm.materialType === 'image') {
+                fileSize = 409600
+            } else {
+                fileSize = 524288000
+            }
 
+            if (queryForm.materialType === 'image') {
+                if (file?.size > fileSize) { // 大于400kb进入压缩
+                    let bole = await compressAccurately(file, fileSize / 1024 - 50)
+                    if (bole?.size > fileSize) {
+                        bole = await compressAccurately(file, fileSize / 1024 - 100)
+                    }
+                    if (bole?.size > fileSize) {
+                        bole = await compressAccurately(file, fileSize / 1024 - 150)
+                    }
+                    if (bole?.size > fileSize) {
+                        bole = await compressAccurately(file, fileSize / 1024 - 200)
+                    }
+                    let newFile = await blobToBase64(bole)
+                    message.warning({
+                        content: `选择的图片大于${fileSize / 1024}KB,图片已压缩`,
+                        duration: 3
+                    })
+                    file = await dataURLtoFile(newFile, file?.name)
+                }
+            } else if (queryForm.materialType === 'video') {
+                if (file?.size > fileSize) { // 大于100mb进入压缩
+                    message.error({
+                        content: `选择的视频大于${fileSize / 1024 / 1024}MB,请重新选择提交`,
+                        duration: 3
+                    })
+                    return
+                }
+            }
+            setLoading(() => true)
+            let width = 0
+            let height = 0
+            let videoDuration = 0
+            if (queryForm.materialType === 'image') {
+                let imgData = await getImgSize(file)
+                width = imgData.width
+                height = imgData.height
+            } else if (queryForm.materialType === "video") {
+                let videoInfo: { width: number, height: number, videoLength: number }[] = await videoMessage([file])
+                width = videoInfo[0].width
+                height = videoInfo[0].height
+                videoDuration = videoInfo[0].videoLength
+            }
+            /**修改文件名以用户设置的文件title命名*/
+            let newFile = new File([file], queryForm?.materialName ? queryForm?.materialName + '.' + file?.name?.split('.')[1] : file?.name, { type: file?.type })
+            let formData = new FormData();
+            /**向阿里云请求上传地址*/
+            getFileUrlAjx.run({ type: newFile.type, fileType: queryForm.materialType }).then(res1 => {
+                Object.keys(res1).forEach((key: string) => {
+                    if (key !== 'url') {
+                        formData.append(key, res1[key])
+                    }
+                })
+                formData.append('file', newFile)
+                /**向阿里云返回的上传地址上传文件*/
+                request(res1?.ossUrl, { method: 'post', body: formData }).then(async (res2: { code: number, data: { url: string } }) => {
+                    if (res2.code === 200) {
+                        /**取到返回的文件地址向后端发送具体数据*/
+                        if (res2?.data?.url) {
+                            let fileMd5 = await getMD5(newFile)
+                            let obj: CLOUDNEW.AddMaterialProps = { ...queryForm, width, height, md5: fileMd5, ossUrl: res2?.data?.url, fileSize: newFile?.size, fileMime: newFile.type, aspectRatio: width / height }
+                            delete obj?.file
+                            if (!obj?.folderId) obj.folderId = folderId
+                            if (!obj?.designerId) obj.designerId = userId;
+                            if (queryForm.materialType === 'video') obj.videoDuration = videoDuration;
+                            if (!obj?.materialName) obj.materialName = queryForm.file.name;
+                            if (obj?.materialName && obj.materialName.match(RegExp(/[<>&\\'"/\x08\x09\x0A\x0D\x7F]/g))) {
+                                obj.materialName = obj.materialName.replace(RegExp(/[<>&\\'"/\x08\x09\x0A\x0D\x7F]/g), '')
+                            }
+                            obj.materialName = (obj as any).materialName.replace(/\.(jpg|jpeg|gif|png|mp4)$/i, '')
+                            addMaterial.run(obj).then((res) => {
+                                setLoading(() => false)
+                                if (res) {
+                                    message.success('添加成功')
+                                    onChange?.()
+                                }
+                            }).catch(() => setLoading(() => false))
+                        }
+                    } else {
+                        message.error('上传失败!')
+                    }
+                }).catch(() => setLoading(() => false))
+            }).catch(() => setLoading(() => false))
+        } else {
+            message.error('请选择素材')
+        }
     }
 
     const getVideo = useMemo(() => {
@@ -49,14 +178,30 @@ const UploadFile: React.FC<Props> = ({ visible, onClose }) => {
         }
     }, [queryForm?.file])
 
+    // 下级目录
+    const handleUpdateFolder = (parentId: number) => {
+        return getFolderListApi({ parentId }).then(res => {
+            setTreeData(origin =>
+                updateTreeData(origin, parentId, res?.data?.map((item: { folderName: any; id: any; createBy: number }) => ({
+                    title: item.folderName,
+                    value: item.id,
+                    key: item.id,
+                    disabled: userId.toString() !== item.createBy.toString()
+                })) || []),
+            );
+        })
+    }
+
     return <Modal
         title={<strong>{'上传素材'}</strong>}
         open={visible}
         onCancel={onClose}
+        onOk={handleOk}
         className="modalResetCss"
         bodyStyle={{ backgroundColor: '#f1f4fc', maxHeight: 650, overflow: 'hidden', overflowY: 'auto', padding: '10px' }}
         maskClosable={false}
         width={600}
+        confirmLoading={loading}
     >
         <Card className="cardResetCss">
             <Form
@@ -82,6 +227,26 @@ const UploadFile: React.FC<Props> = ({ visible, onClose }) => {
                     />
                 </Form.Item>
 
+                <Form.Item label={<strong>文件夹</strong>}>
+                    <TreeSelect
+                        loading={getFolderList.loading}
+                        style={{ width: '100%' }}
+                        value={queryForm?.folderId}
+                        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+                        placeholder="请选择文件夹(不选择默认当前文件夹)"
+                        onChange={(e) => {
+                            setQueryForm({ ...queryForm, folderId: e })
+                        }}
+                        loadData={({ value }) => {
+                            return new Promise<void>(async (resolve) => {
+                                await handleUpdateFolder(Number(value))
+                                resolve()
+                            })
+                        }}
+                        treeData={treeData}
+                    />
+                </Form.Item>
+
                 <Form.Item label={<strong>素材类型</strong>}>
                     <Radio.Group value={queryForm?.materialType} onChange={(e) => setQueryForm({ ...queryForm, materialType: e.target.value })} buttonStyle="solid">
                         <Radio.Button value="image">图片</Radio.Button>
@@ -93,7 +258,7 @@ const UploadFile: React.FC<Props> = ({ visible, onClose }) => {
                     <Input.TextArea placeholder="描述" value={queryForm?.description} onChange={(e) => setQueryForm({ ...queryForm, description: e.target.value })} />
                 </Form.Item>
 
-                <Form.Item label={<strong>{'上传图片'}</strong>}>
+                <Form.Item label={<strong>{queryForm?.materialType ? '上传图片' : '上传视频'}</strong>}>
                     {queryForm?.materialType === 'image' ? <Space>
                         <Upload
                             listType="picture-card"
@@ -117,29 +282,27 @@ const UploadFile: React.FC<Props> = ({ visible, onClose }) => {
                         >
                             {fileList?.length < 1 && '普通上传'}
                         </Upload>
-                        {
-                            fileList?.length === 0 && <Upload
-                                listType="picture-card"
-                                accept='image/gif,image/jpeg,image/png,image/jpg'
-                                beforeUpload={(file: RcFile) => {
-                                    return false
-                                }}
-                                fileList={fileList}
-                                onChange={(newFileList: any) => {
-                                    setQueryForm({ ...queryForm, file: newFileList.file })
-                                    setVisibleCropper(true)
-                                }}
-                                onRemove={() => {
-                                    setQueryForm({ ...queryForm, file: null })
-                                }}
-                                onPreview={(file: any) => {
-                                    setPreviewVisible(true)
-                                    setFileUrl(file.thumbUrl)
-                                }}
-                            >
-                                {fileList?.length < 1 && '裁剪上传'}
-                            </Upload>
-                        }
+                        {fileList?.length === 0 && <Upload
+                            listType="picture-card"
+                            accept='image/gif,image/jpeg,image/png,image/jpg'
+                            beforeUpload={(file: RcFile) => {
+                                return false
+                            }}
+                            fileList={fileList}
+                            onChange={(newFileList: any) => {
+                                setQueryForm({ ...queryForm, file: newFileList.file })
+                                setVisibleCropper(true)
+                            }}
+                            onRemove={() => {
+                                setQueryForm({ ...queryForm, file: null })
+                            }}
+                            onPreview={(file: any) => {
+                                setPreviewVisible(true)
+                                setFileUrl(file.thumbUrl)
+                            }}
+                        >
+                            {fileList?.length < 1 && '裁剪上传'}
+                        </Upload>}
                     </Space> : <>
                         <div className={style.file}>
                             <Button type='primary'><UploadOutlined />上传视频</Button>

+ 12 - 4
src/pages/launchSystemV3/material/typings.d.ts

@@ -20,6 +20,14 @@ declare namespace CLOUDNEW {
         setBatchFolderVisible: React.Dispatch<React.SetStateAction<boolean>>
         handleType: "folder" | "file"
         setHandleType: React.Dispatch<React.SetStateAction<"folder" | "file">>
+        searchParams: SearchProps
+        setSearchParams: React.Dispatch<React.SetStateAction<SearchProps>>
+        materialParams: GetMaterialListProps
+        setMaterialParams: React.Dispatch<React.SetStateAction<GetMaterialListProps>>
+    }
+    interface SearchProps {
+        searchType: "file" | "folder";
+        keyword?: string;
     }
     interface BreadcrumdData {
         label: string
@@ -43,23 +51,23 @@ declare namespace CLOUDNEW {
         pageSize: number,
         designerIds?: number[]  // 设计师
         folderId?: number       // 文件夹
-        height?: number         
+        height?: number
         width?: number
         materialName?: string   // 素材名称
         materialType?: string   // 素材类型
     }
     interface AddMaterialProps {
         designerId?: number,    // 设计师
-        fileMime?: string,  
+        fileMime?: string,
         fileSize?: number,
         height?: number,
         width?: number,
         materialName?: string,  // 素材名称
         materialType?: string,  // 素材类型
-        md5?: string,           
+        md5?: string,
         ossUrl?: string,        // 链接地址
         folderId?: number,     // 文件夹ID  
-        aspectRatio?: string,  // 素材比率
+        aspectRatio?: number,  // 素材比率
         description?: string,  // 描述
         videoDuration?: number, // 视频时长
         file?: any

+ 25 - 0
src/services/adqV3/cloudNew.ts

@@ -64,6 +64,18 @@ export async function moveFolderApi({ targetId, sourceId }: { targetId: number,
     })
 }
 
+/**
+ * 修改文件所属人
+ * @param params 
+ * @returns 
+ */
+export async function updateOwnerApi(params: { folderId: number, ownerId: number }) {
+    return request(api + `/material/folder/updateOwner`, {
+        method: 'POST',
+        params
+    })
+}
+
 /**
  * 文件夹详情
  * @param id 
@@ -124,4 +136,17 @@ export async function delMaterialApi(id: number) {
     return request(api + `/material/material/delete/${id}`, {
         method: 'DELETE'
     })
+}
+
+/**
+ * 移动素材
+ * @param param0 
+ * @returns 
+ */
+export async function moveMaterialApi({ folderId, materialIds }: { folderId: number, materialIds: number[] }) {
+    return request(api + `/material/material/move`, {
+        method: 'POST',
+        data: materialIds,
+        params: { folderId }
+    })
 }

+ 328 - 314
src/utils/utils.ts

@@ -8,66 +8,66 @@ const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(
 export const isUrl = (path: string): boolean => reg.test(path);
 
 export const isAntDesignPro = (): boolean => {
-  if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
-    return true;
-  }
-  return window.location.hostname === 'preview.pro.ant.design';
+    if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
+        return true;
+    }
+    return window.location.hostname === 'preview.pro.ant.design';
 };
 
 // 给官方演示站点用,用于关闭真实开发环境不需要使用的特性
 export const isAntDesignProOrDev = (): boolean => {
-  const { NODE_ENV } = process.env;
-  if (NODE_ENV === 'development') {
-    return true;
-  }
-  return isAntDesignPro();
+    const { NODE_ENV } = process.env;
+    if (NODE_ENV === 'development') {
+        return true;
+    }
+    return isAntDesignPro();
 };
 
 export const getPageQuery = () => {
-  const { href } = window.location;
-  const qsIndex = href.indexOf('?');
-  const sharpIndex = href.indexOf('#');
+    const { href } = window.location;
+    const qsIndex = href.indexOf('?');
+    const sharpIndex = href.indexOf('#');
 
-  if (qsIndex !== -1) {
-    if (qsIndex > sharpIndex) {
-      return parse(href.split('?')[1]);
-    }
+    if (qsIndex !== -1) {
+        if (qsIndex > sharpIndex) {
+            return parse(href.split('?')[1]);
+        }
 
-    return parse(href.slice(qsIndex + 1, sharpIndex));
-  }
+        return parse(href.slice(qsIndex + 1, sharpIndex));
+    }
 
-  return {};
+    return {};
 };
 
 // 排序这是比较函数
 export const compare = (field: string, order: 'descend' | 'ascend') => {
-  // descend 降序 大到小  ascend 升序 小到大
-  if (order === 'ascend') {
-    return function (m: any, n: any) {
-      var a = m[field];
-      var b = n[field];
-      return a - b; //升序
-    }
-  } else {
-    return function (m: any, n: any) {
-      var a = m[field];
-      var b = n[field];
-      return b - a; //降序
+    // descend 降序 大到小  ascend 升序 小到大
+    if (order === 'ascend') {
+        return function (m: any, n: any) {
+            var a = m[field];
+            var b = n[field];
+            return a - b; //升序
+        }
+    } else {
+        return function (m: any, n: any) {
+            var a = m[field];
+            var b = n[field];
+            return b - a; //降序
+        }
     }
-  }
 }
 
 // 返回别名
 export const getChannelName = (name: string) => {
-  let newName = name
-  let abridgeServer: string[] = ['知定', '巨网', '广联', '太古', '云广', '傲星', '弘捷', '开域']
-  let asName = abridgeServer.find((item: string) => name?.indexOf(item) !== -1)
-  if (asName) {
-    newName = asName
-  } else if (newName?.length > 5) {
-    newName = newName?.slice(2, 5) + '...'
-  }
-  return newName
+    let newName = name
+    let abridgeServer: string[] = ['知定', '巨网', '广联', '太古', '云广', '傲星', '弘捷', '开域']
+    let asName = abridgeServer.find((item: string) => name?.indexOf(item) !== -1)
+    if (asName) {
+        newName = asName
+    } else if (newName?.length > 5) {
+        newName = newName?.slice(2, 5) + '...'
+    }
+    return newName
 }
 
 
@@ -77,122 +77,122 @@ export const getChannelName = (name: string) => {
  * @returns 
  */
 function arrayToRegex(arr: string[]) {
-  // 转义正则表达式中的特殊字符
-  function escapeRegExp(str: string) {
-    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-  }
+    // 转义正则表达式中的特殊字符
+    function escapeRegExp(str: string) {
+        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+    }
 
-  // 将数组中的每个元素用括号包裹,并用 | 连接
-  const regexString = arr.map(item => `(${escapeRegExp(item)})`).join('|');
+    // 将数组中的每个元素用括号包裹,并用 | 连接
+    const regexString = arr.map(item => `(${escapeRegExp(item)})`).join('|');
 
-  // 创建正则表达式
-  const regex = new RegExp(regexString, 'g');
-  return regex;
+    // 创建正则表达式
+    const regex = new RegExp(regexString, 'g');
+    return regex;
 }
 
 function extractAndFilterBracketsContent(input: string): { extracted: string[], filteredString: string } {
-  const arr = ['[微笑]', '[撇嘴]', '[色]', '[发呆]', '[流泪]', '[害羞]', '[睡]', '[大哭]', '[尴尬]', '[发怒]', '[调皮]', '[呲牙]', '[惊讶]', '[难过]', '[冷汗]', '[抓狂]', '[偷笑]', '[愉快]', '[白眼]', '[傲慢]', '[惊恐]', '[流汗]', '[憨笑]', '[奋斗]', '[疑问]', '[晕]', '[衰]', '[敲打]', '[再见]', '[擦汗]', '[鼓掌]', '[坏笑]', '[左哼哼]', '[右哼哼]', '[哈欠]', '[委屈]', '[快哭了]', '[阴险]', '[亲亲]', '[可怜]', '[西瓜]', '[咖啡]', '[猪头]', '[玫瑰]', '[嘴唇]', '[爱心]', '[蛋糕]', '[月亮]', '[太阳]', '[拥抱]', '[强]', '[胜利]', '[握手]', '[抱拳]', '[勾引]', '[拳头]', '[OK]', '[跳跳]', '[发抖]', '[怄火]', '[转圈]', '[嘿哈]', '[捂脸]', '[奸笑]', '[机智]', '[皱眉]', '[耶]', '[加油]', '[汗]', '[天啊]', '[社会社会]', '[旺柴]', '[好的]', '[加油加油]', '[哇]', '[红包]', '[發]', '[福]'];
-  const regex = arrayToRegex(arr);
-  const matches: string[] = [];
-  let match;
-
-  // 提取方括号内的内容
-  while ((match = regex.exec(input)) !== null) {
-    matches.push(match[1]);
-  }
+    const arr = ['[微笑]', '[撇嘴]', '[色]', '[发呆]', '[流泪]', '[害羞]', '[睡]', '[大哭]', '[尴尬]', '[发怒]', '[调皮]', '[呲牙]', '[惊讶]', '[难过]', '[冷汗]', '[抓狂]', '[偷笑]', '[愉快]', '[白眼]', '[傲慢]', '[惊恐]', '[流汗]', '[憨笑]', '[奋斗]', '[疑问]', '[晕]', '[衰]', '[敲打]', '[再见]', '[擦汗]', '[鼓掌]', '[坏笑]', '[左哼哼]', '[右哼哼]', '[哈欠]', '[委屈]', '[快哭了]', '[阴险]', '[亲亲]', '[可怜]', '[西瓜]', '[咖啡]', '[猪头]', '[玫瑰]', '[嘴唇]', '[爱心]', '[蛋糕]', '[月亮]', '[太阳]', '[拥抱]', '[强]', '[胜利]', '[握手]', '[抱拳]', '[勾引]', '[拳头]', '[OK]', '[跳跳]', '[发抖]', '[怄火]', '[转圈]', '[嘿哈]', '[捂脸]', '[奸笑]', '[机智]', '[皱眉]', '[耶]', '[加油]', '[汗]', '[天啊]', '[社会社会]', '[旺柴]', '[好的]', '[加油加油]', '[哇]', '[红包]', '[發]', '[福]'];
+    const regex = arrayToRegex(arr);
+    const matches: string[] = [];
+    let match;
+
+    // 提取方括号内的内容
+    while ((match = regex.exec(input)) !== null) {
+        matches.push(match[1]);
+    }
 
-  // 过滤掉原字符串中方括号及其包裹的内容
-  const filteredString = input.replace(regex, '');
+    // 过滤掉原字符串中方括号及其包裹的内容
+    const filteredString = input.replace(regex, '');
 
-  return { extracted: matches, filteredString: filteredString };
+    return { extracted: matches, filteredString: filteredString };
 }
 
 // 输入文案时判断
 export const txtLength = (t?: string) => {
-  if (t) {
-    const result = extractAndFilterBracketsContent(t);
-    let extracted = result.extracted;
-    let value = result.filteredString;
-    let length = value?.length
-    let text = value?.replace(/[\x00-\xff]/g, '')
-    return extracted.length + text?.length + Number(((length - text?.length) / 2).toFixed())
-  } else {
-    return 0
-  }
+    if (t) {
+        const result = extractAndFilterBracketsContent(t);
+        let extracted = result.extracted;
+        let value = result.filteredString;
+        let length = value?.length
+        let text = value?.replace(/[\x00-\xff]/g, '')
+        return extracted.length + text?.length + Number(((length - text?.length) / 2).toFixed())
+    } else {
+        return 0
+    }
 }
 
 // 返回图片宽高
 export const getImgSize = (file: RcFile): Promise<{ width: number, height: number }> => {
-  return new Promise((resolve: (value: any) => void, reject: (reason?: any) => void) => {
-    if (file) {
-      let img: any = new Image();
-      let _URL = window.URL || window.webkitURL;
-      img.onload = function (e: any) {
-        resolve({ width: this.width, height: this.height })
-      }
-      img.src = _URL.createObjectURL(file);
-    } else {
-      reject()
-    }
-  })
+    return new Promise((resolve: (value: any) => void, reject: (reason?: any) => void) => {
+        if (file) {
+            let img: any = new Image();
+            let _URL = window.URL || window.webkitURL;
+            img.onload = function (e: any) {
+                resolve({ width: this.width, height: this.height })
+            }
+            img.src = _URL.createObjectURL(file);
+        } else {
+            reject()
+        }
+    })
 }
 
 // 返回落地页组件key
 export const getTypeKey = (key: string): string => {
-  switch (key) {
-    case 'TOP_IMAGE':
-      return 'topImageSpec'
-    case 'TOP_SLIDER':
-      return 'topSliderSpec'
-    case 'TOP_VIDEO':
-      return 'topVideoSpec'
-    case 'IMAGE':
-      return 'imageSpec'
-    case 'TEXT':
-      return 'textSpec'
-    case 'GH':
-      return 'ghSpec'
-    case 'ENTERPRISE_WX':
-      return 'enterpriseWxSpec'
-    case 'IMAGE_TEXT':
-      return 'imageTextSpec'
-    case 'FLOAT_BUTTON':
-      return 'floatButtonSpec'
-  }
-  return ''
+    switch (key) {
+        case 'TOP_IMAGE':
+            return 'topImageSpec'
+        case 'TOP_SLIDER':
+            return 'topSliderSpec'
+        case 'TOP_VIDEO':
+            return 'topVideoSpec'
+        case 'IMAGE':
+            return 'imageSpec'
+        case 'TEXT':
+            return 'textSpec'
+        case 'GH':
+            return 'ghSpec'
+        case 'ENTERPRISE_WX':
+            return 'enterpriseWxSpec'
+        case 'IMAGE_TEXT':
+            return 'imageTextSpec'
+        case 'FLOAT_BUTTON':
+            return 'floatButtonSpec'
+    }
+    return ''
 }
 
 // 点击复制
 export const copy = (str: string) => {
-  let element = document.createElement("textarea");
-  element.id = 'myTextarea'
-  element.textContent = str
-  document.body.append(element);
-  (document.getElementById('myTextarea') as any).select();
-  document.execCommand("Copy")
-  document.body.removeChild(element);
-  message.success(`复制成功:${str}`)
+    let element = document.createElement("textarea");
+    element.id = 'myTextarea'
+    element.textContent = str
+    document.body.append(element);
+    (document.getElementById('myTextarea') as any).select();
+    document.execCommand("Copy")
+    document.body.removeChild(element);
+    message.success(`复制成功:${str}`)
 }
 
 // 数组分组
 export const groupBy = (array: any[], f: (item: any) => any[]) => {
-  const groups: any = {};
-  array.forEach(function (o) { //注意这里必须是forEach 大写
-    const group = JSON.stringify(f(o));
-    groups[group] = groups[group] || [];
-    groups[group].push(o);
-  });
-  return Object.keys(groups).map(function (group) {
-    return groups[group];
-  });
+    const groups: any = {};
+    array.forEach(function (o) { //注意这里必须是forEach 大写
+        const group = JSON.stringify(f(o));
+        groups[group] = groups[group] || [];
+        groups[group].push(o);
+    });
+    return Object.keys(groups).map(function (group) {
+        return groups[group];
+    });
 }
 
 export const replaceSpecialTxt = (text: string | number | null | undefined) => {
-  if (text) {
-    return text.toString().replace(/[<>]/ig, '')
-  } else {
-    return text
-  }
+    if (text) {
+        return text.toString().replace(/[<>]/ig, '')
+    } else {
+        return text
+    }
 }
 
 /**
@@ -203,16 +203,16 @@ export const replaceSpecialTxt = (text: string | number | null | undefined) => {
  * @returns string
  */
 export const randomString = (flag: boolean, min: number, max: number) => {
-  let str = "", range = min
-  let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
-  if (flag) {
-    range = Math.round(Math.random() * (max - min)) + min;
-  }
-  for (let i = 0; i < range; i++) {
-    let pos = Math.round(Math.random() * (arr.length - 1));
-    str += arr[pos];
-  }
-  return str;
+    let str = "", range = min
+    let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
+    if (flag) {
+        range = Math.round(Math.random() * (max - min)) + min;
+    }
+    for (let i = 0; i < range; i++) {
+        let pos = Math.round(Math.random() * (arr.length - 1));
+        str += arr[pos];
+    }
+    return str;
 }
 
 
@@ -223,9 +223,9 @@ export const randomString = (flag: boolean, min: number, max: number) => {
  * @returns 
  */
 export const getArrDifference = (arr1: any[], arr2: any[]) => {
-  return arr1.concat(arr2).filter((v, i, arr) => {
-    return arr.indexOf(v) === arr.lastIndexOf(v);
-  });
+    return arr1.concat(arr2).filter((v, i, arr) => {
+        return arr.indexOf(v) === arr.lastIndexOf(v);
+    });
 }
 
 /**
@@ -233,33 +233,33 @@ export const getArrDifference = (arr1: any[], arr2: any[]) => {
  * @param videoUrl t_0 0秒的视频截图 t_10000 10秒的视频截图帧
  */
 export const getVideoImgUrl = (videoUrl: string) => {
-  if (['.mp4', '.swf', '.flv', '.rm', '.ram', '.mov', '.mpg', '.mpeg', '.wmv', '.avi'].some(item => videoUrl.includes(item))) {
-    return videoUrl + "?x-oss-process=video/snapshot,t_0,f_jpg,w_0,h_0,m_fast,ar_auto"
-  }
-  return videoUrl
+    if (['.mp4', '.swf', '.flv', '.rm', '.ram', '.mov', '.mpg', '.mpeg', '.wmv', '.avi'].some(item => videoUrl.includes(item))) {
+        return videoUrl + "?x-oss-process=video/snapshot,t_0,f_jpg,w_0,h_0,m_fast,ar_auto"
+    }
+    return videoUrl
 }
 
 // 设置值
 export const setValueAtPath = (path: string, object: { [k: string]: any }, value: any): void => {
-  if (path.startsWith('/')) {
-    path = path.slice(1);
-  }
-  let parts = path.split('/');
-  let current: { [k: string]: any } = object;
-  for (let i = 0; i < parts.length; i++) {
-    let part = parts[i];
-    if (i === parts.length - 2) {
-      let children = current?.['children'] || {}
-      children[parts[parts.length - 1]] = value
-      current['children'] = children;
-      break
-    } else {
-      if (!(part in current)) {
-        current[part] = {};
-      }
-      current = current[part];
+    if (path.startsWith('/')) {
+        path = path.slice(1);
+    }
+    let parts = path.split('/');
+    let current: { [k: string]: any } = object;
+    for (let i = 0; i < parts.length; i++) {
+        let part = parts[i];
+        if (i === parts.length - 2) {
+            let children = current?.['children'] || {}
+            children[parts[parts.length - 1]] = value
+            current['children'] = children;
+            break
+        } else {
+            if (!(part in current)) {
+                current[part] = {};
+            }
+            current = current[part];
+        }
     }
-  }
 }
 
 /**
@@ -268,115 +268,115 @@ export const setValueAtPath = (path: string, object: { [k: string]: any }, value
  * @returns 
  */
 export const processData = (data: string | any[]) => {
-  let children = [], parentData: { [k: string]: any } = {};
-  for (let i = 0; i < data.length; i += 1) {
-    let node = data[i];
-    if (!(node?.parentName) || node?.parentName === "") {
-      parentData[node.name] = {
-        ...data[i],
-        children: {}
-      }
-    } else {
-      children.push(data[i])
+    let children = [], parentData: { [k: string]: any } = {};
+    for (let i = 0; i < data.length; i += 1) {
+        let node = data[i];
+        if (!(node?.parentName) || node?.parentName === "") {
+            parentData[node.name] = {
+                ...data[i],
+                children: {}
+            }
+        } else {
+            children.push(data[i])
+        }
     }
-  }
-  // console.log('children--->', children)
-  let jumpInfo = [], list: any[] = [], left_button: any = {}, right_button: any = {}
-  for (let i = 0; i < children.length; i += 1) {
-    let node = children[i];
-    if (node.parentName) {
-      if (node.parentName === "jump_info" && !['/jump_info/page_type', '/jump_info/backups', '/jump_info/page_spec'].includes(node.path)) {
-        jumpInfo.push(node)
-      } else if (node.parentName === "list") {
-        list.push(node)
-      } else if (node.parentName === "left_button") {
-        left_button[node.name] = node
-      } else if (node.parentName === "right_button") {
-        right_button[node.name] = node
-      } else {
-        let c = parentData[node.parentName]
-        if (c) {
-          c['children'][node.name] = node
-          parentData[node.parentName] = c
+    // console.log('children--->', children)
+    let jumpInfo = [], list: any[] = [], left_button: any = {}, right_button: any = {}
+    for (let i = 0; i < children.length; i += 1) {
+        let node = children[i];
+        if (node.parentName) {
+            if (node.parentName === "jump_info" && !['/jump_info/page_type', '/jump_info/backups', '/jump_info/page_spec'].includes(node.path)) {
+                jumpInfo.push(node)
+            } else if (node.parentName === "list") {
+                list.push(node)
+            } else if (node.parentName === "left_button") {
+                left_button[node.name] = node
+            } else if (node.parentName === "right_button") {
+                right_button[node.name] = node
+            } else {
+                let c = parentData[node.parentName]
+                if (c) {
+                    c['children'][node.name] = node
+                    parentData[node.parentName] = c
+                } else {
+                    console.log('空childen-->', node.parentName)
+                }
+            }
         } else {
-          console.log('空childen-->', node.parentName)
+            console.log('空node.parentName--->', node)
         }
-      }
-    } else {
-      console.log('空node.parentName--->', node)
     }
-  }
-  if (jumpInfo.length > 0) {
-    jumpInfo.forEach(item => {
-      if (item.path) {
-        setValueAtPath(item.path, parentData, item)
-      } else {
-        console.log('没有path---->', item)
-      }
-    })
-  }
-  if (Object.keys(left_button).length > 0) {
-    let children = {
-      text: left_button?.text
+    if (jumpInfo.length > 0) {
+        jumpInfo.forEach(item => {
+            if (item.path) {
+                setValueAtPath(item.path, parentData, item)
+            } else {
+                console.log('没有path---->', item)
+            }
+        })
     }
-    let newLeftButton = [{ ...left_button?.jump_info, children }]
-    newLeftButton.forEach(item => {
-      if (item.path) {
-        setValueAtPath(item.path, parentData, item)
-      } else {
-        console.log('没有path---->', item)
-      }
-    })
-  }
-  if (Object.keys(right_button).length > 0) {
-    let children = {
-      text: right_button?.text
+    if (Object.keys(left_button).length > 0) {
+        let children = {
+            text: left_button?.text
+        }
+        let newLeftButton = [{ ...left_button?.jump_info, children }]
+        newLeftButton.forEach(item => {
+            if (item.path) {
+                setValueAtPath(item.path, parentData, item)
+            } else {
+                console.log('没有path---->', item)
+            }
+        })
     }
-    let newRightButton = [{ ...right_button?.jump_info, children }]
-    newRightButton.forEach(item => {
-      if (item.path) {
-        setValueAtPath(item.path, parentData, item)
-      } else {
-        console.log('没有path---->', item)
-      }
-    })
-  }
-  if (list?.length > 0 && Object.keys(parentData).includes('label')) {
-    let l = parentData?.label?.children?.list
-    if (l) {
-      let lChildren: any = {}
-      list.forEach(item => {
-        lChildren[item.name] = item
-      })
-      l = { ...l, children: lChildren }
-      parentData.label.children.list = l
+    if (Object.keys(right_button).length > 0) {
+        let children = {
+            text: right_button?.text
+        }
+        let newRightButton = [{ ...right_button?.jump_info, children }]
+        newRightButton.forEach(item => {
+            if (item.path) {
+                setValueAtPath(item.path, parentData, item)
+            } else {
+                console.log('没有path---->', item)
+            }
+        })
     }
-  }
-  if (list?.length > 0 && Object.keys(parentData).includes('element_story')) {
-    let l = parentData?.element_story?.children?.list
-    if (l) {
-      let lChildren: any = {}
-      list.forEach(item => {
-        lChildren[item.name] = item
-      })
-      l = { ...l, children: lChildren }
-      parentData.element_story.children.list = l
+    if (list?.length > 0 && Object.keys(parentData).includes('label')) {
+        let l = parentData?.label?.children?.list
+        if (l) {
+            let lChildren: any = {}
+            list.forEach(item => {
+                lChildren[item.name] = item
+            })
+            l = { ...l, children: lChildren }
+            parentData.label.children.list = l
+        }
     }
-  }
-
-  if (list?.length > 0 && Object.keys(parentData).includes('image_list')) {
-    let l = parentData?.image_list?.children?.list
-    if (l) {
-      let lChildren: any = {}
-      list.forEach(item => {
-        lChildren[item.name] = item
-      })
-      l = { ...l, children: lChildren }
-      parentData.image_list.children.list = l
+    if (list?.length > 0 && Object.keys(parentData).includes('element_story')) {
+        let l = parentData?.element_story?.children?.list
+        if (l) {
+            let lChildren: any = {}
+            list.forEach(item => {
+                lChildren[item.name] = item
+            })
+            l = { ...l, children: lChildren }
+            parentData.element_story.children.list = l
+        }
+    }
+
+    if (list?.length > 0 && Object.keys(parentData).includes('image_list')) {
+        let l = parentData?.image_list?.children?.list
+        if (l) {
+            let lChildren: any = {}
+            list.forEach(item => {
+                lChildren[item.name] = item
+            })
+            l = { ...l, children: lChildren }
+            parentData.image_list.children.list = l
+        }
     }
-  }
 
-  return parentData;
+    return parentData;
 }
 
 /**
@@ -386,7 +386,7 @@ export const processData = (data: string | any[]) => {
  * @returns 
  */
 export function cartesianProduct<T, U>(arr1: T[], arr2: U[]): [T, U, string][] {
-  return arr1.flatMap((d1, index1) => arr2.map((d2, index2) => [d1, d2, `${index1 + 1}_${index2 + 1}`] as [T, U, string]));
+    return arr1.flatMap((d1, index1) => arr2.map((d2, index2) => [d1, d2, `${index1 + 1}_${index2 + 1}`] as [T, U, string]));
 }
 
 /**
@@ -396,22 +396,22 @@ export function cartesianProduct<T, U>(arr1: T[], arr2: U[]): [T, U, string][] {
  * @returns 
  */
 export const arraysHaveSameValues = (arr1: any[], arr2: any[]): boolean => {
-  // 首先检查数组的长度是否相同
-  if (arr1.length !== arr2.length) {
-    return false;
-  }
-
-  // 使用数组的 sort 方法对两个数组进行排序,然后逐个比较
-  const sortedArr1 = [...arr1].sort();
-  const sortedArr2 = [...arr2].sort();
-
-  for (let i = 0; i < sortedArr1.length; i++) {
-    if (sortedArr1[i] !== sortedArr2[i]) {
-      return false;
+    // 首先检查数组的长度是否相同
+    if (arr1.length !== arr2.length) {
+        return false;
+    }
+
+    // 使用数组的 sort 方法对两个数组进行排序,然后逐个比较
+    const sortedArr1 = [...arr1].sort();
+    const sortedArr2 = [...arr2].sort();
+
+    for (let i = 0; i < sortedArr1.length; i++) {
+        if (sortedArr1[i] !== sortedArr2[i]) {
+            return false;
+        }
     }
-  }
 
-  return true;
+    return true;
 }
 
 
@@ -422,15 +422,15 @@ export const arraysHaveSameValues = (arr1: any[], arr2: any[]): boolean => {
  * @returns 
  */
 export const distributeArray = (originalArray: any[], numGroups: number): any[][] => {
-  // 初始化结果数组,每个子数组为空
-  const result: any[][] = Array.from({ length: numGroups }, () => []);
+    // 初始化结果数组,每个子数组为空
+    const result: any[][] = Array.from({ length: numGroups }, () => []);
 
-  // 分配原始数组中的元素到结果数组
-  originalArray.forEach((element, index) => {
-    result[index % numGroups].push(element);
-  });
+    // 分配原始数组中的元素到结果数组
+    originalArray.forEach((element, index) => {
+        result[index % numGroups].push(element);
+    });
 
-  return result;
+    return result;
 };
 
 
@@ -441,21 +441,21 @@ export const distributeArray = (originalArray: any[], numGroups: number): any[][
  * @returns 
  */
 export function getRandomElements<T>(arr: T[], n: number): T[] {
-  if (n > arr.length) {
-    throw new RangeError("getRandomElements: more elements taken than available");
-  }
+    if (n > arr.length) {
+        throw new RangeError("getRandomElements: more elements taken than available");
+    }
 
-  const result: T[] = new Array(n);
-  let len: number = arr.length;
-  const taken: number[] = new Array(len);
+    const result: T[] = new Array(n);
+    let len: number = arr.length;
+    const taken: number[] = new Array(len);
 
-  while (n--) {
-    const x: number = Math.floor(Math.random() * len);
-    result[n] = arr[x in taken ? taken[x] : x];
-    taken[x] = --len in taken ? taken[len] : len;
-  }
+    while (n--) {
+        const x: number = Math.floor(Math.random() * len);
+        result[n] = arr[x in taken ? taken[x] : x];
+        taken[x] = --len in taken ? taken[len] : len;
+    }
 
-  return result;
+    return result;
 }
 
 
@@ -465,20 +465,20 @@ export function getRandomElements<T>(arr: T[], n: number): T[] {
  * @returns 
  */
 export function shuffleArray<T>(array: T[]): T[] {
-  const shuffled = array.slice();
-  for (let i = shuffled.length - 1; i > 0; i--) {
-    const j = Math.floor(Math.random() * (i + 1));
-    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
-  }
-  return shuffled;
+    const shuffled = array.slice();
+    for (let i = shuffled.length - 1; i > 0; i--) {
+        const j = Math.floor(Math.random() * (i + 1));
+        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+    }
+    return shuffled;
 }
 
 export function chunkArray<T>(array: T[], numChunks: number): T[][] {
-  const chunks: T[][] = Array.from({ length: numChunks }, () => []);
-  array.forEach((item, index) => {
-    chunks[index % numChunks].push(item);
-  });
-  return chunks;
+    const chunks: T[][] = Array.from({ length: numChunks }, () => []);
+    array.forEach((item, index) => {
+        chunks[index % numChunks].push(item);
+    });
+    return chunks;
 }
 /**
  * 按多少个一组分组
@@ -487,11 +487,11 @@ export function chunkArray<T>(array: T[], numChunks: number): T[][] {
  * @returns 
  */
 export function chunkArray1<T>(array: T[], chunkSize: number): T[][] {
-  const result: T[][] = [];
-  for (let i = 0; i < array.length; i += chunkSize) {
-      result.push(array.slice(i, i + chunkSize));
-  }
-  return result;
+    const result: T[][] = [];
+    for (let i = 0; i < array.length; i += chunkSize) {
+        result.push(array.slice(i, i + chunkSize));
+    }
+    return result;
 }
 /**
  * 打乱随机分配数组
@@ -500,6 +500,20 @@ export function chunkArray1<T>(array: T[], chunkSize: number): T[][] {
  * @returns 
  */
 export function splitArrayIntoRandomChunks<T>(array: T[], numChunks: number): T[][] {
-  const shuffledArray = shuffleArray(array);
-  return chunkArray(shuffledArray, numChunks);
-}
+    const shuffledArray = shuffleArray(array);
+    return chunkArray(shuffledArray, numChunks);
+}
+
+/**
+ * 秒数格式化
+ * @param seconds 秒
+ * @returns 
+ */
+export function formatSecondsToTime(seconds: number): string {
+    const hours = Math.floor(seconds / 3600);
+    const minutes = Math.floor((seconds % 3600) / 60);
+    const secs = seconds % 60;
+
+    // 使用 padStart 确保每个部分都是两位数  
+    return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
+}