Browse Source

Merge branch 'develop' of http://git.zanxiangnet.com/wjx/ad-manage into wangjianxin

wjx 7 tháng trước cách đây
mục cha
commit
904ce232fe

+ 6 - 0
config/routerConfig.ts

@@ -195,6 +195,12 @@ const launchSystemV3 = {
                     access: 'cloud',
                     component: './launchSystemNew/material/cloud',
                 },
+                {
+                    name: '云端素材',
+                    path: '/launchSystemV3/material/tencent',
+                    access: 'tencent',
+                    component: './launchSystemV3/material/tencent',
+                },
                 {
                     name: '素材库',
                     path: '/launchSystemV3/material/cloudNew',

+ 12 - 2
src/pages/launchSystemV3/material/cloudNew/global.less

@@ -1,6 +1,5 @@
-
 .content_global {
-    
+
     >div {
         height: 100%;
 
@@ -8,4 +7,15 @@
             height: 100%;
         }
     }
+}
+
+.transition-enter {
+    opacity: 0;
+    transform: scale(1.04);
+}
+
+.transition-enter-active {
+    opacity: 1;
+    transform: scale(1);
+    transition: opacity 400ms, transform 400ms;
 }

+ 16 - 0
src/pages/launchSystemV3/material/cloudNew/index.less

@@ -175,10 +175,26 @@
             }
         }
 
+        .imgPreview {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            color: #fff;
+            padding: 1px 8px;
+            transform: translate(-50%, -50%);
+            background-color: rgba(0, 0, 0, 0.55);
+            border-radius: 6px;
+            opacity: 0;
+            transition: opacity 0.2s;
+        }
+
         &:hover {
             .file_info>div {
                 opacity: 0.25;
             }
+            .imgPreview {
+                opacity: 1;
+            }
         }
     }
 

+ 9 - 2
src/pages/launchSystemV3/material/cloudNew/material.tsx

@@ -18,6 +18,7 @@ import UpdateFile from "./updateFile"
 import Details from "./details"
 import useFileDrop from "@/Hook/useFileDrop"
 import UploadsTable from "./uploadsTable"
+import Lazyimg from "react-lazyimg-component"
 const { Text } = Typography;
 
 interface Props {
@@ -410,7 +411,12 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
                                             <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="" />
+                                        <Lazyimg
+                                            animateType="transition"
+                                            src={item.materialType === 'image' ? item.ossUrl : getVideoImgUrl(item.ossUrl)}
+                                            className={`${style.coverImg} lazy`}
+                                            animateClassName={['transition-enter', 'transition-enter-active']}
+                                        />
                                     </div>}
                                     onClick={() => { setDetailsData({ visible: true, data: item }) }}
                                 >
