wjx il y a 7 mois
Parent
commit
0b7043d95d

+ 3 - 3
src/components/FileBoxAD/components/Cropper/index.tsx

@@ -97,7 +97,7 @@ const CropperImg: React.FC<Props> = (props) => {
     const getCropData = async () => {
         if (typeof cropper !== "undefined") {
             let newFile = await dataURLtoFile(cropper.getCroppedCanvas().toDataURL('image/jpeg'), file?.name)
-            onChange && onChange([{
+            onChange?.([{
                 lastModified: newFile.lastModified,
                 // lastModifiedDate: newFile.lastModifiedDate,
                 name: newFile.name,
@@ -108,8 +108,8 @@ const CropperImg: React.FC<Props> = (props) => {
                 // uid: newFile.uid,
                 originFileObj: newFile
             }], newFile)
-            setCropper(undefined)
-            setImage('')
+            // setCropper(undefined)
+            // setImage('')
         }
     }
 

+ 20 - 0
src/pages/launchSystemV3/adMonitorListV3/FilterDynamicQuery.tsx

@@ -7,6 +7,7 @@ import { useLocalStorageState, useTimeout } from "ahooks"
 import { GetDynamicCreativeMonitorProps } from "@/services/adMonitor/adMonitor"
 import { useModel } from "umi"
 import { DELIVERY_MODE_ENUM } from "../tencentAdPutIn/const"
+import { AdUnitType_Enum } from "@/pages/launchSystemNew/account/const"
 const { RangePicker } = DatePicker;
 
 type TypeProps = 'DatePicker' | 'Input' | 'InputNumber' | 'Select'
@@ -91,6 +92,22 @@ const FilterDynamicQuery: React.FC<Props> = ({ onChange, initialValues, queryFor
             type: 'Input',
             value: (params) => <Input placeholder="请输入广告账号(多个,隔开)" style={{ width: '100%' }} {...params} />
         },
+        {
+            lable: '业务单元类型',
+            name: 'adUnitType',
+            type: 'Select',
+            value: (params) => <Select
+                placeholder='业务单元类型'
+                style={{ width: '100%', minWidth: 130 }}
+                showSearch
+                filterOption={(input: any, option: any) =>
+                    (option!.children as unknown as string).toLowerCase().includes(input.toLowerCase())
+                }
+                allowClear
+                {...params}
+                options={Object.keys(AdUnitType_Enum).map(key => ({ label: AdUnitType_Enum[key as keyof typeof AdUnitType_Enum], value: key }))}
+            />
+        },
         {
             lable: '最小总消耗',
             name: 'costTotalMin',
@@ -308,6 +325,9 @@ const FilterDynamicQuery: React.FC<Props> = ({ onChange, initialValues, queryFor
                     case 'deliveryMode':
                         value = value?.length > 0 ? value.map((item: string) => DELIVERY_MODE_ENUM[item as keyof typeof DELIVERY_MODE_ENUM]).toString() : '请选择'
                         break
+                    case 'adUnitType':
+                        value = AdUnitType_Enum[value as keyof typeof AdUnitType_Enum] || '请选择'
+                        break
                     case 'createdTime':
                         value = value ? '已经选择时间' : '请选择时间'
                         break

+ 20 - 63
src/pages/launchSystemV3/adMonitorListV3/FilterQuery.tsx

@@ -8,6 +8,7 @@ import { AdStatusEnum, OptimizationGoalEnum, PromotedObjectType } from "@/servic
 import { AdListProps } from "@/services/adMonitor/adMonitor"
 import { useModel } from "umi"
 import { ADGROUP_STATUS } from "../adqv3/const"
+import { AdUnitType_Enum } from "@/pages/launchSystemNew/account/const"
 const { RangePicker } = DatePicker;
 
 type TypeProps = 'DatePicker' | 'Input' | 'InputNumber' | 'Select'
@@ -122,25 +123,6 @@ const FilterQuery: React.FC<Props> = ({ onChange, initialValues, queryForm, setQ
             type: 'Input',
             value: (params) => <Input placeholder="请输入本地账号备注" style={{ width: '100%' }} {...params} />
         },
-        // {
-        //     lable: '推广目标',
-        //     name: 'promotedObjectType',
-        //     type: 'Select',
-        //     value: (params) => <Select
-        //         placeholder='推广目标'
-        //         style={{ width: '100%', minWidth: 130 }}
-        //         showSearch
-        //         filterOption={(input: any, option: any) =>
-        //             (option!.children as unknown as string).toLowerCase().includes(input.toLowerCase())
-        //         }
-        //         allowClear
-        //         {...params}
-        //     >
-        //         {Object.keys(PromotedObjectType).map(key => {
-        //             return <Select.Option value={key} key={key}>{PromotedObjectType[key]}</Select.Option>
-        //         })}
-        //     </Select>
-        // },
         {
             lable: '优化目标',
             name: 'optimizationGoal',
@@ -176,50 +158,22 @@ const FilterQuery: React.FC<Props> = ({ onChange, initialValues, queryForm, setQ
                 <Select.Option value={false}>否</Select.Option>
             </Select>
         },
-        // {
-        //     lable: '已删除?',
-        //     name: 'isDeleted',
-        //     type: 'Select',
-        //     value: (params) => <Select
-        //         placeholder='已删除?'
-        //         style={{ width: '100%', minWidth: 130 }}
-        //         showSearch
-        //         allowClear
-        //         filterOption={(input: any, option: any) =>
-        //             (option!.children as unknown as string).toLowerCase().includes(input.toLowerCase())
-        //         }
-        //         {...params}
-        //     >
-        //         <Select.Option value={true}>是</Select.Option>
-        //         <Select.Option value={false}>否</Select.Option>
-        //     </Select>
-        // },
-        // {
-        //     lable: '广告状态',
-        //     name: 'status',
-        //     type: 'Select',
-        //     value: (params) => <Select
-        //         placeholder='广告状态'
-        //         mode="multiple"
-        //         style={{ width: '100%', minWidth: 130 }}
-        //         showSearch
-        //         filterOption={(input: any, option: any) =>
-        //             (option!.children as unknown as string).toLowerCase().includes(input.toLowerCase())
-        //         }
-        //         allowClear
-        //         {...params}
-        //     >
-        //         {Object.keys(AdStatusEnum).map(key => {
-        //             return <Select.Option value={key} key={key}>{AdStatusEnum[key]}</Select.Option>
-        //         })}
-        //     </Select>
-        // },
-        // {
-        //     lable: '最低今日转化数',
-        //     name: 'conversionsCountDayMin',
-        //     type: 'InputNumber',
-        //     value: (params) => <InputNumber placeholder="请输入最低今日转化数" style={{ width: '100%' }} {...params} />
-        // },
+        {
+            lable: '业务单元类型',
+            name: 'adUnitType',
+            type: 'Select',
+            value: (params) => <Select
+                placeholder='业务单元类型'
+                style={{ width: '100%', minWidth: 130 }}
+                showSearch
+                filterOption={(input: any, option: any) =>
+                    (option!.children as unknown as string).toLowerCase().includes(input.toLowerCase())
+                }
+                allowClear
+                {...params}
+                options={Object.keys(AdUnitType_Enum).map(key => ({ label: AdUnitType_Enum[key as keyof typeof AdUnitType_Enum], value: key }))}
+            />
+        },
         {
             lable: '最低总转化数',
             name: 'conversionsCountTotalMin',
@@ -470,6 +424,9 @@ const FilterQuery: React.FC<Props> = ({ onChange, initialValues, queryForm, setQ
                     case 'promotedObjectType':
                         value = PromotedObjectType[value as keyof typeof PromotedObjectType] || '请选择'
                         break
+                    case 'adUnitType':
+                        value = AdUnitType_Enum[value as keyof typeof AdUnitType_Enum] || '请选择'
+                        break
                     case 'adCreateTime': case 'putDate':
                         value = value ? '已经选择时间' : '请选择时间'
                         break

+ 2 - 0
src/pages/launchSystemV3/adMonitorListV3/config.ts

@@ -26,6 +26,7 @@ const planAdConfig = [
             { title: '广告状态', dataIndex: 'system_status', serverIndex: 'adgroup.system_status', label: '广告详情', default: 20, width: 100 },
             { title: '广告详情', dataIndex: 'cost_speed', label: '广告详情', default: 21, width: 80 },
             { title: '标记备注', dataIndex: 'tag_remark', label: '广告详情', width: 80, serverIndex: 'adgroup_user_tag.tag_remark' },
+            { title: '业务单元类型', dataIndex: 'ad_unit_type', label: '广告详情', width: 75, serverIndex: 'ad_account.ad_unit_type' },
             { title: '创意预览', dataIndex: 'creative_ids', serverIndex: 'adgroup_data.creative_ids', label: '广告详情', width: 130 },
             { title: '操作', dataIndex: 'cz', label: '广告详情', default: 22, width: 116 },
         ]
@@ -260,6 +261,7 @@ const dynamicConfig = [
             { title: '创意形式', dataIndex: 'creative_template_id', label: '基本信息', serverIndex: 'dynamic_creative.creative_template_id', default: 11, width: 90 },
             { title: '是否已删除', dataIndex: 'is_deleted', serverIndex: 'dynamic_creative.is_deleted', label: '基本信息', default: 12, width: 60 },
             { title: '创意预览', dataIndex: 'creative_components_json', serverIndex: 'dynamic_creative.creative_components_json', label: '基本信息', default: 20, width: 120 },
+            { title: '业务单元类型', dataIndex: 'ad_unit_type', label: '基本信息', width: 75, serverIndex: 'ad_account.ad_unit_type' },
             { title: '操作', dataIndex: 'cz', label: '基本信息', default: 21, width: 100 },
         ]
     },

+ 11 - 0
src/pages/launchSystemV3/adMonitorListV3/tableDynamicConfig.tsx

@@ -8,6 +8,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons'
 import { DELIVERY_MODE, DYNAMIC_CREATIVE_TYPE } from '../adqv3/const'
 import { creativeTemplate } from '../tencentAdPutIn/const'
 import CreativePreview from './CreativePreview'
+import { AdUnitType_Enum } from '@/pages/launchSystemNew/account/const'
 
 function tableDynamicConfig(
     suspendHandle: (b: any, suspend: '启动' | '暂停') => void
@@ -167,6 +168,16 @@ function tableDynamicConfig(
                 return <CreativePreview creativePreview={b?.creative_components ? [b?.creative_components] : []} deliveryMode={[b?.delivery_mode]} />
             }
         },
+        {
+            title: '业务单元类型',
+            dataIndex: 'ad_unit_type',
+            key: 'ad_unit_type',
+            align: 'center',
+            width: 75,
+            render: (a: string) => {
+                return AdUnitType_Enum[a as keyof typeof AdUnitType_Enum] || '--'
+            }
+        },
         {
             title: '操作',
             dataIndex: 'cz',

+ 11 - 0
src/pages/launchSystemV3/adMonitorListV3/tablePlanListConfig.tsx

@@ -11,6 +11,7 @@ import TimeSeriesLook from '../../launchSystemNew/adq/ad/timeSeriesLook'
 import SwitchStatus from '../adqv3/ad/switchStatus'
 import CreativePreview from './CreativePreview'
 import { ADGROUP_STATUS } from '@/pages/adMonitor/adMonitorList/data'
+import { AdUnitType_Enum } from '@/pages/launchSystemNew/account/const'
 function tablePlanConfig(
     onChange: () => void,
     details: (data: any) => void,
@@ -280,6 +281,16 @@ function tablePlanConfig(
                 return value || '--'
             },
         },
+        {
+            title: '业务单元类型',
+            dataIndex: 'ad_unit_type',
+            key: 'ad_unit_type',
+            align: 'center',
+            width: 75,
+            render: (a: string) => {
+                return AdUnitType_Enum[a as keyof typeof AdUnitType_Enum] || '--'
+            }
+        },
         {
             title: '广告详情',
             dataIndex: 'cost_speed',

+ 26 - 0
src/pages/launchSystemV3/material/cloudNew/const.ts

@@ -0,0 +1,26 @@
+import { DataNode } from "antd/lib/tree";
+
+/**
+ * 更新目录
+ * @param list 
+ * @param key 
+ * @param children 
+ * @returns 
+ */
+export const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] => {
+    return list.map(node => {
+        if (node.key === key) {
+            return {
+                ...node,
+                children,
+            };
+        }
+        if (node.children) {
+            return {
+                ...node,
+                children: updateTreeData(node.children, key, children),
+            };
+        }
+        return node;
+    });
+}

+ 17 - 5
src/pages/launchSystemV3/material/cloudNew/folder.tsx

@@ -18,7 +18,7 @@ interface Props {
 const Folder = forwardRef(({ loading, onLoadData }: Props, ref: Ref<FolderRef>) => {
 
     /******************************/
-    const { treeData, setExpandedKeys, expandedKeys, setBreadcrumdData, findParentKeys, loadedKeys } = useContext(DispatchCloudNew)!;
+    const { treeData, setSelectedKeys, selectedKeys, setBreadcrumdData, findParentKeys, loadedKeys, setBatchFolderVisible, handleUpdateFolder, expandedKeys, setExpandedKeys } = useContext(DispatchCloudNew)!;
 
     /******************************/
 
@@ -28,24 +28,34 @@ const Folder = forwardRef(({ loading, onLoadData }: Props, ref: Ref<FolderRef>)
         }
     }));
 
+    // 选中文件
     const onSelect: DirectoryTreeProps['onSelect'] = (keys, info) => {
         console.log('Trigger Expand', keys, info);
+        if (!info.selected) {
+            return
+        }
         if (keys?.length) {
             findParentKeys(keys?.[0] as string, treeData)
         } else {
             setBreadcrumdData([{ label: '全部素材', key: 0, currentKey: '0' }])
         }
-        setExpandedKeys(keys);
+        // 判断是否加载了下级文件  加载了 就不更新
+        if (!loadedKeys.includes(keys[0] as string))
+            handleUpdateFolder(keys[0] as string)
+        setBatchFolderVisible(false)
+        setSelectedKeys(keys);
     };
 
-
+    const onExpand = (expandedKeysValue: React.Key[]) => {
+        setExpandedKeys(expandedKeysValue);
+    };
 
     return <div className={style.folder}>
         <div className={style.top}>
             <Button
                 style={{ padding: 0, height: 'auto', width: 71, fontSize: 14 }}
                 onClick={() => {
-                    setExpandedKeys([])
+                    setSelectedKeys([])
                     setBreadcrumdData([{ label: '全部素材', key: 0, currentKey: '0' }])
                 }}
                 type="text"
@@ -57,11 +67,13 @@ const Folder = forwardRef(({ loading, onLoadData }: Props, ref: Ref<FolderRef>)
                 <Tree
                     showIcon
                     blockNode={true}
-                    selectedKeys={expandedKeys}
+                    selectedKeys={selectedKeys}
                     loadData={onLoadData}
                     onSelect={onSelect}
                     treeData={treeData}
                     loadedKeys={loadedKeys}
+                    onExpand={onExpand}
+                    expandedKeys={expandedKeys}
                 />
                 {!(treeData?.length > 0) && (loading ? <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: 200 }} ></div> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无文件夹" />)}
             </Spin>

+ 11 - 0
src/pages/launchSystemV3/material/cloudNew/global.less

@@ -0,0 +1,11 @@
+
+.content_global {
+    
+    >div {
+        height: 100%;
+
+        >div.ant-spin-container {
+            height: 100%;
+        }
+    }
+}

+ 36 - 10
src/pages/launchSystemV3/material/cloudNew/index.less

@@ -37,12 +37,15 @@
             border-bottom: 1px solid #e8eaec;
             padding: 8px 16px;
             flex: 0 1;
+            display: flex;
+            justify-content: space-between;
         }
 
         .left_bts {
             display: flex;
             flex-wrap: wrap;
             gap: 10px;
+            align-items: center;
         }
 
         .content {
@@ -50,14 +53,6 @@
             flex: 1 0;
             overflow: hidden;
             min-height: 100px;
-
-            >div {
-                height: 100%;
-
-                >div {
-                    height: 100%;
-                }
-            }
         }
 
         .content_scroll {
@@ -65,14 +60,16 @@
             overflow-y: auto;
             padding: 12px;
 
-            >div {
+            div.content_scroll_div {
                 display: flex;
                 // gap: 10px;
                 flex-wrap: wrap;
             }
         }
+
         .content_row {
             padding: 4px;
+
             >div {
                 border-radius: 6px;
             }
@@ -90,6 +87,13 @@
             border-top-left-radius: 6px;
             border-top-right-radius: 6px;
             text-align: center;
+            position: relative;
+
+            .checkbox {
+                position: absolute;
+                top: 6px;
+                left: 10px;
+            }
         }
 
         .body {
@@ -97,7 +101,7 @@
         }
 
         .actions {
-            border-top: 1px solid rgba(0, 0, 0, .06);
+            // border-top: 1px solid rgba(0, 0, 0, .06);
             padding: 4px 10px;
             display: flex;
             justify-content: space-between;
@@ -110,4 +114,26 @@
             padding: 8px 16px;
         }
     }
+}
+
+.file {
+    position: relative;
+    cursor: pointer;
+
+    >input {
+        opacity: 0;
+        position: absolute;
+        display: block;
+        top: 0;
+        left: 0;
+        cursor: pointer;
+        right: 0;
+        bottom: 0;
+        width: 100%;
+        font-size: 0;
+
+        >span {
+            display: none;
+        }
+    }
 }

+ 70 - 33
src/pages/launchSystemV3/material/cloudNew/index.tsx

@@ -2,13 +2,14 @@ import React, { useEffect, useRef, useState } from "react"
 import style from './index.less'
 import Folder from "./folder";
 import Material from "./material";
-import { Card } from "antd";
+import { Card, message, Modal } from "antd";
 import '../../tencentAdPutIn/index.less'
 import { DataNode, EventDataNode } from "antd/lib/tree";
-import { getFolderListApi } from "@/services/adqV3/cloudNew";
+import { delFolderApi, getFolderListApi } from "@/services/adqV3/cloudNew";
 import { useAjax } from "@/Hook/useAjax";
 import ManageFolder from "./manageFolder";
-import { FolderOpenOutlined, FolderOutlined } from "@ant-design/icons";
+import { ExclamationCircleOutlined, FolderOpenOutlined, FolderOutlined } from "@ant-design/icons";
+import { updateTreeData } from "./const";
 
 export const DispatchCloudNew = React.createContext<CLOUDNEW.CloudNewReactContent | null>(null);
 
@@ -21,21 +22,35 @@ const CloudNew: React.FC = () => {
     /**********************************/
     const refMaterial = useRef(null);
     const [treeData, setTreeData] = useState<DataNode[]>([]);
-    const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
+    const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
     const [queryFormFolder, setQueryFormFolder] = useState<CLOUDNEW.GetFolderListProps>({})
     const [folderVisible, setFolderVisible] = useState<boolean>(false)
     const [folderLoading, setFolderLoading] = useState<boolean>(false)
     const [breadcrumdData, setBreadcrumdData] = useState<CLOUDNEW.BreadcrumdData[]>([{ label: '全部素材', key: 0, currentKey: '0' }])
     const [loadedKeys, setLoadedKeys] = useState<string[]>([])  // 已经加载了的节点
     const [initialValuesFolder, setInitialValuesFolder] = useState<any>({})
+    const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]); // 目录树打开控制
+    const [folderCreateBy, setFolderCreateBy] = useState<number>()     // 获取最上层文件夹创建人
+    const [batchFolderVisible, setBatchFolderVisible] = useState<boolean>(false) // 批量操作文件夹控制
+    const [handleType, setHandleType] = useState<'folder' | 'file'>('file') // 操作类型
 
     const getFolderList = useAjax((params) => getFolderListApi(params))
+    const delFolderAjax = useAjax((params) => delFolderApi(params))
     /**********************************/
 
     useEffect(() => {
         handleGetFolder()
     }, [queryFormFolder?.folderName])
 
+    useEffect(() => {
+        if (breadcrumdData.length > 1) {
+            let folderId = breadcrumdData[1].key
+            setFolderCreateBy(getFolderList.data.find((item: { id: number; }) => item.id === folderId).createBy)
+        } else {
+            setFolderCreateBy(undefined)
+        }
+    }, [breadcrumdData, getFolderList.data])
+
     // 获取顶级文件夹列表
     const handleGetFolder = () => {
         setFolderLoading(() => true)
@@ -56,7 +71,8 @@ const CloudNew: React.FC = () => {
         const parentIdArrLength = parentIdArr.length
         const parentId = Number(parentIdArr[parentIdArrLength - 1])
         return getFolderListApi({ parentId }).then(res => {
-            setLoadedKeys(loadedkeys => [...loadedkeys, parentIdStr])
+            setLoadedKeys(loadedkeys => [...loadedkeys, parentIdStr].filter(item => !item.includes(parentIdStr + '-')))
+            setExpandedKeys(expandedKeys => [...expandedKeys, parentIdStr])
             setTreeData(origin =>
                 updateTreeData(origin, parentIdStr, res?.data?.map((item: { folderName: any; id: any; }) => ({
                     title: item.folderName,
@@ -67,25 +83,6 @@ const CloudNew: React.FC = () => {
         })
     }
 
-    // 更新目录
-    const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] => {
-        return list.map(node => {
-            if (node.key === key) {
-                return {
-                    ...node,
-                    children,
-                };
-            }
-            if (node.children) {
-                return {
-                    ...node,
-                    children: updateTreeData(node.children, key, children),
-                };
-            }
-            return node;
-        });
-    }
-
     // 查找所有菜单列表(m面包屑)
     const findParentKeys = (key: string, tree: DataNode[]) => {
         let newBreadcrumdData: CLOUDNEW.BreadcrumdData[] = [{ label: '全部素材', key: 0, currentKey: '0' }]
@@ -107,8 +104,32 @@ const CloudNew: React.FC = () => {
         setBreadcrumdData(newBreadcrumdData)
     };
 
-    const delFolder = (id: number) => {
-
+    // 删除文件夹
+    const delFolder = (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) => {
+                    delFolderAjax.run(id).then(res => {
+                        if (res) {
+                            message.success('删除成功');
+                            (refMaterial.current as any).folderRefresh()
+                            if (selectedKeys[0] === '0' || !selectedKeys[0]) {
+                                setTreeData(data => data.filter(item => item.key !== ('0-' + id)))
+                            } else {
+                                handleUpdateFolder(selectedKeys[0] as string)
+                            }
+                            resolve('')
+                        }
+                    })
+                })
+            }
+        });
     }
 
     return <Card
@@ -120,11 +141,15 @@ const CloudNew: React.FC = () => {
         <div className={style.cloudNew}>
             <DispatchCloudNew.Provider value={{
                 treeData, setTreeData,
-                expandedKeys, setExpandedKeys,
+                selectedKeys, setSelectedKeys,
                 queryFormFolder, setQueryFormFolder,
                 breadcrumdData, setBreadcrumdData,
                 loadedKeys, setLoadedKeys,
-                findParentKeys
+                findParentKeys, handleUpdateFolder,
+                expandedKeys, setExpandedKeys,
+                folderCreateBy, setFolderCreateBy,
+                batchFolderVisible, setBatchFolderVisible,
+                handleType, setHandleType
             }}>
                 {/* 文件夹 */}
                 <Folder
@@ -155,19 +180,31 @@ const CloudNew: React.FC = () => {
         {/* 文件夹新增修改 */}
         {folderVisible && <ManageFolder
             initialValues={initialValuesFolder}
-            parentIdStr={expandedKeys?.[0] as string || '0'}
+            parentIdStr={selectedKeys?.[0] as string || '0'}
             visible={folderVisible}
             onClose={() => {
                 setFolderVisible(false)
             }}
             onChange={(newData) => {
                 setFolderVisible(false)
-                if (expandedKeys?.[0]) {
+                if (initialValuesFolder?.id) {
                     (refMaterial.current as any).folderRefresh()
-                    handleUpdateFolder(expandedKeys[0] as string)
+                    if (selectedKeys?.[0]) {
+                        handleUpdateFolder(selectedKeys[0] as string)
+                    } else {
+                        setTreeData(data => data.map(item => {
+                            return item.key === ('0-' + newData.id) ? { ...item, title: newData.folderName } : item
+                        }))
+                    }
+                    setInitialValuesFolder(undefined)
                 } else {
-                    // handleGetFolder()
-                    setTreeData(data => [...data, { title: newData.folderName, key: '0-' + newData.id, icon: ({ selected }: any) => (selected ? <FolderOpenOutlined style={{ color: 'rgb(255, 202, 40)' }} /> : <FolderOutlined style={{ color: 'rgb(255, 202, 40)' }} />) }])
+                    if (selectedKeys?.[0]) {
+                        (refMaterial.current as any).folderRefresh()
+                        handleUpdateFolder(selectedKeys[0] as string)
+                    } else {
+                        // handleGetFolder()
+                        setTreeData(data => [...data, { title: newData.folderName, key: '0-' + newData.id, icon: ({ selected }: any) => (selected ? <FolderOpenOutlined style={{ color: 'rgb(255, 202, 40)' }} /> : <FolderOutlined style={{ color: 'rgb(255, 202, 40)' }} />) }])
+                    }
                 }
             }}
         />}

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

@@ -3,7 +3,7 @@ import React, { useEffect } from "react"
 import '../../tencentAdPutIn/index.less'
 import { useAjax } from "@/Hook/useAjax"
 import { getUserAllApi } from "@/services/operating/account"
-import { addFolderApi, updateFolderApi } from "@/services/adqV3/cloudNew"
+import { addFolderApi, getFolderDetailsApi, updateFolderApi } from "@/services/adqV3/cloudNew"
 
 interface Props {
     parentIdStr: string
@@ -27,8 +27,19 @@ const ManageFolder: React.FC<Props> = ({ parentIdStr = '0', visible, onChange, o
     const getUserAll = useAjax(() => getUserAllApi())
     const addFolder = useAjax((params) => addFolderApi(params))
     const updateFolder = useAjax((params) => updateFolderApi(params))
+    const getFolderDetails = useAjax((params) => getFolderDetailsApi(params))
     /************************************/
 
+    useEffect(() => {
+        if (initialValues?.id) {
+            getFolderDetails.run(initialValues.id).then(res => {
+                if (res) {
+                    form.setFieldsValue({ ...res, permissionUserId: res.permissionUserId || undefined })
+                }
+            })
+        }
+    }, [initialValues?.id])
+
     useEffect(() => {
         getUserAll.run()
     }, [])
@@ -82,7 +93,7 @@ const ManageFolder: React.FC<Props> = ({ parentIdStr = '0', visible, onChange, o
                 message.error(errorFields?.[0]?.errors?.[0])
             }}
             onFinish={handleOk}
-            initialValues={initialValues ? initialValues :{
+            initialValues={{
                 isPublic: true
             }}
         >

+ 207 - 53
src/pages/launchSystemV3/material/cloudNew/material.tsx

@@ -1,17 +1,23 @@
 import React, { forwardRef, Ref, useContext, useEffect, useImperativeHandle, useRef, useState } from "react"
 import style from './index.less'
-import { Breadcrumb, Button, Card, Dropdown, Pagination, Spin, Typography } from "antd"
+import { Breadcrumb, Button, Card, Checkbox, Dropdown, Pagination, Radio, Spin, Typography } from "antd"
 import { DispatchCloudNew } from "."
 import { getFolderListApi } from "@/services/adqV3/cloudNew"
 import { useAjax } from "@/Hook/useAjax"
 import { useSize } from "ahooks"
-const { Text, Title } = Typography;
+import './global.less'
+import { useModel } from "umi"
+import { CheckboxValueType } from "antd/lib/checkbox/Group"
+import MoveFile from "./moveFile"
+import { ItemType } from "antd/lib/menu/hooks/useItems"
+import UploadFile from "./uploadFile"
+const { Text } = Typography;
 
 interface Props {
     /** 新增文件夹 */
     onAddFolder?: () => void
     onUpdateFolder?: (data: any) => void
-    onDelFolder?: (id: number) => void
+    onDelFolder?: (id: number, name: string) => void
 }
 
 interface MaterialRef {
@@ -24,11 +30,19 @@ interface MaterialRef {
 const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props, ref1: Ref<MaterialRef>) => {
 
     /********************************/
-    const { breadcrumdData, setExpandedKeys, setBreadcrumdData, expandedKeys } = useContext(DispatchCloudNew)!;
+    const { initialState } = useModel('@@initialState');
+    const { breadcrumdData, setSelectedKeys, setBreadcrumdData, 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, description?: string }[]>([])
+    const [folderList, setFolderList] = useState<{ id: number, folderName: string, createBy: number, description?: string }[]>([])
     const [rowNum, setRowNum] = useState<number>(0)
+    const [checkedFolderList, setCheckedFolderList] = useState<CheckboxValueType[]>([])
+    const [checkFolderAll, setCheckFolderAll] = useState<boolean>(false);
+    const [indeterminateFolder, setIndeterminateFolder] = useState<boolean>(false);
+    const [moveVisible, setMoveVisible] = useState<boolean>(false)
+    const [moveType, setMoveType] = useState<'folder' | 'file'>('folder')
+    const [checkedList, setCheckedList] = useState<CheckboxValueType[]>([])
+    const [uploadVisible, setUploadVisible] = useState<boolean>(false)
 
     const getFolderList = useAjax((params) => getFolderListApi(params))
     /********************************/
@@ -43,88 +57,228 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
     // 根据内容宽度计算列数
     useEffect(() => {
         if (size?.width) {
-            let rowNum = Math.floor((size?.width - 26) / 240)
+            let rowNum = Math.floor((size?.width - 26) / 200)
             setRowNum(rowNum || 1)
         }
     }, [size?.width])
 
     useEffect(() => {
-        getFolder()
-    }, [expandedKeys])
+        if (isShowFolder()) {
+            // 文件夹
+            getFolder()
+        } else {
+            // 文件
+        }
+    }, [selectedKeys, handleType])
 
     /** 获取下级文件夹 */
     const getFolder = () => {
-        if (expandedKeys?.length) {
-            const parentIdArr = (expandedKeys[0] as string).split('-')
+        let parentId: number | undefined;
+        if (selectedKeys?.length) {
+            const parentIdArr = (selectedKeys[0] as string).split('-')
             const parentIdArrLength = parentIdArr.length
-            const parentId = Number(parentIdArr[parentIdArrLength - 1])
-            getFolderList.run({ parentId }).then(res => {
-                setFolderList(res || [])
-            })
+            parentId = Number(parentIdArr[parentIdArrLength - 1])
+        }
+        getFolderList.run({ parentId }).then(res => {
+            setFolderList(() => res || [])
+        })
+    }
+
+    // 文件夹选择
+    const onCheckboxChange = (checkedValues: CheckboxValueType[]) => {
+        setCheckedFolderList(checkedValues)
+        setIndeterminateFolder(!!checkedValues.length && checkedValues.length < folderList.length);
+        setCheckFolderAll(checkedValues.length === folderList.length);
+    };
+
+    // 取消选择
+    const cancelSelect = () => {
+        setCheckFolderAll(false)
+        setCheckedFolderList([])
+        setIndeterminateFolder(false)
+        setCheckedList([])
+    }
+
+    // 是否管理员
+    const isAdmin = () => {
+        return ['999'].includes(initialState?.currentUser?.powerLevel?.toString() || '')
+    }
+
+    // 是否有权限
+    const isPermission = (createBy: any) => {
+        return initialState?.currentUser?.userId?.toString() === createBy.toString()
+    }
+
+    const getItems = (item: any) => {
+        let data: ItemType[] = []
+        if (isPermission(item.createBy)) {
+            data.push({ label: '编辑', style: { fontSize: 12 }, key: 'edit', onClick: () => onUpdateFolder?.(item) })
+            if (!!selectedKeys?.[0]) {
+                data.push({
+                    label: '移动', style: { fontSize: 12 }, key: 'move', onClick: () => {
+                        setCheckedList([item.id])
+                        setMoveType('folder')
+                        setMoveVisible(true)
+                    }
+                })
+            } else if (isAdmin()) {
+                data.push({
+                    label: '修改文件夹所属人', style: { fontSize: 12 }, key: 'createBy', onClick: () => {
+
+                    }
+                })
+            }
+            data.push({ label: <span style={{ color: 'red', fontSize: 12 }}>删除</span>, key: 'del', onClick: () => onDelFolder?.(item.id, item?.folderName) })
         } else {
-            setFolderList([])
+            if (isAdmin() && !selectedKeys?.[0]) {
+                data.push({
+                    label: '修改文件夹所属人', style: { fontSize: 12 }, key: 'createBy', onClick: () => {
+
+                    }
+                })
+            }
         }
+        return data
+    }
+
+    // 是否展示文件夹
+    const isShowFolder = () => {
+        return !selectedKeys?.[0] || handleType === 'folder'
     }
 
     return <div className={style.material}>
         <div className={style.operates}>
             <div className={style.left_bts}>
-                <Button type="primary">上传素材</Button>
-                <Button onClick={() => onAddFolder?.()}>新建文件夹</Button>
+                {!!selectedKeys?.[0] && <Checkbox
+                    onChange={(e) => {
+                        setHandleType(e.target.checked ? 'folder' : 'file')
+                    }}
+                    checked={handleType === 'folder'}
+                ><span style={{ fontSize: 12 }}>是否操作文件夹</span></Checkbox>}
+                {(!selectedKeys?.[0] || handleType === 'folder') && <Button onClick={() => onAddFolder?.()} disabled={folderCreateBy ? !isPermission(folderCreateBy) : false}>新建文件夹</Button>}
+                {handleType === 'folder' ? <>
+                    {!!selectedKeys?.[0] && <Button onClick={() => setBatchFolderVisible(true)} disabled={folderCreateBy ? !isPermission(folderCreateBy) : false}>批量操作文件夹</Button>}
+                </> : <>
+                    {!!selectedKeys?.[0] && <Button type="primary" disabled={folderCreateBy ? !isPermission(folderCreateBy) : false} onClick={() => setUploadVisible(true)}>上传素材</Button>}
+                </>}
             </div>
         </div>
-        <div className={style.operates}>
+        {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>
+            </div>
+            <div className={style.left_bts}>
+                <Button disabled={checkedFolderList?.length === 0} onClick={() => {
+                    setCheckedList(checkedFolderList)
+                    setMoveVisible(true)
+                    setMoveType('folder')
+                }}>移动文件夹</Button>
+                <Button type="primary" onClick={() => {
+                    setBatchFolderVisible(false)
+                }}>完成</Button>
+            </div>
+        </div> : <div className={style.operates}>
             <Breadcrumb>
                 {breadcrumdData.map((item, index) => <Breadcrumb.Item key={item.key}>
                     {breadcrumdData.length !== index + 1 ? <a
                         style={{ color: '#1890ff' }}
                         onClick={() => {
                             setBreadcrumdData(data => data.splice(0, index + 1))
-                            setExpandedKeys([item.currentKey])
+                            setSelectedKeys(item.currentKey === '0' ? [] : [item.currentKey])
                         }}
                     >{item.label}</a> : item.label}
                 </Breadcrumb.Item>)}
             </Breadcrumb>
-        </div>
-        <div className={style.content}>
+        </div>}
+        <div className={`${style.content} content_global`}>
             <Spin spinning={getFolderList.loading}>
                 <div className={style.content_scroll} ref={ref}>
-                    <div>
-                        {folderList.map((item, index) => <div key={index} className={style.content_row} style={{ width: rowNum ? (1 / rowNum * 100) + '%' : 240 }}>
-                            <Card
-                                hoverable
-                                bodyStyle={{ padding: 0 }}
-                                className={`${style.content_col}`}
-                                cover={<div style={{ height: 150 }} className={style.content_cover}>
-                                    <img src={require('../../../../../public/file.png')} height={'100%'} alt="" />
-                                </div>}
-                            >
-                                <div className={style.body}>
-                                    <Text ellipsis>{item?.folderName}</Text>
-                                </div>
-                                <div className={style.actions}>
-                                    <div></div>
-                                    <Dropdown menu={{
-                                        items: [
-                                            { label: '编辑', key: 'edit', onClick: () => onUpdateFolder?.(item) },
-                                            { label: <span style={{ color: 'red' }}>删除</span>, key: 'del', onClick: () => onDelFolder?.(item.id) }
-                                        ]
-                                    }}>
-                                        <a onClick={e => e.preventDefault()} style={{ fontSize: 12 }}>
-                                            更多
-                                        </a>
-                                    </Dropdown>
-                                </div>
-                            </Card>
-                        </div>)}
-
-                    </div>
+                    <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 }}>
+                                <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>}
+                                        <img src={require('../../../../../public/file.png')} height={'100%'} alt="" />
+                                    </div>}
+                                    onDoubleClick={() => {
+                                        if (!batchFolderVisible) {
+                                            let newExpandedKeys = '0-' + item.id
+                                            if (selectedKeys?.[0]) {
+                                                newExpandedKeys = selectedKeys[0] + '-' + item.id
+                                            }
+                                            findParentKeys(newExpandedKeys, treeData)
+                                            setSelectedKeys([newExpandedKeys])
+                                            // 判断是否加载了下级文件  加载了 就不更新
+                                            if (!loadedKeys.includes(newExpandedKeys))
+                                                handleUpdateFolder(newExpandedKeys)
+                                        }
+                                    }}
+                                >
+                                    <div className={style.body}>
+                                        <Text ellipsis>{item?.folderName}</Text>
+                                    </div>
+                                    <div className={style.actions}>
+                                        <div style={{ height: 22 }}></div>
+                                        {isPermission(item.createBy) || (!selectedKeys?.[0] && isAdmin()) ? <Dropdown menu={{
+                                            items: getItems(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>
-        <div className={style.fotter}>
+        {handleType === 'file' && <div className={style.fotter}>
             <Pagination size="small" total={50} showSizeChanger showQuickJumper />
-        </div>
+        </div>}
+
+        {/* 移动文件 */}
+        {moveVisible && <MoveFile
+            moveType={moveType}
+            checkedList={checkedList}
+            userId={initialState?.currentUser?.userId?.toString() || ''}
+            visible={moveVisible}
+            onClose={() => {
+                setMoveVisible(false)
+                cancelSelect()
+            }}
+            onChange={(selectedKey: string) => {
+                getFolder()
+                setMoveVisible(false)
+                cancelSelect()
+                handleUpdateFolder(selectedKeys[0] as string)
+                if (loadedKeys.includes(selectedKey)) {
+                    handleUpdateFolder(selectedKey)
+                }
+                setBatchFolderVisible(false)
+            }}
+        />}
+        
+        {/* 上传文件 */}
+        {uploadVisible && <UploadFile
+            visible={uploadVisible}
+            onChange={() => {
+
+            }}
+            onClose={() => {
+                setUploadVisible(false)
+            }}
+        />}
     </div>
 })
 

+ 114 - 0
src/pages/launchSystemV3/material/cloudNew/moveFile.tsx

@@ -0,0 +1,114 @@
+import { useAjax } from "@/Hook/useAjax"
+import { getFolderListApi, moveFolderApi } 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"
+import React, { useEffect, useState } from "react"
+import { updateTreeData } from "./const"
+import '../../tencentAdPutIn/index.less'
+import { CheckboxValueType } from "antd/lib/checkbox/Group"
+
+interface Props {
+    moveType: 'folder' | 'file'
+    checkedList: CheckboxValueType[]
+    userId: string,
+    visible?: boolean
+    onChange?: (selectedKey: string) => void
+    onClose?: () => void
+}
+const MoveFile: React.FC<Props> = ({ moveType, checkedList, visible, userId, onChange, onClose }) => {
+
+    /**********************************/
+    const [treeData, setTreeData] = useState<DataNode[]>([]);
+    const [folderLoading, setFolderLoading] = useState<boolean>(false)
+    const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([])
+
+    const getFolderList = useAjax((params) => getFolderListApi(params))
+    const moveFolder = useAjax((params) => moveFolderApi(params))
+    /**********************************/
+
+    useEffect(() => {
+        handleGetFolder()
+    }, [])
+
+    // 获取顶级文件夹列表
+    const handleGetFolder = () => {
+        setFolderLoading(() => true)
+        getFolderList.run({}).then(res => {
+            setFolderLoading(() => false)
+            setTreeData(() => res?.map((item: { folderName: any; id: any; createBy: number }) => ({
+                title: item.folderName,
+                key: '0-' + item.id,
+                disabled: userId !== item.createBy.toString(),
+                icon: ({ selected }: any) => (selected ? <FolderOpenOutlined style={{ color: 'rgb(255, 202, 40)' }} /> : <FolderOutlined style={{ color: 'rgb(255, 202, 40)' }} />),
+            })) || [])
+        }).catch(() => setFolderLoading(false))
+    }
+
+    // 下级目录
+    const handleUpdateFolder = (parentIdStr: string) => {
+        const parentIdArr = parentIdStr.split('-')
+        const parentIdArrLength = parentIdArr.length
+        const parentId = Number(parentIdArr[parentIdArrLength - 1])
+        return getFolderListApi({ parentId }).then(res => {
+            setTreeData(origin =>
+                updateTreeData(origin, parentIdStr, res?.data?.map((item: { folderName: any; id: any; createBy: number }) => ({
+                    title: item.folderName,
+                    key: parentIdStr + '-' + item.id,
+                    disabled: userId !== item.createBy.toString(),
+                    icon: ({ selected }: any) => (selected ? <FolderOpenOutlined /> : <FolderOutlined />)
+                })) || []),
+            );
+        })
+    }
+
+    const handleOk = () => {
+        if (selectedKeys?.length) {
+            if (moveType === 'folder') {
+                const parentIdArr = (selectedKeys[0] as string).split('-')
+                const parentIdArrLength = parentIdArr.length
+                const parentId = Number(parentIdArr[parentIdArrLength - 1])
+                moveFolder.run({ targetId: parentId, sourceId: checkedList }).then(res => {
+                    if (res) {
+                        message.success('移动成功')
+                        onChange?.(selectedKeys[0] as string)
+                    }
+                })
+            }
+        } else {
+            message.error('请选择文件夹')
+        }
+    }
+
+    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}
+        okText="移动"
+        confirmLoading={moveFolder.loading}
+    >
+        <Card className="cardResetCss" title={<strong style={{ fontSize: 12 }}>文件夹列表</strong>}>
+            <Spin spinning={folderLoading}>
+                <Tree
+                    showIcon
+                    blockNode={true}
+                    selectedKeys={selectedKeys}
+                    loadData={(treeNode: EventDataNode<DataNode>) => {
+                        return new Promise<void>(async (resolve) => {
+                            await handleUpdateFolder(treeNode.key as string)
+                            resolve()
+                        })
+                    }}
+                    onSelect={(keys) => setSelectedKeys(keys)}
+                    treeData={treeData}
+                />
+            </Spin>
+        </Card>
+    </Modal>
+}
+
+export default React.memo(MoveFile)

+ 178 - 0
src/pages/launchSystemV3/material/cloudNew/uploadFile.tsx

@@ -0,0 +1,178 @@
+import { Button, Card, Form, Input, message, Modal, Radio, Select, Space, Upload } from "antd"
+import React, { useEffect, useMemo, useState } from "react"
+import '../../tencentAdPutIn/index.less'
+import { useAjax } from "@/Hook/useAjax"
+import { getUserAllApi } from "@/services/operating/account"
+import { RcFile } from "antd/lib/upload"
+import { UploadOutlined } from "@ant-design/icons"
+import style from './index.less'
+import CropperImg from "@/components/FileBoxAD/components/Cropper"
+
+interface Props {
+    visible?: boolean
+    onChange?: () => void
+    onClose?: () => void
+}
+/**
+ * 上传素材
+ * @returns 
+ */
+const UploadFile: React.FC<Props> = ({ visible, onClose }) => {
+
+    /**********************************/
+    const [queryForm, setQueryForm] = useState<CLOUDNEW.AddMaterialProps>({ materialType: 'image' })
+    const [fileList, setFileList] = useState<any>([])
+    const [fileUrl, setFileUrl] = useState<string>('')
+    const [previewVisible, setPreviewVisible] = useState<boolean>(false)
+    const [visibleCropper, setVisibleCropper] = useState<boolean>(false)
+
+    const getUserAll = useAjax(() => getUserAllApi())
+    /**********************************/
+
+    useEffect(() => {
+        getUserAll.run()
+    }, [])
+
+    const handleOk = () => {
+
+    }
+
+    const getVideo = useMemo(() => {
+        if (queryForm?.materialType === 'video') {
+            if (queryForm?.file) {
+                return <video src={URL.createObjectURL(queryForm?.file)} style={{ width: 200 }} controls />
+            } else {
+                return null
+            }
+        } else {
+            return null
+        }
+    }, [queryForm?.file])
+
+    return <Modal
+        title={<strong>{'上传素材'}</strong>}
+        open={visible}
+        onCancel={onClose}
+        className="modalResetCss"
+        bodyStyle={{ backgroundColor: '#f1f4fc', maxHeight: 650, overflow: 'hidden', overflowY: 'auto', padding: '10px' }}
+        maskClosable={false}
+        width={600}
+    >
+        <Card className="cardResetCss">
+            <Form
+                labelAlign='left'
+                labelCol={{ span: 4 }}
+                layout="horizontal"
+                colon={false}
+                style={{}}
+            >
+                <Form.Item label={<strong>素材名称</strong>}>
+                    <Input placeholder="素材名称(不填是上传素材名称)" value={queryForm?.materialName} onChange={(e) => setQueryForm({ ...queryForm, materialName: e.target.value })} />
+                </Form.Item>
+
+                <Form.Item label={<strong>设计师</strong>}>
+                    <Select
+                        placeholder="请选择设计师(不选默认自己)"
+                        filterOption={(input, option) =>
+                            ((option?.children ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                        }
+                        value={queryForm?.designerId}
+                        onChange={(e => setQueryForm({ ...queryForm, designerId: e }))}
+                        options={getUserAll?.data?.map((item: { nickname: any; userId: any }) => ({ label: item.nickname, value: item.userId }))}
+                    />
+                </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>
+                        <Radio.Button value="video">视频</Radio.Button>
+                    </Radio.Group>
+                </Form.Item>
+
+                <Form.Item label={<strong>描述</strong>}>
+                    <Input.TextArea placeholder="描述" value={queryForm?.description} onChange={(e) => setQueryForm({ ...queryForm, description: e.target.value })} />
+                </Form.Item>
+
+                <Form.Item label={<strong>{'上传图片'}</strong>}>
+                    {queryForm?.materialType === 'image' ? <Space>
+                        <Upload
+                            listType="picture-card"
+                            accept='image/gif,image/jpeg,image/png,image/jpg'
+                            beforeUpload={(file: RcFile) => {
+                                return false
+                            }}
+                            fileList={fileList}
+                            onChange={(newFileList: any) => {
+                                console.log('newFileList-->', newFileList)
+                                setQueryForm({ ...queryForm, file: newFileList.file })
+                                setFileList([...newFileList.fileList])
+                            }}
+                            onPreview={(file: any) => {
+                                setPreviewVisible(true)
+                                setFileUrl(file.thumbUrl)
+                            }}
+                            onRemove={() => {
+                                setQueryForm({ ...queryForm, file: null })
+                            }}
+                        >
+                            {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>
+                            <input type='file' onChange={(e) => {
+                                if (e?.target?.files) {
+                                    setQueryForm({ ...queryForm, file: e?.target?.files[0] })
+                                }
+                            }} accept="video/*" />
+                        </div>
+                        {queryForm?.file && <>
+                            <p>{queryForm?.file?.name}</p>
+                            {getVideo}
+                        </>}
+                    </>}
+
+                </Form.Item>
+            </Form>
+        </Card>
+
+        {/* 预览 */}
+        <Modal
+            open={previewVisible}
+            footer={null}
+            onCancel={() => {
+                setPreviewVisible(false)
+            }}
+        >
+            <img alt="example" style={{ width: '100%' }} src={fileUrl} />
+        </Modal>
+
+        {/* 裁剪 */}
+        {visibleCropper && <CropperImg visible={visible} onClose={() => setVisibleCropper(false)} file={queryForm.file} onChange={(fileList: any[], file: any) => { setFileList(fileList); setQueryForm({ ...queryForm, file: file }); setVisibleCropper(false) }} />}
+    </Modal>
+}
+
+export default React.memo(UploadFile)

+ 23 - 13
src/pages/launchSystemV3/material/typings.d.ts

@@ -2,15 +2,24 @@ declare namespace CLOUDNEW {
     interface CloudNewReactContent {
         treeData: DataNode[]
         setTreeData: React.Dispatch<React.SetStateAction<DataNode[]>>
-        expandedKeys: React.Key[]
-        setExpandedKeys: React.Dispatch<React.SetStateAction<React.Key[]>>
+        selectedKeys: React.Key[]
+        setSelectedKeys: React.Dispatch<React.SetStateAction<React.Key[]>>
         queryFormFolder: CLOUDNEW.GetFolderListProps
         setQueryFormFolder: React.Dispatch<React.SetStateAction<CLOUDNEW.GetFolderListProps>>
         breadcrumdData: CLOUDNEW.BreadcrumdData[]
         setBreadcrumdData: React.Dispatch<React.SetStateAction<CLOUDNEW.BreadcrumdData[]>>
         loadedKeys: string[]
         setLoadedKeys: React.Dispatch<React.SetStateAction<string[]>>
-        findParentKeys: (key: string, tree: DataNode[]) => void
+        findParentKeys: (key: string, tree: DataNode[]) => void,
+        handleUpdateFolder: (parentIdStr: string) => Promise<void>
+        expandedKeys: React.Key[]
+        setExpandedKeys: React.Dispatch<React.SetStateAction<React.Key[]>>
+        folderCreateBy: number | undefined
+        setFolderCreateBy: React.Dispatch<React.SetStateAction<number | undefined>>
+        batchFolderVisible: boolean
+        setBatchFolderVisible: React.Dispatch<React.SetStateAction<boolean>>
+        handleType: "folder" | "file"
+        setHandleType: React.Dispatch<React.SetStateAction<"folder" | "file">>
     }
     interface BreadcrumdData {
         label: string
@@ -40,19 +49,20 @@ declare namespace CLOUDNEW {
         materialType?: string   // 素材类型
     }
     interface AddMaterialProps {
-        designerId: number,    // 设计师
-        fileMime: string,  
-        fileSize: number,
-        height: number,
-        width: number,
-        materialName: string,  // 素材名称
-        materialType: string,  // 素材类型
-        md5: string,           
-        ossUrl: string,        // 链接地址
+        designerId?: number,    // 设计师
+        fileMime?: string,  
+        fileSize?: number,
+        height?: number,
+        width?: number,
+        materialName?: string,  // 素材名称
+        materialType?: string,  // 素材类型
+        md5?: string,           
+        ossUrl?: string,        // 链接地址
         folderId?: number,     // 文件夹ID  
         aspectRatio?: string,  // 素材比率
         description?: string,  // 描述
-        videoDuration?: number // 视频时长
+        videoDuration?: number, // 视频时长
+        file?: any
     }
     interface UpdateMaterialProps {
         folderId?: number,     // 文件夹ID  

+ 2 - 2
src/pages/launchSystemV3/tencentAdPutIn/create/Target/addTarget.tsx

@@ -483,7 +483,7 @@ const AddTarget: React.FC<Props> = ({ isBackVal, value, visible, onChange, onClo
                         </Radio.Group>
                     </Form.Item>
                     {ageType === '1' && <div className={`${style.newSpace_bottom} flexStart`} style={{ '--g': '5px' } as React.CSSProperties}>
-                        <Form.Item name={['age', 'min']}>
+                        <Form.Item name={['age', 'min']} style={{ marginBottom: 0 }}>
                             <Select style={{ width: 185 }} placeholder="请选择">
                                 {Array(66 - 13).fill('').map((_, i) => i + 14).filter(i => i !== 15 && i !== 16 && i !== 17).map(i => {
                                     return <Select.Option disabled={i > max} value={i} key={i}>{i === 66 ? '66 岁及以上' : i + ' 岁'}</Select.Option>
@@ -491,7 +491,7 @@ const AddTarget: React.FC<Props> = ({ isBackVal, value, visible, onChange, onClo
                             </Select>
                         </Form.Item>
                         <span>-</span>
-                        <Form.Item name={['age', 'max']}>
+                        <Form.Item name={['age', 'max']} style={{ marginBottom: 0 }}>
                             <Select style={{ width: 185 }} placeholder="请选择">
                                 {Array(66 - 17).fill('').map((_, i) => {
                                     return <Select.Option disabled={i + 18 < min} value={i + 18} key={i + 18}>{i + 18 === 66 ? '66 岁及以上' : i + 18 + ' 岁'}</Select.Option>

+ 139 - 12
src/pages/launchSystemV3/tencentAdPutIn/create/Target/generateTarget.tsx

@@ -1,5 +1,5 @@
 import { Button, Card, Form, InputNumber, Modal, Select, Space, message } from "antd"
-import React from "react"
+import React, { useEffect, useState } from "react"
 import '../../index.less'
 import { getRandomElements } from "@/utils/utils"
 import { REGION_DATA } from "./const"
@@ -10,30 +10,64 @@ interface Props {
     onClose?: () => void
     onChange?: (value: any) => void
 }
+interface Region {
+    title: string;
+    value: number;
+    key: number;
+    parentId?: number;
+    disabled?: boolean;
+    children?: Region[];
+}
+// 年龄前驱
+const START_AGE = Array(66 - 13).fill('').map((_, i) => i + 14).filter(i => i !== 15 && i !== 16 && i !== 17)
+// 年龄后驱
+const END_AGE = Array(66 - 17).fill('')
+/**
+ * 一键生成
+ * @param param0 
+ * @returns 
+ */
 const GenerateTarget: React.FC<Props> = ({ target, visible, onChange, onClose }) => {
 
     /*********************************/
     const [form] = Form.useForm();
+    const startAgeMax = Form.useWatch('startAgeMax', form)
+    const startAgeMin = Form.useWatch('startAgeMin', form)
+    const defaultStartAge = Form.useWatch('defaultStartAge', form)
+    const endAgeMax = Form.useWatch('endAgeMax', form)
+    const endAgeMin = Form.useWatch('endAgeMin', form)
+    const defaultEndAge = Form.useWatch('defaultEndAge', form)
 
+    const [startAgeList, setStartAgeList] = useState<number[]>([])
+    const [endAgeList, setEndAgeList] = useState<number[]>([])
     /*********************************/
 
+    useEffect(() => {
+        if (typeof startAgeMin === 'number' && typeof startAgeMax === 'number') {
+            let startAgeList = Array(startAgeMin === 14 ? startAgeMax - startAgeMin + 1 : startAgeMax - startAgeMin + 1).fill('').map((_, index) => startAgeMin + index).filter(i => i !== 15 && i !== 16 && i !== 17 && i !== defaultStartAge)
+            setStartAgeList(startAgeList)
+        } else {
+            setStartAgeList([])
+        }
+    }, [startAgeMin, startAgeMax, defaultStartAge])
+
+    useEffect(() => {
+        if (typeof endAgeMin === 'number' && typeof endAgeMax === 'number') {
+            let endAgeList = Array(endAgeMax - endAgeMin + 1).fill('').map((_, index) => endAgeMin + index).filter(item => item !== defaultEndAge)
+            setEndAgeList(endAgeList)
+        } else {
+            setEndAgeList([])
+        }
+    }, [endAgeMax, endAgeMin, defaultEndAge])
+
     const handleOk = (values: any) => {
         const zhongguo = [156, 540000, 630000, 510000, 450000, 320000, 220000, 370000, 340000, 150000, 140000, 420000, 130000, 360000, 310000, 330000, 650000, 350000, 120000, 110000, 640000, 530000, 210000, 610000, 520000, 230000, 460000, 440000, 500000, 410000, 620000, 430000]
         const locationTypes = ["LIVE_IN"]
-        const { regions, count } = values
+        const { regions, count, defaultStartAge, defaultEndAge } = values
 
         let regionsData = REGION_DATA.find(item => item.value === regions)
         let children = regionsData?.children || []
 
-        interface Region {
-            title: string;
-            value: number;
-            key: number;
-            parentId?: number;
-            disabled?: boolean;
-            children?: Region[];
-        }
-
         function getRegionPaths(region: Region, parentPath: string = ''): string[] {
             const currentPath = parentPath ? `${parentPath},${region.value}` : `${region.value}`;
             const paths = [];
@@ -111,6 +145,32 @@ const GenerateTarget: React.FC<Props> = ({ target, visible, onChange, onClose })
         } else {
             data = data.concat(getTarget(count, regionsList))
         }
+        data = data.map(item => {
+            const { targeting, id, createTime, updateTime, createBy, ...data } = item
+            let age: { min: number, max: number }
+            if (startAgeList.length || endAgeList.length) {
+                if (Math.random() > 0.5 && startAgeList.length > 0) {
+                    age = { min: startAgeList.pop() as number, max: defaultEndAge }
+                } else if (endAgeList.length > 0) {
+                    age = { min: defaultStartAge, max: endAgeList.pop() as number }
+                } else {
+                    if (startAgeList.length > 0) {
+                        age = { min: startAgeList.pop() as number, max: defaultEndAge }
+                    } else {
+                        age = { min: defaultStartAge, max: endAgeList.pop() as number }
+                    }
+                }
+                data.targetingName = data.targetingName.replace('年龄', '')
+                data.targetingName += `+年龄${age.min}岁至${age.max === 66 ? '66岁及以上' : age.max + '岁'}`
+                let newTargeting = {
+                    ...targeting,
+                    age: [age]
+                }
+                return { ...data, targeting: newTargeting }
+            }
+            data.targetingName = '年龄不够数量分配,请删除'
+            return { ...data, targeting }
+        })
         onChange?.(data)
     }
 
@@ -121,12 +181,13 @@ const GenerateTarget: React.FC<Props> = ({ target, visible, onChange, onClose })
         onCancel={onClose}
         bodyStyle={{ padding: '0 0 40px', position: 'relative', borderRadius: '0 0 8px 8px' }}
         footer={null}
+        width={700}
     >
         <Form
             form={form}
             name="generateTarget"
             labelAlign='left'
-            labelCol={{ span: 4 }}
+            labelCol={{ span: 5 }}
             colon={false}
             style={{ backgroundColor: '#f1f4fc', maxHeight: 600, overflow: 'hidden', overflowY: 'auto', padding: '10px 10px 10px', borderRadius: '0 0 8px 8px' }}
             scrollToFirstError
@@ -136,6 +197,12 @@ const GenerateTarget: React.FC<Props> = ({ target, visible, onChange, onClose })
             onFinish={handleOk}
             initialValues={{
                 regions: 540000,
+                defaultStartAge: 35,
+                startAgeMin: 30,
+                startAgeMax: 40,
+                defaultEndAge: 66,
+                endAgeMin: 50,
+                endAgeMax: 66,
                 count: 3
             }}
         >
@@ -160,6 +227,66 @@ const GenerateTarget: React.FC<Props> = ({ target, visible, onChange, onClose })
                         {REGION_DATA.map(item => <Select.Option value={item.value} key={item.key}>{item.title}</Select.Option>)}
                     </Select>
                 </Form.Item>
+                <Form.Item
+                    label={<strong>年龄前段扩展区间</strong>}
+                    required
+                    help={<span style={{ color: 'red', fontSize: 12 }}>{`前段扩展的时候,后段固定年龄“${defaultEndAge === 66 ? '66岁及以上' : defaultEndAge + '岁'}”,可扩展${startAgeMin === 66 ? '66岁及以上' : `${startAgeMin}岁~${startAgeMax === 66 ? '66岁及以上' : startAgeMax + '岁'}`},可扩展数量${startAgeList.length}个`}</span>}
+                >
+                    <Space>
+                        <Form.Item name={'defaultStartAge'} noStyle>
+                            <Select style={{ width: 138 }} placeholder="前段默认年龄">
+                                {START_AGE.map(i => {
+                                    return <Select.Option disabled={i > defaultEndAge} value={i} key={i}>{i === 66 ? '66岁及以上' : i + ' 岁'}</Select.Option>
+                                })}
+                            </Select>
+                        </Form.Item>
+                        <Form.Item name={'startAgeMin'} noStyle>
+                            <Select style={{ width: 170 }} placeholder="请选择">
+                                {START_AGE.map(i => {
+                                    return <Select.Option disabled={i > startAgeMax} value={i} key={i}>{i === 66 ? '66 岁及以上' : i + ' 岁'}</Select.Option>
+                                })}
+                            </Select>
+                        </Form.Item>
+                        <span>-</span>
+                        <Form.Item name={'startAgeMax'} noStyle>
+                            <Select style={{ width: 170 }} placeholder="请选择">
+                                {END_AGE.map((_, i) => {
+                                    return <Select.Option disabled={(i + 18 < startAgeMin) || (i + 18 > defaultEndAge)} value={i + 18} key={i + 18}>{i + 18 === 66 ? '66 岁及以上' : i + 18 + ' 岁'}</Select.Option>
+                                })}
+                            </Select>
+                        </Form.Item>
+                    </Space>
+                </Form.Item>
+                <Form.Item
+                    label={<strong>年龄后段扩展区间</strong>}
+                    required
+                    help={<span style={{ color: 'red', fontSize: 12 }}>{`后段扩展的时候,前段固定年龄“${defaultStartAge === 66 ? '66岁及以上' : defaultStartAge + '岁'}”,可扩展${endAgeMin === 66 ? '66岁及以上' : `${endAgeMin}岁~${endAgeMax === 66 ? '66岁及以上' : endAgeMax + '岁'}`}, 可扩展数量${endAgeList.length}个`}</span>}
+                >
+                    <Space>
+                        <Form.Item name={'defaultEndAge'} noStyle>
+                            <Select style={{ width: 138 }} placeholder="请选择">
+                                {END_AGE.map((_, i) => {
+                                    return <Select.Option disabled={i + 18 < defaultStartAge} value={i + 18} key={i + 18}>{i + 18 === 66 ? '66 岁及以上' : i + 18 + ' 岁'}</Select.Option>
+                                })}
+                            </Select>
+                        </Form.Item>
+                        <Form.Item name={'endAgeMin'} noStyle>
+                            <Select style={{ width: 170 }} placeholder="请选择">
+                                {END_AGE.map((_, i) => {
+                                    return <Select.Option disabled={(i > endAgeMax) || (i + 18 < defaultStartAge)} value={i + 18} key={i + 18}>{i + 18 === 66 ? '66 岁及以上' : i + 18 + ' 岁'}</Select.Option>
+                                })}
+                            </Select>
+                        </Form.Item>
+                        <span>-</span>
+                        <Form.Item name={'endAgeMax'} noStyle>
+                            <Select style={{ width: 170 }} placeholder="请选择">
+                                {END_AGE.map((_, i) => {
+                                    return <Select.Option disabled={i + 18 < endAgeMin} value={i + 18} key={i + 18}>{i + 18 === 66 ? '66 岁及以上' : i + 18 + ' 岁'}</Select.Option>
+                                })}
+                            </Select>
+                        </Form.Item>
+                    </Space>
+                </Form.Item>
                 <Form.Item
                     label={<strong>生成数量</strong>}
                     name='count'

+ 1 - 1
src/services/adqV3/cloudNew.ts

@@ -59,7 +59,7 @@ export async function delFolderApi(id: number) {
 export async function moveFolderApi({ targetId, sourceId }: { targetId: number, sourceId: number[] }) {
     return request(api + `/material/folder/move`, {
         method: 'POST',
-        data: { sourceId },
+        data: sourceId,
         params: { targetId }
     })
 }