@@ -446,7 +452,8 @@ const Material = forwardRef(({ onAddFolder, onUpdateFolder, onDelFolder }: Props
                 pageSize={getMaterialList?.data?.pageSize || 30}
                 current={getMaterialList?.data?.pageNumber || 1}
                 onChange={(page: number, pageSize: number) => {
-                    setMaterialParams({ ...materialParams, pageNum: page, pageSize })
+                    ref.current?.scrollTo({ top: 0 })
+                    setTimeout(() => setMaterialParams({ ...materialParams, pageNum: page, pageSize }), 50)
                 }}
             />
         </div>}

+ 1 - 1
src/pages/launchSystemV3/material/cloudNew/selectCloudNew.tsx

@@ -91,7 +91,7 @@ const SelectCloudNew: React.FC<CLOUDNEW.SelectCloudNewProps> = ({ visible, defau
         onChange?.(checkedFolderList)
     }
 
-    // 文件夹选择
+    // 选择
     const onCheckboxChange = (checked: boolean, item: any) => {
         let newCheckedFolderList: any[] = JSON.parse(JSON.stringify(checkedFolderList))
         if (checked) { // 选中

+ 45 - 0
src/pages/launchSystemV3/material/tencent/const.ts

@@ -0,0 +1,45 @@
+
+/** 选择素材 展示字段 */
+export const showFieldList = [
+    { label: '创建时间', value: 'created_time', field: 'created_time' },
+    { label: '消耗', value: 'material_data.cost', field: 'cost' },
+    { label: '曝光量', value: 'material_data.view_count', field: 'view_count' },
+    { label: '点击量', value: 'material_data.valid_click_count', field: 'valid_click_count' },
+    { label: '公众号关注人数(点击归因)', value: 'material_data.from_follow_by_click_uv', field: 'from_follow_by_click_uv' },
+    { label: '公众号关注成本(点击归因)', value: 'material_data.from_follow_by_click_cost', field: 'from_follow_by_click_cost' },
+    { label: '公众号关注人数(平台上报)', value: 'material_data.biz_follow_uv', field: 'biz_follow_uv' },
+    { label: '目标转化量', value: 'material_data.conversions_count', field: 'conversions_count' },
+    { label: '目标转化成本', value: 'material_data.conversions_cost', field: 'conversions_cost' },
+    { label: '深度目标转化量', value: 'material_data.deep_conversions_count', field: 'deep_conversions_count' },
+    { label: '加企业微信客服次数', value: 'material_data.scan_follow_count', field: 'scan_follow_count' },
+    { label: '加企业微信客服人数', value: 'material_data.scan_follow_user_count', field: 'scan_follow_user_count' },
+    { label: '下单次数', value: 'material_data.order_pv', field: 'order_pv' },
+    { label: '下单金额', value: 'material_data.order_amount', field: 'order_amount' },
+    { label: '下单次数(点击归因)', value: 'material_data.order_by_click_count', field: 'order_by_click_count' },
+    { label: '下单金额(点击归因)', value: 'material_data.order_by_click_amount', field: 'order_by_click_amount' },
+    { label: '用户点击广告当天内,完成的下单次数加和', value: 'material_data.first_day_order_by_click_count', field: 'first_day_order_by_click_count' },
+    // { label: '点击首日付费次数', value: 'material_data.cheout_pv1d', field: 'cheout_pv1d' },
+    // { label: '点击首日付费金额', value: 'material_data.cheout_fd', field: 'cheout_fd' },
+    // { label: '点击首日付费成本', value: 'material_data.cheout1d_cost', field: 'cheout1d_cost' },
+    // { label: '点击首日付费金额(平台上报)', value: 'material_data.purchase_pla_clk1d_amount', field: 'purchase_pla_clk1d_amount' },
+    { label: '点击首日下单次数(首日新增下单量)', value: 'material_data.first_day_order_count', field: 'first_day_order_count' },
+    { label: '首日新增下单金额(点击归因)', value: 'material_data.first_day_order_by_click_amount', field: 'first_day_order_by_click_amount' },
+    { label: '点击首日下单金额(首日新增下单金额)', value: 'material_data.first_day_order_amount', field: 'first_day_order_amount' },
+    { label: '点击均价', value: 'calculate_material_data.cpc', field: 'cpc' },
+    { label: '千次曝光成本', value: 'calculate_material_data.thousand_display_price', field: 'thousand_display_price' },
+    { label: '点击率', value: 'calculate_material_data.ctr', field: 'ctr', isRate: true },
+    { label: '公众号关注成本(平台上报)', value: 'calculate_material_data.biz_follow_cost', field: 'biz_follow_cost' },
+    { label: '公众号关注率(平台上报)', value: 'calculate_material_data.biz_follow_rate', field: 'biz_follow_rate', isRate: true },
+    { label: '目标转化率', value: 'calculate_material_data.conversions_rate', field: 'conversions_rate', isRate: true },
+    { label: '深度目标转化率', value: 'calculate_material_data.deep_conversions_rate', field: 'deep_conversions_rate', isRate: true },
+    { label: '加企业微信客服成本', value: 'calculate_material_data.scan_follow_user_cost', field: 'scan_follow_user_cost' },
+    { label: '加企业微信客服率', value: 'calculate_material_data.scan_follow_user_rate', field: 'scan_follow_user_rate', isRate: true },
+    { label: '下单单价', value: 'calculate_material_data.order_unit_price', field: 'order_unit_price' },
+    { label: '下单率', value: 'calculate_material_data.order_rate', field: 'order_rate', isRate: true },
+    { label: '下单成本', value: 'calculate_material_data.order_by_display_cost', field: 'order_by_display_cost' },
+    { label: '下单ROI', value: 'calculate_material_data.order_roi', field: 'order_roi', isRate: true },
+    // { label: '客单价', value: 'calculate_material_data.unit_price', field: 'unit_price' },
+    { label: '下单率(点击归因)', value: 'calculate_material_data.order_by_click_rate', field: 'order_by_click_rate', isRate: true },
+    { label: '下单成本(点击归因)', value: 'calculate_material_data.order_by_click_cost', field: 'order_by_click_cost' },
+    { label: '首日新增下单ROI', value: 'calculate_material_data.first_day_order_roi', field: 'first_day_order_roi', isRate: true },
+]

+ 300 - 0
src/pages/launchSystemV3/material/tencent/index.tsx

@@ -0,0 +1,300 @@
+import { useAjax } from "@/Hook/useAjax"
+import { getRemoteMaterialListApi } from "@/services/adqV3/cloudNew"
+import React, { useEffect, useRef, useState } from "react"
+import style from '../cloudNew/index.less'
+import Search from "./search"
+import { Button, Card, Checkbox, Divider, Dropdown, Empty, Form, message, Pagination, Popover, Radio, Select, Space, Spin, Statistic, Typography, Image } from "antd"
+import { EyeOutlined, SortAscendingOutlined } from "@ant-design/icons"
+import PlayVideo from "../cloudNew/playVideo"
+import { formatBytes, formatSecondsToTime } from "@/utils/utils"
+import { useLocalStorageState, useSize } from "ahooks"
+import { showFieldList } from "./const"
+import Lazyimg from "react-lazyimg-component"
+import '../cloudNew/global.less'
+import SaveMaterial from "./saveMaterial"
+const { Text, Paragraph } = Typography;
+
+const Tencent: React.FC = () => {
+
+    /*******************************/
+    const ref = useRef<HTMLDivElement>(null);
+    const size = useSize(ref);
+    const [rowNum, setRowNum] = useState<number>(0)
+    const [queryParams, setQueryParams] = useState<CLOUDNEW.GetRemoteMaterialListProps>({ pageNum: 1, pageSize: 50 })
+    const [searchParams, setSearchParams] = useState<Partial<CLOUDNEW.GetRemoteMaterialListProps>>({})
+    const [checkedFolderList, setCheckedFolderList] = useState<any[]>([])
+    const [checkFolderAll, setCheckFolderAll] = useState<boolean>(false);
+    const [indeterminateFolder, setIndeterminateFolder] = useState<boolean>(false);
+    const [showField, setShowField] = useLocalStorageState<string[]>('show-tencent-field', ['material_data.cost', 'material_data.view_count', 'material_data.valid_click_count', 'material_data.order_pv', 'material_data.order_uv', 'calculate_material_data.order_rate']);
+    const [sortData, setSortData] = useLocalStorageState<{ sortField: string | undefined, sortType: boolean }>('sort-tencent-data', { sortField: undefined, sortType: false });
+    const [previewData, setPreviewData] = useState<{ visible: boolean, url?: string }>({ visible: false })
+    const [moveData, setMoveData] = useState<{ visible: boolean, data: any[] }>({ visible: false, data: [] })
+
+    const getRemoteMaterialList = useAjax((params) => getRemoteMaterialListApi(params))
+    /*******************************/
+
+    useEffect(() => {
+        let params = { ...queryParams, ...searchParams, columns: showField || [] }
+        if (sortData?.sortField) {
+            params = { ...params, ...sortData }
+        }
+        getRemoteMaterialList.run(params)
+    }, [queryParams, searchParams, sortData, showField])
+
+    // 根据内容宽度计算列数
+    useEffect(() => {
+        if (size?.width) {
+            let rowNum = Math.floor((size?.width - 26) / 230)
+            setRowNum(rowNum || 1)
+        }
+    }, [size?.width])
+
+    // 处理全选按钮状态
+    useEffect(() => {
+        let data: any[] = getRemoteMaterialList?.data?.records || []
+        let dataIds = data.map(item => item.preview_url)
+        let selectIds = checkedFolderList.map(item => item.preview_url)
+        if (selectIds.length === 0 || dataIds.length === 0) {
+            setCheckFolderAll(false)
+            setIndeterminateFolder(false);
+        } else if (dataIds.every((element) => selectIds.includes(element))) {
+            setCheckFolderAll(true)
+            setIndeterminateFolder(false);
+        } else if (dataIds.some((element) => selectIds.includes(element))) {
+            setCheckFolderAll(false)
+            setIndeterminateFolder(true);
+        } else {
+            setCheckFolderAll(false)
+            setIndeterminateFolder(false);
+        }
+    }, [checkedFolderList, getRemoteMaterialList?.data?.records])
+
+    // 选择
+    const onCheckboxChange = (checked: boolean, item: any) => {
+        let newCheckedFolderList: any[] = JSON.parse(JSON.stringify(checkedFolderList))
+        if (checked) { // 选中
+            newCheckedFolderList.push(item)
+        } else { // 取消
+            newCheckedFolderList = newCheckedFolderList.filter(i => i.preview_url !== item.preview_url)
+        }
+        setCheckedFolderList(newCheckedFolderList)
+    };
+
+    return <div className={style.cloudNew_layout}>
+        {/* 搜索 */}
+        <Search
+            onSearch={(value) => { setSearchParams(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}>
+                <div className={style.material} style={{ height: '100%' }}>
+                    <div className={style.operates}>
+                        <div className={style.left_bts}>
+                            <Checkbox
+                                onChange={(e) => {
+                                    let newCheckedFolderList: any[] = JSON.parse(JSON.stringify(checkedFolderList))
+                                    const data: any[] = getRemoteMaterialList?.data?.records
+
+                                    if (e.target.checked) { // 全选
+                                        newCheckedFolderList.push(...data)
+                                        newCheckedFolderList = Array.from(new Set(newCheckedFolderList.map(item => JSON.stringify(item)))).map(item => JSON.parse(item));
+                                    } else { // 取消全选
+                                        const dataIds = data.map(item => item.preview_url)
+                                        newCheckedFolderList = newCheckedFolderList.filter(i => !dataIds.includes(i.preview_url))
+                                    }
+                                    setCheckedFolderList(newCheckedFolderList)
+                                }}
+                                indeterminate={indeterminateFolder}
+                                checked={checkFolderAll}
+                            >全选</Checkbox>
+                            <span>已选 <span style={{ color: '#1890FF' }}>{checkedFolderList?.length || 0}</span> 个素材</span>
+                            {checkedFolderList.length > 0 && <a style={{ color: 'red' }} onClick={() => setCheckedFolderList([])}>清除所有</a>}
+                            {sortData?.sortField && <Text>「排序-{showFieldList.find(item => item.field === sortData.sortField)?.label}-{sortData.sortType ? '正序' : '倒序'}」</Text>}
+                        </div>
+                        <div className={style.left_bts}>
+                            {checkedFolderList.length > 0 && <Button
+                                type="primary"
+                                onClick={() => {
+                                    setMoveData({ visible: true, data: checkedFolderList })
+                                }}
+                            >保存至素材库</Button>}
+                            <Popover
+                                content={<div style={{ width: 400 }}>
+                                    <Form
+                                        labelCol={{ span: 5 }}
+                                        labelAlign="left"
+                                        colon={false}
+                                    >
+                                        <Form.Item label={<strong>展示指标</strong>}>
+                                            <Select
+                                                placeholder="选择展示指标"
+                                                showSearch
+                                                filterOption={(input, option) =>
+                                                    (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                }
+                                                mode="multiple"
+                                                options={showFieldList}
+                                                value={showField}
+                                                onChange={(value) => {
+                                                    if (value.length > 8) {
+                                                        message.warn('最多只能选择8个指标')
+                                                        return
+                                                    }
+                                                    if (value.length < 1) {
+                                                        message.warn('最少选择1个指标')
+                                                        return
+                                                    }
+                                                    setShowField(value as any)
+                                                }}
+                                            />
+                                        </Form.Item>
+                                        <Form.Item label={<strong>排序</strong>}>
+                                            <Space>
+                                                <Select
+                                                    style={{ width: 180 }}
+                                                    placeholder="选择排序指标"
+                                                    showSearch
+                                                    filterOption={(input, option) =>
+                                                        (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                    }
+                                                    options={showFieldList.map(item => ({ label: item.label, value: item.field }))}
+                                                    value={sortData.sortField}
+                                                    allowClear
+                                                    onChange={(value) => {
+                                                        setSortData({ ...sortData, sortField: value as any })
+                                                    }}
+                                                />
+                                                <Radio.Group value={sortData.sortType} onChange={(e) => setSortData({ ...sortData, sortType: e.target.value })} buttonStyle="solid">
+                                                    <Radio.Button value={true}>正序</Radio.Button>
+                                                    <Radio.Button value={false}>倒序</Radio.Button>
+                                                </Radio.Group>
+                                            </Space>
+                                        </Form.Item>
+                                    </Form>
+                                </div>}
+                                trigger={['click']}
+                                title={<strong>自定义展示指标与排序</strong>}
+                                placement="bottomRight"
+                            >
+                                <Button icon={<SortAscendingOutlined />}>自定义展示指标与排序</Button>
+                            </Popover>
+                        </div>
+                    </div>
+                    <div className={`${style.content} content_global`}>
+                        <Spin spinning={getRemoteMaterialList.loading}>
+                            <div className={style.content_scroll} ref={ref}>
+                                {getRemoteMaterialList?.data?.records?.length > 0 ? <Checkbox.Group value={checkedFolderList?.map(item => item.preview_url)} style={{ width: '100%' }}>
+                                    <div className={style.content_scroll_div}>
+                                        {getRemoteMaterialList?.data?.records.map((item: any, index: number) => {
+                                            const isSelect = checkedFolderList?.map(item => item.preview_url).includes(item.preview_url)
+                                            return <div key={(queryParams.pageNum).toString() + (index + 1).toString()} className={style.content_row} style={{ width: rowNum ? (1 / rowNum * 100) + '%' : 230 }}>
+                                                <Card
+                                                    hoverable
+                                                    bodyStyle={{ padding: 0 }}
+                                                    className={`${style.content_col}`}
+                                                    cover={<div style={{ height: 120, padding: 0 }} className={style.content_cover}>
+                                                        <div className={style.checkbox}><Checkbox value={item.preview_url} onChange={(e) => onCheckboxChange(e.target.checked, item)} /></div>
+                                                        {item.source === 'video' ? <div className={style.playr}>
+                                                            <PlayVideo videoUrl={item.preview_url}>{(onPlay) => <img onClick={(e) => {
+                                                                e.stopPropagation(); e.preventDefault()
+                                                                onPlay()
+                                                            }} src={require('../../../../../public/image/play.png')} alt="" />}</PlayVideo>
+                                                        </div> : <div className={style.imgPreview} onClick={(e) => {
+                                                            e.stopPropagation()
+                                                            setPreviewData({ visible: true, url: item.preview_url })
+                                                        }}>
+                                                            <Space><EyeOutlined /><span>预览</span></Space>
+                                                        </div>}
+                                                        <div className={style.file_info}>
+                                                            <div>{item.width}*{item.height}</div>
+                                                            {item.source === 'video' && item.image_duration_millisecond && <div>{formatSecondsToTime(Math.floor(item.image_duration_millisecond / 1000))}</div>}
+                                                        </div>
+                                                        <Lazyimg
+                                                            animateType="transition"
+                                                            src={item.source === 'image' ? item.preview_url : item?.key_frame_image_url}
+                                                            className={`${style.coverImg} lazy`}
+                                                            animateClassName={['transition-enter', 'transition-enter-active']}
+                                                        />
+                                                    </div>}
+                                                    onClick={() => onCheckboxChange(!isSelect, item)}
+                                                >
+                                                    <div className={style.body}>
+                                                        <Text ellipsis strong style={{ flex: '1 0' }}>{item?.description}</Text>
+                                                        <Text style={{ fontSize: 12 }}>{formatBytes(item?.file_size)}</Text>
+                                                    </div>
+                                                    <Divider style={{ margin: '0 0 4px 0' }} />
+                                                    <div style={{ padding: '0 10px 6px' }}>
+                                                        {showFieldList.filter(f => showField.includes(f.value)).map(f => {
+                                                            return <div key={f.value} style={{ display: 'flex', marginBottom: 1 }}>
+                                                                <div style={{ maxWidth: 120, display: 'flex' }}><Paragraph ellipsis={{ tooltip: { mouseEnterDelay: 0.5, placement: 'bottom' } }} style={{ fontSize: 12, marginBottom: 0 }}>{f.label}</Paragraph>:</div>
+                                                                <div style={{ flex: '1 0' }}>{f?.isRate ? <Statistic valueStyle={{ fontSize: 12 }} value={item?.[f.field] * 100} precision={2} suffix="%" /> : <Statistic valueStyle={{ fontSize: 12 }} value={item?.[f.field]} />} </div>
+                                                            </div>
+                                                        })}
+                                                    </div>
+                                                    <Divider style={{ margin: '0 0 4px 0' }} />
+                                                    <div className={style.actions}>
+                                                        <div style={{ height: 22 }}></div>
+                                                        <Dropdown menu={{
+                                                            items: [{ label: '保存至素材库', key: '1', onClick: () => { setMoveData({ visible: true, data: [item] }) } }]
+                                                        }}>
+                                                            <a onClick={e => e.preventDefault()} style={{ fontSize: 11 }}>更多</a>
+                                                        </Dropdown>
+                                                    </div>
+                                                </Card>
+                                            </div>
+                                        })}
+                                    </div>
+                                </Checkbox.Group> : <div style={{ height: '100%', width: '100%', alignContent: 'center' }}><Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /></div>}
+                            </div>
+                        </Spin>
+                    </div>
+                    <div className={style.fotter}>
+                        <Pagination
+                            size="small"
+                            total={getRemoteMaterialList?.data?.total || 0}
+                            showSizeChanger
+                            showQuickJumper
+                            pageSize={getRemoteMaterialList?.data?.size || 20}
+                            current={getRemoteMaterialList?.data?.current || 1}
+                            onChange={(page: number, pageSize: number) => {
+                                ref.current?.scrollTo({ top: 0 })
+                                setTimeout(() => setQueryParams({ ...queryParams, pageNum: page, pageSize }), 50)
+                            }}
+                        />
+                    </div>
+                </div>
+            </div>
+        </Card>
+
+        {/* 预览 */}
+        {previewData.visible && <Image
+            width={200}
+            style={{ display: 'none' }}
+            preview={{
+                visible: previewData.visible,
+                src: previewData.url,
+                onVisibleChange: value => {
+                    setPreviewData({ visible: false })
+                },
+            }}
+        />}
+
+        {/* 移动至 */}
+        {moveData.visible && <SaveMaterial
+            {...moveData}
+            onChange={() => {
+                setMoveData({ visible: false, data: [] })
+            }}
+            onClose={() => {
+                setMoveData({ visible: false, data: [] })
+            }}
+        />}
+    </div>
+}
+
+export default Tencent

+ 140 - 0
src/pages/launchSystemV3/material/tencent/saveMaterial.tsx

@@ -0,0 +1,140 @@
+import { Button, Card, Form, message, Modal, Space, TreeSelect } from "antd"
+import React, { useEffect, useState } from "react"
+import '../../tencentAdPutIn/index.less'
+import { addRemoteMaterialApi, getFolderListApi } from "@/services/adqV3/cloudNew"
+import { useAjax } from "@/Hook/useAjax"
+import { DataNode } from "antd/lib/tree"
+import { updateTreeData } from "../cloudNew/const"
+
+interface Props {
+    data: any[]
+    visible?: boolean
+    onChange?: () => void
+    onClose?: () => void
+}
+/**
+ * 移动至素材库
+ * @returns 
+ */
+const SaveMaterial: React.FC<Props> = ({ data, visible, onChange, onClose }) => {
+
+    /*************************************/
+    const [treeData, setTreeData] = useState<DataNode[]>([]);
+    const [form] = Form.useForm();
+
+    const getFolderList = useAjax((params) => getFolderListApi(params))
+    const addRemoteMaterial = useAjax((params) => addRemoteMaterialApi(params))
+    /*************************************/
+
+    const handleOk = (values: any) => {
+        console.log(values)
+        addRemoteMaterial.run({
+            ...values,
+            data: data.map(item => ({
+                aspectRatio: item.aspect_ratio,
+                description: item.description,
+                designerId: localStorage.getItem('userId'),
+                fileSize: item.file_size,
+                height: item.height,
+                width: item.width,
+                materialName: item.description,
+                materialType: item.source,
+                md5: item.signature,
+                url: item.preview_url,
+            }))
+        }).then(res => {
+            if (res) {
+                message.success('移动成功')
+                onChange?.()
+            }
+        })
+    }
+
+    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: localStorage.getItem('userId') !== item.createBy.toString()
+            })) || [])
+        })
+    }
+
+    // 下级目录
+    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: localStorage.getItem('userId') !== item.createBy.toString()
+                })) || []),
+            );
+        })
+    }
+
+    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}
+        >
+            <Card className="cardResetCss">
+                <Form.Item label={<strong>文件夹</strong>} name={'folderId'} rules={[{ required: true, message: '请选择文件夹' }]}>
+                    <TreeSelect
+                        loading={getFolderList.loading}
+                        allowClear
+                        style={{ width: '100%' }}
+                        dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+                        placeholder="请选择文件夹"
+                        loadData={({ value }) => {
+                            return new Promise<void>(async (resolve) => {
+                                await handleUpdateFolder(Number(value))
+                                resolve()
+                            })
+                        }}
+                        treeData={treeData}
+                    />
+                </Form.Item>
+            </Card>
+
+            <Form.Item className="submit_pull">
+                <Space>
+                    <Button onClick={onClose}>取消</Button>
+                    <Button type="primary" htmlType="submit" loading={addRemoteMaterial.loading} className="modalResetCss">
+                        确定
+                    </Button>
+                </Space>
+            </Form.Item>
+        </Form>
+    </Modal>
+}
+
+export default React.memo(SaveMaterial)

+ 99 - 0
src/pages/launchSystemV3/material/tencent/search.tsx

@@ -0,0 +1,99 @@
+import { Button, Card, Col, DatePicker, Form, Input, InputNumber, Row, Select, Space } from "antd"
+import React, { useEffect } from "react"
+import { getUserAllApi } from "@/services/operating/account";
+import { useAjax } from "@/Hook/useAjax";
+import { SearchOutlined } from "@ant-design/icons";
+import moment from "moment";
+import '../../tencentAdPutIn/index.less'
+
+interface Props {
+    onSearch?: (value: Partial<CLOUDNEW.GetMaterialListProps>) => void
+}
+
+/**
+ * 远程素材库
+ * @param param0 
+ * @returns 
+ */
+const Search: React.FC<Props> = ({ onSearch }) => {
+
+    /**********************************/
+    const [form] = Form.useForm();
+
+    const getUserAll = useAjax(() => getUserAllApi())
+    /**********************************/
+
+    useEffect(() => {
+        getUserAll.run()
+    }, [])
+
+    const handleOk = (values: any) => {
+        console.log(values)
+        let params: any = []
+        Object.keys(values).forEach(key => {
+            let value = values[key]
+            if (['accountIds', 'adgroupIds', 'dynamicCreativeIds', 'tencentMaterialId'].includes(key) && value) {
+                let value1 = value.replace(/[,,\s]/g, ',')
+                params[key] = value1.split(',').filter((a: any) => a)
+            } else if ('uploadTime' === key && value?.length === 2) {
+                params.uploadTimeMin = moment(value?.[0]).format('YYYY-MM-DD')
+                params.uploadTimeMax = moment(value?.[1]).format('YYYY-MM-DD')
+            } else {
+                params[key] = value
+            }
+        })
+        onSearch?.(params)
+    }
+
+    return <Card
+        bodyStyle={{ padding: '5px 10px', overflow: 'auto hidden', display: 'flex', gap: 6, flexWrap: 'wrap' }}
+        className="cardResetCss buttonResetCss"
+        bordered
+    >
+        <Form
+            layout="inline"
+            name="basicSelectSearch"
+            colon={false}
+            form={form}
+            onFinish={handleOk}
+        >
+            <Row gutter={[0, 6]}>
+                <Col><Form.Item name="materialType">
+                    <Select
+                        placeholder="素材类型"
+                        filterOption={(input, option) =>
+                            ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                        }
+                        allowClear
+                        style={{ width: 120 }}
+                        options={[
+                            { label: '图片', value: 'image' },
+                            { label: '视频', value: 'video' }
+                        ]}
+                    />
+                </Form.Item></Col>
+                {/* <Col><Form.Item name={'sysUserIds'}>
+                    <Select
+                        placeholder="投手"
+                        filterOption={(input, option) =>
+                            ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                        }
+                        style={{ width: 190 }}
+                        maxTagCount={1}
+                        mode="multiple"
+                        allowClear
+                        options={getUserAll?.data?.map((item: { nickname: any; userId: any }) => ({ label: item.nickname, value: item.userId }))}
+                    />
+                </Form.Item></Col> */}
+                <Col><Form.Item>
+                    <Space>
+                        <Button onClick={() => form.resetFields()}>重置</Button>
+                        <Button type="primary" htmlType="submit" icon={<SearchOutlined />} className="modalResetCss">搜索</Button>
+                    </Space>
+                </Form.Item></Col>
+            </Row>
+        </Form>
+    </Card>
+}
+
+export default React.memo(Search)

+ 22 - 0
src/pages/launchSystemV3/material/typings.d.ts

@@ -130,4 +130,26 @@ declare namespace CLOUDNEW {
         onClose?: () => void
         onChange?: (data: any[]) => void
     }
+
+    interface GetRemoteMaterialListProps {
+        pageNum: number,
+        pageSize: number,
+        columns?: string[],
+        materialType?: string,
+        sortField?: string,
+        sortType?: boolean
+    }
+
+    interface AddRemoteMaterialProps {
+        aspectRatio: string
+        description: string
+        designerId: number
+        fileSize: number
+        height: number
+        width: number
+        materialName: string
+        materialType: string
+        md5: string
+        url: string
+    }
 }

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

@@ -221,4 +221,26 @@ export async function moveOldMaterialApi(data: { materialIds: number[], material
         method: 'POST',
         data
     })
+}
+
+/**
+ * 云端素材库
+ * @param data 
+ * @returns 
+ */
+export async function getRemoteMaterialListApi(data: CLOUDNEW.GetRemoteMaterialListProps) {
+    return request(api + `/material/material/getRemoteMaterialList`, {
+        method: 'POST',
+        data
+    })
+}
+
+
+export async function addRemoteMaterialApi(d: { folderId: number, data: CLOUDNEW.AddRemoteMaterialProps[] }) {
+    const { folderId, data } = d
+    return request(api + `/material/material/addRemoteMaterial`, {
+        method: 'POST',
+        data,
+        params: { folderId }
+    })
 }