Browse Source

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

wjx 1 month ago
parent
commit
f8cd87b679
25 changed files with 2632 additions and 5 deletions
  1. 6 0
      config/routerConfig.ts
  2. 24 0
      src/pages/launchSystemV3/components/AdsComponent/Image1X1.tsx
  3. 18 0
      src/pages/launchSystemV3/components/AdsComponent/index.less
  4. 0 1
      src/pages/launchSystemV3/material/cloudNew/selectGroupCloudNew.tsx
  5. 0 1
      src/pages/launchSystemV3/material/cloudNew/selectGroupUnitNew.tsx
  6. 100 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/addPageTieUp.tsx
  7. 77 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/addWechatTieUp.tsx
  8. 89 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/group.tsx
  9. 77 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/index.less
  10. 45 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/index.tsx
  11. 35 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/nTree.less
  12. 193 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/nTree.tsx
  13. 257 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/pageTieUp.tsx
  14. 120 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/selectPage.tsx
  15. 188 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/settingsEnterprise.tsx
  16. 70 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/tableConfig.tsx
  17. 41 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/userInfo.tsx
  18. 180 0
      src/pages/launchSystemV3/tencenTasset/enterpriseWechat/wechatTieUp.tsx
  19. 90 0
      src/pages/launchSystemV3/tencenTasset/manageComponent/addComponents.tsx
  20. 426 0
      src/pages/launchSystemV3/tencenTasset/manageComponent/const.ts
  21. 45 0
      src/pages/launchSystemV3/tencenTasset/manageComponent/index.less
  22. 187 3
      src/pages/launchSystemV3/tencenTasset/manageComponent/index.tsx
  23. 140 0
      src/pages/launchSystemV3/tencenTasset/manageComponent/tableConfig.tsx
  24. 1 0
      src/pages/launchSystemV3/tencentAdPutIn/create/Material/index.less
  25. 223 0
      src/services/adqV3/global.ts

+ 6 - 0
config/routerConfig.ts

@@ -197,6 +197,12 @@ const launchSystemV3 = {
                     path: '/launchSystemV3/tencenTasset/manageComponent',
                     access: 'manageComponent',
                     component: './launchSystemV3/tencenTasset/manageComponent',
+                },
+                {
+                    name: '客服管理',
+                    path: '/launchSystemV3/tencenTasset/enterpriseWechat',
+                    access: 'enterpriseWechat',
+                    component: './launchSystemV3/tencenTasset/enterpriseWechat',
                 }
             ],
         },

+ 24 - 0
src/pages/launchSystemV3/components/AdsComponent/Image1X1.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import './index.less';
+import Lazyimg from "react-lazyimg-component"
+
+interface Props {
+    imageUrl: string
+    style?: React.CSSProperties
+}
+const Image1X1: React.FC<Props> = ({ imageUrl, style }) => {
+
+
+    return (
+        <div style={style} className='mediaPic'>
+            <Lazyimg
+                animateType="transition"
+                src={imageUrl}
+                className={`lazy`}
+                animateClassName={['transition-enter', 'transition-enter-active']}
+            />
+        </div>
+    );
+};
+
+export default React.memo(Image1X1);

+ 18 - 0
src/pages/launchSystemV3/components/AdsComponent/index.less

@@ -0,0 +1,18 @@
+.mediaPic {
+    position: relative;
+    display: inline-flex;
+    width: 64px;
+    height: 36px;
+    background: #eef1f4;
+    color: #c4c7cc;
+    border-radius: 4px;
+    overflow: hidden;
+    justify-content: center;
+
+    >img {
+        max-width: 100%;
+        max-height: 100%;
+        vertical-align: middle;
+        display: inline-block;
+    }
+}

+ 0 - 1
src/pages/launchSystemV3/material/cloudNew/selectGroupCloudNew.tsx

@@ -11,7 +11,6 @@ import { showField1List } from "./const"
 import { EyeOutlined, SortAscendingOutlined } from "@ant-design/icons"
 import PlayVideo from "./playVideo"
 import Lazyimg from "react-lazyimg-component"
-import { useLocalStorageState } from "ahooks"
 import SyncCloudSc from "./syncCloudSc"
 import { getUserAccountListApi } from "@/services/launchAdq/adAuthorize"
 import { addOnlyDataApi, getOnlyDataApi } from "@/services/adqV3"

+ 0 - 1
src/pages/launchSystemV3/material/cloudNew/selectGroupUnitNew.tsx

@@ -11,7 +11,6 @@ import { showField1List } from "./const"
 import { EyeOutlined, SortAscendingOutlined } from "@ant-design/icons"
 import PlayVideo from "./playVideo"
 import Lazyimg from "react-lazyimg-component"
-import { useLocalStorageState } from "ahooks"
 import SyncCloudSc from "./syncCloudSc"
 import { checkAccountUnitApi } from "@/services/launchAdq/adAuthorize"
 import { addOnlyDataApi, getOnlyDataApi } from "@/services/adqV3"

+ 100 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/addPageTieUp.tsx

@@ -0,0 +1,100 @@
+import { useAjax } from '@/Hook/useAjax';
+import { addPageCustomerGroupApi, getCustomerServiceGroupListApi } from '@/services/adqV3/global';
+import { Form, Modal, Select } from 'antd';
+import React, { useEffect, useState } from 'react';
+import '../../tencentAdPutIn/index.less'
+import SelectPage from './selectPage';
+
+interface Props {
+    adAccountId: number
+    corpList?: { label: string, value: string }[]
+    visible?: boolean
+    onChange?: () => void
+    onClose?: () => void
+}
+
+/**
+ * 新增官方落地页与客服组关系
+ * @param param0 
+ * @returns 
+ */
+const AddPageTieUp: React.FC<Props> = ({ adAccountId, corpList, visible, onChange, onClose }) => {
+
+    /*******************************************/
+    const [form] = Form.useForm();
+    const tencentCorpId = Form.useWatch('tencentCorpId', form)
+    const [groupList, setGroupList] = useState<{ label: string, value: number }[]>([])
+
+    const addPageCustomerGroup = useAjax((params) => addPageCustomerGroupApi(params))
+    const getCustomerServiceGroupList = useAjax((params) => getCustomerServiceGroupListApi(params))
+    // const getCorpListAll = useAjax(() => getCorpListAllApi())
+    /*******************************************/
+
+    /** 客服组 */
+    useEffect(() => {
+        if (adAccountId && tencentCorpId) {
+            getCustomerServiceGroupList.run({ adAccountId, pageNum: 1, pageSize: 100, tencentCorpId }).then(res => {
+                setGroupList(res?.records?.map((item: { groupName: string; groupId: number; }) => ({ label: item.groupName, value: item.groupId + '&&' + item.groupName })))
+            }).catch(() => setGroupList([]))
+        }
+    }, [adAccountId, tencentCorpId])
+
+    const handleOk = () => {
+        form.validateFields().then(values => {
+            const { customerGroup, page, ...params } = values
+            const [customerGroupId, customerGroupName] = customerGroup.split('&&')
+            const { pageId, pageName } = page
+            addPageCustomerGroup.run({ adAccountId, customerGroupName, customerGroupId, pageId, pageName, ...params }).then((res) => {
+                if (res)
+                    onChange?.()
+            })
+        })
+    };
+
+    return <Modal
+        title={<strong>新增官方落地页与客服组关系</strong>}
+        open={visible}
+        onOk={handleOk}
+        onCancel={onClose}
+        className='modalResetCss'
+        confirmLoading={addPageCustomerGroup.loading}
+    >
+        <Form
+            form={form}
+            name='basicWechatTieUp'
+            autoComplete="off"
+            colon={false}
+            labelCol={{ span: 5 }}
+            labelAlign="left"
+        >
+            <Form.Item label={<strong>企微端企业</strong>} name="tencentCorpId" rules={[{ required: true, message: '请选择企业!' }]}>
+                <Select
+                    showSearch
+                    placeholder="请选择企业"
+                    filterOption={(input, option) =>
+                        ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                    }
+                    allowClear
+                    options={corpList}
+                />
+            </Form.Item>
+            {tencentCorpId && <Form.Item label={<strong>客服组</strong>} name="customerGroup" rules={[{ required: true, message: '请选择客服组!' }]}>
+                <Select
+                    showSearch
+                    placeholder="请选择客服组"
+                    filterOption={(input, option) =>
+                        ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                    }
+                    loading={getCustomerServiceGroupList.loading}
+                    allowClear
+                    options={groupList}
+                />
+            </Form.Item>}
+            <Form.Item label={<strong>官方落地页</strong>} name="page" rules={[{ required: true, message: '请选择官方落地页!' }]}>
+                <SelectPage accountId={adAccountId} />
+            </Form.Item>
+        </Form>
+    </Modal>;
+};
+
+export default React.memo(AddPageTieUp);

+ 77 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/addWechatTieUp.tsx

@@ -0,0 +1,77 @@
+import React, { useEffect } from 'react';
+import { Modal, Form, Input, Select } from 'antd';
+import '../../tencentAdPutIn/index.less'
+import { useAjax } from '@/Hook/useAjax';
+import { addCorpRelationListApi, getCorpListAllApi } from '@/services/adqV3/global';
+
+interface Props {
+    adAccountId: number
+    visible?: boolean
+    onChange?: () => void
+    onClose?: () => void
+}
+
+/**
+ * 新增企微与投放端关系
+ * @param param0 
+ * @returns 
+ */
+const AddWechatTieUp: React.FC<Props> = ({ adAccountId, visible, onChange, onClose }) => {
+
+    /*******************************************/
+    const [form] = Form.useForm();
+
+    const addCorpRelationList = useAjax((params) => addCorpRelationListApi(params))
+    const getCorpListAll = useAjax(() => getCorpListAllApi())
+    /*******************************************/
+
+    useEffect(() => {
+        getCorpListAll.run()
+    }, [])
+
+    const handleOk = () => {
+        form.validateFields().then(values => {
+            const { corp, ...params } = values
+            const [corpName, corpId] = corp.split('&&')
+            addCorpRelationList.run({ adAccountId, corpName, corpId, ...params }).then((res) => {
+                if (res)
+                    onChange?.()
+            })
+        })
+    };
+
+    return <Modal
+        title={<strong>新增企微与投放端关系</strong>}
+        open={visible}
+        onOk={handleOk}
+        onCancel={onClose}
+        className='modalResetCss'
+        confirmLoading={addCorpRelationList.loading}
+    >
+        <Form
+            form={form}
+            name='basicWechatTieUp'
+            autoComplete="off"
+            colon={false}
+            labelCol={{ span: 5 }}
+            labelAlign="left"
+        >
+            <Form.Item label={<strong>企微端企业</strong>} name="corp" rules={[{ required: true, message: '请输入企微端企业!' }]}>
+                <Select
+                    showSearch
+                    placeholder="请选择企微端企业"
+                    filterOption={(input, option) =>
+                        ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                    }
+                    allowClear
+                    options={getCorpListAll?.data?.map((item: { corpName: string; corpId: string; }) => ({ label: item.corpName, value: item.corpName + '&&' + item.corpId }))}
+                />
+            </Form.Item>
+            <Form.Item label={<strong>投放端企业ID</strong>} name="tencentCorpId" rules={[{ required: true, message: '请输入投放端企业ID!' }]}>
+                <Input placeholder='请输入投放端企业ID' />
+            </Form.Item>
+        </Form>
+    </Modal>;
+};
+
+export default React.memo(AddWechatTieUp);

+ 89 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/group.tsx

@@ -0,0 +1,89 @@
+import { Button, Select, Table } from 'antd';
+import React, { useEffect, useState } from 'react';
+import '../../tencentAdPutIn/index.less'
+import { useAjax } from '@/Hook/useAjax';
+import { getCorpRelationAllApi, getCustomerServiceGroupListApi } from '@/services/adqV3/global';
+import { SearchOutlined } from '@ant-design/icons';
+import columns from './tableConfig';
+import SettingsEnterprise from './settingsEnterprise';
+
+const Group: React.FC<{ adAccountId?: number }> = ({ adAccountId }) => {
+
+    /**********************************/
+    const [queryParamsNew, setQueryParamsNew] = useState<{ adAccountId: number, pageNum: number, pageSize: number, tencentCorpId?: string }>({ pageNum: 1, pageSize: 20, adAccountId: adAccountId || 0 })
+    const [settingsData, setSettingsData] = useState<{ visible: boolean, data: any }>({ visible: false, data: {} })
+
+    const getCustomerServiceGroupList = useAjax((params) => getCustomerServiceGroupListApi(params))
+    const getCorpRelationAll = useAjax((params) => getCorpRelationAllApi(params))
+    /**********************************/
+
+    useEffect(() => {
+        if (adAccountId) {
+            getCorpRelationAll.run({ adAccountId })
+        }
+    }, [adAccountId])
+
+    useEffect(() => {
+        if (adAccountId) {
+            getCustomerServiceGroupList.run({ ...queryParamsNew, adAccountId })
+        }
+    }, [queryParamsNew, adAccountId])
+
+    const handleEdit = (data: any) => {
+        setSettingsData({ visible: true, data })
+        // setQueryParamsNew({ ...queryParamsNew, tencentCorpId: data.tencentCorpId })
+    }
+
+    return <>
+        <div className="flexStart" style={{ gap: 8, marginBottom: 16 }}>
+            <Select
+                showSearch
+                placeholder="请选择企业"
+                filterOption={(input, option) =>
+                    ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                }
+                style={{ width: 200 }}
+                allowClear
+                value={queryParamsNew?.tencentCorpId}
+                loading={getCorpRelationAll.loading}
+                onChange={(e) => {
+                    setQueryParamsNew({ ...queryParamsNew, tencentCorpId: e, pageNum: 1 })
+                }}
+                options={getCorpRelationAll?.data?.map((item: { corpName: string; tencentCorpId: string; }) => ({ label: item.corpName, value: item.tencentCorpId }))}
+            />
+            <Button type="primary" icon={<SearchOutlined />} loading={getCustomerServiceGroupList.loading} onClick={() => getCustomerServiceGroupList.refresh()}>刷新</Button>
+        </div>
+        <Table
+            columns={columns(handleEdit)}
+            dataSource={getCustomerServiceGroupList.data?.records}
+            size="small"
+            loading={getCustomerServiceGroupList?.loading}
+            scroll={{ y: 600, x: 1000 }}
+            rowKey={'groupId'}
+            pagination={{
+                total: getCustomerServiceGroupList.data?.total,
+                defaultPageSize: 20,
+                current: getCustomerServiceGroupList.data?.current,
+                pageSize: getCustomerServiceGroupList.data?.size
+            }}
+            onChange={(pagination) => {
+                const { current, pageSize } = pagination
+                setQueryParamsNew({ ...queryParamsNew, pageNum: current as number, pageSize: pageSize as number || 20 })
+            }}
+
+        />
+
+        {/* 设置修改 */}
+        {settingsData.visible && <SettingsEnterprise
+            {...settingsData}
+            adAccountId={adAccountId as number}
+            onClose={() => setSettingsData({ visible: false, data: {} })}
+            onChange={() => {
+                getCustomerServiceGroupList.refresh()
+                setSettingsData({ visible: false, data: {} })
+            }}
+        />}
+    </>
+};
+
+export default React.memo(Group);

+ 77 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/index.less

@@ -0,0 +1,77 @@
+.settingsEnterprise {
+    display: flex;
+    width: 100%;
+    height: 400px;
+
+    >div {
+        height: 100%;
+        overflow: hidden;
+    }
+
+    .settingsEnterprise-left {
+        flex: 1;
+        overflow: hidden;
+
+        >div {
+            width: 100%;
+            height: 100%;
+            overflow: hidden;
+            overflow-y: auto;
+            padding-top: 10px;
+            padding-left: 10px;
+            padding-right: 10px;
+            box-sizing: border-box;
+        }
+    }
+
+    .settingsEnterprise-right {
+        width: 350px;
+        background-color: rgb(250, 250, 250);
+        box-shadow: rgb(235, 235, 235) 1px 0px 0px 0px inset;
+        box-sizing: border-box;
+
+        >div {
+            width: 100%;
+            height: 100%;
+            overflow: hidden;
+            overflow-y: auto;
+            padding: 16px 20px;
+        }
+
+        .settingsEnterprise-right-title {
+            margin-bottom: 12px;
+            font-weight: 600;
+            color: #1f1f1f;
+
+            >span {
+                color: rgb(7, 193, 96);
+            }
+        }
+
+        .settingsEnterprise-right-list {
+            flex: 1;
+            overflow: hidden;
+        }
+
+        .list-item {
+            display: flex;
+            place-content: center space-between;
+            margin-bottom: 8px;
+            padding: 9px 12px;
+            background-color: rgb(255, 255, 255);
+            box-shadow: rgba(223, 223, 223, 0.5) 0px 0px 0px 1px, rgba(0, 0, 0, 0.04) 0px 3px 6px 0px;
+            border-radius: 6px;
+            animation-duration: 200ms !important;
+            transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+
+            >div {
+                flex: 1 1 0%;
+                display: flex;
+                align-items: center;
+                min-width: 0;
+                color: #828282;
+                gap: 6px;
+            }
+        }
+    }
+}

+ 45 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/index.tsx

@@ -0,0 +1,45 @@
+import { Card, Tabs } from 'antd';
+import React from 'react';
+import '../../tencentAdPutIn/index.less'
+import Group from './group';
+import SelectAdAccount from '@/components/SelectAdAccount';
+import WechatTieUp from './wechatTieUp';
+import PageTieUp from './pageTieUp';
+
+/**
+ * 客服管理
+ * @returns 
+ */
+const EnterpriseWechat: React.FC = () => {
+
+    /**********************************/
+    const [adAccountId, setAdAccountId] = React.useState<number>();
+    /**********************************/
+
+    return <Card
+        className="cardResetCss"
+    >
+        <div className="flexStart" style={{ gap: 8, marginBottom: 16 }}>
+            <SelectAdAccount
+                isReturnFirstValue
+                value={adAccountId ? [adAccountId] : undefined}
+                type="radio"
+                onChange={(value) => {
+                    setAdAccountId(value as number)
+                }}
+                allowClear={false}
+            />
+        </div>
+        
+        <Tabs
+            items={[
+                { label: '客服组', key: '1', children: <Group adAccountId={adAccountId} /> },
+                { label: '企微与投放端关系', key: '2', children: <WechatTieUp adAccountId={adAccountId} /> },
+                { label: '官方落地页与客服组关系', key: '3', children: <PageTieUp adAccountId={adAccountId} /> },
+            ]}
+            size="small"
+        />
+    </Card>
+};
+
+export default EnterpriseWechat;

+ 35 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/nTree.less

@@ -0,0 +1,35 @@
+
+
+.nTree-item {
+    padding: 3px;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    user-select: none;
+
+    &:hover {
+        background-color: #f5f5f5;
+    }
+}
+
+.nTree-switcher {
+    width: 24px;
+    height: 100%;
+    padding-left: 4px;
+    box-sizing: border-box;
+    font-size: 10px;
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+}
+
+.nTree-checkbox {
+    width: 24px;
+    height: 100%;
+    display: flex;
+    align-items: center;
+}
+
+.nTree-children {
+    padding-left: 24px;
+}

+ 193 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/nTree.tsx

@@ -0,0 +1,193 @@
+import { Checkbox, Empty, Typography } from 'antd';
+import { DataNode } from 'antd/lib/tree';
+import React, { useContext, useEffect } from 'react';
+import './nTree.less';
+import { CaretDownOutlined, CaretRightOutlined, LoadingOutlined } from '@ant-design/icons';
+
+interface ValueProps {
+    label: string,
+    value: string
+}
+interface NTreeReactContent {
+    expandedKeys: React.Key[],
+    setExpandedKeys: React.Dispatch<React.SetStateAction<React.Key[]>>
+    selected: ValueProps[]
+    setSelected: React.Dispatch<React.SetStateAction<ValueProps[]>>
+    onSelect?: (selectedKeys: ValueProps[]) => void
+}
+
+export const DispatchNTree = React.createContext<NTreeReactContent | null>(null);
+
+/**
+ * 展开组件
+ * @param param0 
+ * @returns 
+ */
+const OnSwitcher: React.FC<{
+    node: DataNode,
+    elKey: string,
+    loadData?: ((treeNode?: DataNode) => Promise<any>)
+}> = ({ node, elKey, loadData }) => {
+
+    /***********************************/
+    const { expandedKeys, setExpandedKeys } = useContext(DispatchNTree)!;
+    const { isLeaf } = node;
+    const [loading, setLoading] = React.useState<boolean>(false);
+    const [expanded, setExpanded] = React.useState<boolean>(false);
+    /***********************************/
+
+    return <div
+        className='nTree-switcher'
+        style={loading ? { cursor: 'progress' } : {}}
+        onClick={(e) => {
+            e.stopPropagation();
+            e.preventDefault();
+            if (loading || isLeaf) return;
+            if (expandedKeys.includes(elKey)) {
+                setExpandedKeys(expandedKeys.filter(item => item !== elKey))
+            } else {
+                if (expanded) {
+                    setExpandedKeys([...expandedKeys, elKey])
+                } else {
+                    if (typeof loadData === 'function') {
+                        setLoading(true);
+                        loadData(node).then(() => {
+                            setExpanded(true);
+                            setExpandedKeys([...expandedKeys, elKey]);
+                            setLoading(false);
+                        })
+                    } else {
+                        setExpandedKeys([...expandedKeys, elKey]);
+                        setExpanded(true);
+                    }
+                }
+            }
+        }}
+    >{loading ? <LoadingOutlined /> : !isLeaf && (expandedKeys.includes(elKey) ? <CaretDownOutlined /> : <CaretRightOutlined />)}</div>
+}
+
+const renderTitle = (title: React.ReactNode | ((data: DataNode) => React.ReactNode), data: DataNode): React.ReactNode => {
+    if (typeof title === 'function') {
+        return (title as (data: DataNode) => React.ReactNode)(data);
+    }
+    return title;
+};
+
+
+interface NTreeItemProps {
+    treeData?: DataNode[],
+    loadData?: ((treeNode?: DataNode) => Promise<any>),
+    fatherKey?: React.Key
+}
+
+/**
+ * 自定义树表item
+ * @returns 
+ */
+const NTreeItem: React.FC<NTreeItemProps> = ({ treeData, fatherKey, loadData }) => {
+
+    /*************************/
+    const { expandedKeys, selected, setSelected, onSelect } = useContext(DispatchNTree)!;
+    /*************************/
+
+    return <>
+        {treeData ? treeData?.map((item: DataNode, index: number) => {
+            const { title, key, children, checkable = true, disabled } = item;
+            const elKey = fatherKey ? fatherKey + '-' + index + '-' + key : index + '-' + key;
+            return <div key={elKey}>
+                <div
+                    className='nTree-item'
+                    style={{ cursor: checkable === false ? 'default' : 'pointer' }}
+                    onClick={(e) => {
+                        e.stopPropagation();
+                        e.preventDefault();
+                        if (checkable) {
+                            let newSelected = [...selected];
+                            if (selected.findIndex(item2 => item2.value === key) !== -1) {
+                                newSelected = newSelected.filter(item2 => item2.value !== key)
+                            } else {
+                                const item = { label: title as string, value: key as string }
+                                newSelected = [...newSelected, item];
+                            }
+                            setSelected(newSelected)
+                            onSelect?.(newSelected)
+                        }
+                    }}
+                >
+                    {/* 展开组件 */}
+                    <OnSwitcher
+                        node={item}
+                        elKey={elKey}
+                        loadData={loadData}
+                    />
+                    {checkable && <div className='nTree-checkbox'>
+                        <Checkbox
+                            checked={selected.findIndex(item => item.value === key) !== -1}
+                            disabled={disabled}
+                            onChange={(e) => {
+                                e.stopPropagation();
+                                e.preventDefault();
+                                let newSelected = [...selected];
+                                if (e.target.checked) {
+                                    const item = { label: title as string, value: key as string }
+                                    if (selected.findIndex(item2 => item2.value === key) !== -1) return;
+                                    newSelected = [...newSelected, item];
+                                } else {
+                                    newSelected = newSelected.filter(item2 => item2.value !== key)
+                                }
+                                setSelected(newSelected)
+                                onSelect?.(newSelected)
+                            }}
+                        ></Checkbox>
+                    </div>}
+                    <Typography.Text ellipsis>{renderTitle(title, item)}</Typography.Text>
+                </div>
+                {(children && expandedKeys.includes(elKey)) && <div className='nTree-children'>
+                    <NTreeItem
+                        treeData={children}
+                        loadData={loadData}
+                        fatherKey={elKey}
+                    />
+                </div>}
+            </div>
+        }) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
+    </>
+};
+
+
+
+/**
+ * 自定义树表
+ * @returns 
+ */
+const NTree: React.FC<{
+    treeData?: DataNode[],
+    loadData?: ((treeNode?: DataNode) => Promise<any>),
+    fatherKey?: React.Key
+    selectedKeys?: ValueProps[]
+    onSelect?: (selectedKeys: ValueProps[]) => void
+}> = ({ treeData, loadData, fatherKey, selectedKeys, onSelect }) => {
+
+    /****************************************/
+    const [expandedKeys, setExpandedKeys] = React.useState<React.Key[]>([]);
+    const [selected, setSelected] = React.useState<ValueProps[]>(selectedKeys || []);
+    /****************************************/
+
+    return <div className='nTree'>
+        <DispatchNTree.Provider value={{
+            expandedKeys,
+            setExpandedKeys,
+            selected,
+            setSelected,
+            onSelect
+        }}>
+            <NTreeItem
+                treeData={treeData}
+                loadData={loadData}
+                fatherKey={fatherKey}
+            />
+        </DispatchNTree.Provider>
+    </div>
+};
+
+export default React.memo(NTree);

+ 257 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/pageTieUp.tsx

@@ -0,0 +1,257 @@
+import { useAjax } from '@/Hook/useAjax';
+import { delPageCustomerGroupApi, getAdqLandingPageOfficialListApi, getCorpRelationAllApi, getCustomerServiceGroupListApi, getPageCustomerGroupListApi, GetPageCustomerGroupListProps } from '@/services/adqV3/global';
+import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
+import { Button, Input, message, Popconfirm, Select, Table } from 'antd';
+import React, { useEffect, useState } from 'react';
+import AddPageTieUp from './addPageTieUp';
+import { useDebounce } from 'ahooks';
+
+/**
+ * 落地页与客服组关系
+ * @returns 
+ */
+const PageTieUp: React.FC<{ adAccountId?: number }> = ({ adAccountId }) => {
+
+    /*******************************/
+    const [queryParamsNew, setQueryParamsNew] = useState<GetPageCustomerGroupListProps>({ pageNum: 1, pageSize: 50, adAccountId: 0 })
+    const [visible, setVisible] = useState<boolean>(false)
+    const [corpList, setCorpList] = useState<{ label: string, value: string }[]>([])
+    const [groupList, setGroupList] = useState<{ label: string, value: number }[]>([])
+    const [queryForm, setQueryForm] = useState<{ accountId?: number, pageName?: string, ownerUid?: number, pageSize: number, pageNum: number, isSqDownPage: boolean }>({ pageNum: 1, pageSize: 20, isSqDownPage: false })
+    const [pageName, setPageName] = useState<string>();
+    const debouncedValue = useDebounce(pageName, { wait: 500 });
+
+    const getPageCustomerGroupList = useAjax((params) => getPageCustomerGroupListApi(params))
+    const delPageCustomerGroup = useAjax((params) => delPageCustomerGroupApi(params))
+    const getCorpRelationAll = useAjax((params) => getCorpRelationAllApi(params))
+    const getCustomerServiceGroupList = useAjax((params) => getCustomerServiceGroupListApi(params))
+    const listAjax = useAjax((params) => getAdqLandingPageOfficialListApi(params))
+    /*******************************/
+
+    // 落地页
+    useEffect(() => {
+        if (adAccountId) {
+            const params: any = {
+                ...queryForm,
+                // pageStatus: 'NORMAL',
+                pageType: 'PAGE_TYPE_OFFICIAL',
+                accountId: adAccountId,
+                pageName: debouncedValue
+            }
+            delete params.isSqDownPage
+            listAjax.run(params)
+        }
+    }, [adAccountId, queryForm, debouncedValue])
+
+    /** 客服组 */
+    useEffect(() => {
+        if (adAccountId) {
+            getCustomerServiceGroupList.run({ adAccountId, pageNum: 1, pageSize: 100, tencentCorpId: queryParamsNew?.tencentCorpId }).then(res => {
+                setGroupList(res?.records?.map((item: { groupName: string; groupId: number; }) => ({ label: item.groupName, value: item.groupId })))
+            }).catch(() => setGroupList([]))
+        }
+    }, [adAccountId, queryParamsNew?.tencentCorpId])
+
+    /** 企业列表 */
+    useEffect(() => {
+        if (adAccountId) {
+            getCorpRelationAll.run({ adAccountId }).then(res => {
+                setCorpList(res?.map((item: { corpName: string; tencentCorpId: string; }) => ({ label: item.corpName, value: item.tencentCorpId })))
+            }).catch(() => setCorpList([]))
+        }
+    }, [adAccountId])
+
+    /** 获取关系列表 */
+    useEffect(() => {
+        if (adAccountId) {
+            getPageCustomerGroupList.run({ ...queryParamsNew, adAccountId })
+        }
+    }, [adAccountId, queryParamsNew])
+
+
+    /**
+     * 删除
+     * @param id 
+     */
+    const handleDel = (id: number) => {
+        delPageCustomerGroup.run(id).then((res) => {
+            if (res) {
+                message.success('删除成功')
+                getPageCustomerGroupList.refresh()
+            }
+        })
+    }
+
+
+    return <>
+        <div className="flexStart" style={{ gap: 8, marginBottom: 16 }}>
+            <Select
+                showSearch
+                placeholder="请选择企业"
+                filterOption={(input, option) =>
+                    ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                }
+                style={{ width: 200 }}
+                allowClear
+                value={queryParamsNew?.tencentCorpId}
+                loading={getCorpRelationAll.loading}
+                onChange={(e) => {
+                    setQueryParamsNew({ ...queryParamsNew, tencentCorpId: e, pageNum: 1 })
+                }}
+                options={corpList}
+            />
+            <Select
+                showSearch
+                placeholder="请选择客服组"
+                filterOption={(input, option) =>
+                    ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                }
+                style={{ width: 200 }}
+                allowClear
+                value={queryParamsNew?.customerGroupId}
+                loading={getCustomerServiceGroupList.loading}
+                onChange={(e) => {
+                    setQueryParamsNew({ ...queryParamsNew, customerGroupId: e, pageNum: 1 })
+                }}
+                options={groupList}
+            />
+            <Input style={{ width: 140 }} value={queryParamsNew?.customerGroupName} onChange={(e) => setQueryParamsNew({ ...queryParamsNew, pageNum: 1, customerGroupName: e.target.value })} placeholder='客服组名称' />
+            <Select
+                showSearch
+                placeholder="请选择落地页"
+                filterOption={false}
+                style={{ width: 200 }}
+                allowClear
+                value={queryParamsNew?.pageId}
+                loading={listAjax.loading}
+                onChange={(e) => {
+                    setQueryParamsNew({ ...queryParamsNew, pageId: e, pageNum: 1 })
+                }}
+                defaultActiveFirstOption={false}
+                showArrow={false}
+                onSearch={(newValue: string) => {
+                    setPageName(newValue)
+                }}
+                options={listAjax?.data?.records?.map((item: { pageId: number; pageName: string; }) => ({ label: item.pageName, value: item.pageId }))}
+            />
+            <Button type="primary" icon={<SearchOutlined />} loading={getPageCustomerGroupList.loading} onClick={() => getPageCustomerGroupList.refresh()}>刷新</Button>
+            <Button
+                type="primary"
+                icon={<PlusOutlined />}
+                onClick={() => {
+                    setVisible(true)
+                }}
+            >新增</Button>
+        </div>
+
+        <Table
+            columns={[
+                {
+                    title: '广告账号',
+                    dataIndex: 'adAccountId',
+                    key: 'adAccountId',
+                    width: 90,
+                    ellipsis: true,
+                    align: 'center',
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '落地页名称',
+                    dataIndex: 'pageName',
+                    key: 'pageName',
+                    width: 120,
+                    ellipsis: true,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '落地页ID',
+                    dataIndex: 'pageId',
+                    key: 'pageId',
+                    width: 100,
+                    ellipsis: true,
+                    align: 'center',
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '客服组名称',
+                    dataIndex: 'customerGroupName',
+                    key: 'customerGroupName',
+                    width: 120,
+                    ellipsis: true,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '客服组ID',
+                    dataIndex: 'customerGroupId',
+                    key: 'customerGroupId',
+                    align: 'center',
+                    width: 100,
+                    ellipsis: true,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '投放端企业ID',
+                    dataIndex: 'tencentCorpId',
+                    key: 'tencentCorpId',
+                    width: 300,
+                    ellipsis: true,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '操作',
+                    dataIndex: 'cz',
+                    key: 'cz',
+                    width: 300,
+                    render(_, record) {
+                        return <Popconfirm
+                            title="确定删除?"
+                            onConfirm={() => handleDel(record.id)}
+                        >
+                            <a style={{ color: 'red', fontSize: 12 }}>删除</a>
+                        </Popconfirm>
+                    },
+                },
+            ]}
+            dataSource={getPageCustomerGroupList.data?.records || []}
+            loading={getPageCustomerGroupList.loading}
+            scroll={{ x: 1000 }}
+            pagination={{
+                current: queryParamsNew.pageNum,
+                pageSize: queryParamsNew.pageSize,
+                total: getPageCustomerGroupList.data?.total,
+                onChange: (pageNum, pageSize) => setQueryParamsNew({ ...queryParamsNew, pageNum, pageSize }),
+            }}
+            size="small"
+            bordered
+            rowKey="id"
+        />
+
+        {/* 新增 */}
+        {visible && <AddPageTieUp
+            adAccountId={adAccountId as number}
+            corpList={corpList}
+            visible={visible}
+            onChange={() => {
+                getPageCustomerGroupList.refresh()
+                setVisible(false)
+            }}
+            onClose={() => {
+                setVisible(false)
+            }}
+        />}
+    </>;
+};
+
+export default React.memo(PageTieUp);

+ 120 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/selectPage.tsx

@@ -0,0 +1,120 @@
+import { Badge, Button, Input, Modal, Space, Table, Tag, Typography } from 'antd';
+import React, { useEffect, useState } from 'react';
+import '../../tencentAdPutIn/index.less'
+import { getAdqLandingPageOfficialListApi } from '@/services/adqV3/global';
+import { useAjax } from '@/Hook/useAjax';
+import { SyncOutlined } from '@ant-design/icons';
+import { PageStatusEnum } from '@/services/launchAdq/enum';
+const { Text } = Typography;
+
+/**
+ * 选择落地页
+ * @returns 
+ */
+const SelectPage: React.FC<{ accountId: number, value?: any, onChange?: (e: any) => void }> = ({ accountId, value, onChange }) => {
+
+    /***********************************/
+    const [visible, setVisible] = useState<boolean>(false)
+    const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>([])
+    const [queryForm, setQueryForm] = useState<{ accountId?: number, pageName?: string, ownerUid?: number, pageSize: number, pageNum: number, isSqDownPage: boolean }>({ pageNum: 1, pageSize: 20, isSqDownPage: false })
+    const listAjax = useAjax((params) => getAdqLandingPageOfficialListApi(params))
+    /***********************************/
+
+    useEffect(() => {
+        if (accountId) {
+            const params: any = {
+                ...queryForm,
+                // pageStatus: 'NORMAL',
+                pageType: 'PAGE_TYPE_OFFICIAL',
+                accountId
+            }
+            delete params.isSqDownPage
+            listAjax.run(params)
+        }
+    }, [accountId, queryForm])
+
+    const handleOk = () => {
+        onChange?.(selectedRowKeys?.[0])
+        setVisible(false)
+    }
+
+    return <>
+        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
+            {value && <div style={{ flex: 1, overflow: 'hidden' }}><Text ellipsis>{Array.isArray(value) ? value.map((item: { pageName: any; pageId: any; }) => `${item.pageName}(${item.pageId})`) : `${value?.pageName}(${value?.pageId})`}</Text></div>}
+            <Button type="primary" onClick={() => {
+                setVisible(true)
+                if (value) setSelectedRowKeys(Array.isArray(value) ? value : [value])
+                else setSelectedRowKeys([])
+            }}>选择官方落地页</Button>
+        </div>
+        {visible && <Modal
+            title={<strong>官方落地页</strong>}
+            open={visible}
+            onOk={handleOk}
+            onCancel={() => {
+                setVisible(false)
+            }}
+            className='modalResetCss'
+            width={750}
+        >
+            <Space style={{ marginBottom: 10 }}>
+                <Input value={queryForm?.pageName} style={{ width: 150 }} allowClear placeholder='请输入落地页名称' onChange={(e) => setQueryForm({ ...queryForm, pageNum: 1, pageName: e.target.value })} />
+                <Button style={{ padding: 0, margin: 0 }} icon={<SyncOutlined />} type='link' loading={listAjax?.loading} onClick={() => { listAjax?.refresh() }}><span style={{ fontSize: 12 }}>刷新</span></Button>
+            </Space>
+
+            <Table
+                columns={[
+                    {
+                        title: '落地页ID',
+                        dataIndex: 'pageId',
+                        key: 'pageId',
+                        align: 'center',
+                        width: 85
+                    },
+                    {
+                        title: '落地页名称',
+                        dataIndex: 'pageName',
+                        key: 'pageName',
+                        ellipsis: true,
+                        width: 300
+                    },
+                    {
+                        title: '落地页状态',
+                        dataIndex: 'pageStatus',
+                        key: 'pageStatus',
+                        align: 'center',
+                        width: 90,
+                        render: (a: string | number) => {
+                            return <Badge status={a === 'NORMAL' ? "success" : a === 'DELETED' ? "error" : 'processing'} text={PageStatusEnum[a as keyof typeof PageStatusEnum]} />
+                        }
+                    },
+                ]}
+                dataSource={listAjax?.data?.records}
+                size="small"
+                loading={listAjax?.loading}
+                scroll={{ y: 400 }}
+                bordered
+                rowKey={'pageId'}
+                pagination={{
+                    total: listAjax?.data?.total,
+                    defaultPageSize: 20,
+                    current: listAjax?.data?.current,
+                    pageSize: listAjax?.data?.size
+                }}
+                onChange={(pagination) => {
+                    const { current, pageSize } = pagination
+                    setQueryForm({ ...queryForm, pageNum: current as number, pageSize: pageSize as number || 20 })
+                }}
+                rowSelection={{
+                    selectedRowKeys: selectedRowKeys?.map((item: any) => item?.pageId),
+                    type: 'radio',
+                    onChange(selectedRowKeys, selectedRows, info) {
+                        setSelectedRowKeys(selectedRows)
+                    },
+                }}
+            />
+        </Modal>}
+    </>;
+};
+
+export default SelectPage;

+ 188 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/settingsEnterprise.tsx

@@ -0,0 +1,188 @@
+import { Modal, Spin, Typography } from 'antd';
+import React, { useEffect, useState } from 'react';
+import '../../tencentAdPutIn/index.less';
+import './index.less';
+import { getCorpDepartmentListApi, getWechatPagesCsgroupUserApi, updateCustomerServiceGroupApi } from '@/services/adqV3/global';
+import { CloseOutlined, UserOutlined } from '@ant-design/icons';
+import { useAjax } from '@/Hook/useAjax';
+import { DataNode } from 'antd/lib/tree';
+import NTree from './nTree';
+
+interface IProps {
+    adAccountId: number;
+    data: {
+        corpName: string;
+        tencentCorpId: string;
+        groupId: number;
+        groupName: string;
+        userInfoList: { userId: string, userName: string, msg: string, state: 0 | 1 }[]
+    };
+    visible: boolean;
+    onClose?: () => void;
+    onChange?: () => void;
+}
+
+// 定义接口
+interface Department {
+    corpId: string;
+    departmentId: number;
+    name: string;
+    parentid: number;
+    orderIn: number;
+    createTime: string;
+    children: Department[];
+}
+
+const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[], childrenLen?: number): DataNode[] => list.map(node => {
+    if (node.key === key) {
+        if (childrenLen && node?.children) {
+            return { ...node, children: [...node.children, ...children,] };
+        }
+        return {
+            ...node,
+            children,
+        };
+    }
+    if (node.children) {
+        return {
+            ...node,
+            children: updateTreeData(node.children, key, children, childrenLen),
+        };
+    }
+    return node;
+});
+
+/**
+ * 初始化数据
+ * @param data 
+ * @returns 
+ */
+const getInitializationData = (data: Department[]): DataNode[] => {
+    return data.map((item: Department) => {
+        return { title: item.name, key: item.departmentId, checkable: false, children: item?.children?.length ? getInitializationData(item.children) : [] }
+    })
+}
+
+/**
+ * 修改客服组
+ * @returns 
+ */
+const SettingsEnterprise: React.FC<IProps> = ({ adAccountId, data, visible, onClose, onChange }) => {
+
+    /*********************************/
+    const [treeData, setTreeData] = useState<DataNode[]>([]);
+    const [loading, setLoading] = useState<boolean>(false);
+    const [userInfoList, setUserInfoList] = useState<{ userId: string, userName: string, state?: 0 | 1, msg?: string }[]>(data.userInfoList || [])
+
+    const getCorpDepartmentList = useAjax((params) => getCorpDepartmentListApi(params))
+    const updateCustomerServiceGroup = useAjax((params) => updateCustomerServiceGroupApi(params))
+    /*********************************/
+
+    useEffect(() => {
+        const getData = async () => {
+            try {
+                const res = await getCorpDepartmentList.run({ corpName: data.corpName })
+                let dataTree: DataNode[] = []
+                if (res?.length > 0) {
+                    dataTree = getInitializationData(res)
+                }
+                setLoading(true)
+                const list = await getWechatPagesCsgroupUserApi({
+                    adAccountId,
+                    tencentCorpId: data.tencentCorpId,
+                    corpName: data.corpName
+                })
+                if (list.data?.length) {
+                    dataTree.push(...list.data.map((item: any) => ({ title: item.userName, key: item.userId, isLeaf: true })))
+                }
+                setLoading(false)
+                setTreeData(dataTree)
+            } catch (error) {
+                console.error(error)
+            }
+        }
+        if (adAccountId)
+            getData()
+    }, [adAccountId])
+
+    const handleOk = () => {
+
+        updateCustomerServiceGroup.run({
+            adAccountId,
+            tencentCorpId: data.tencentCorpId,
+            groupId: data.groupId,
+            groupName: data.groupName,
+            userIdList: userInfoList.map(item => item.userId)
+        }).then((res) => {
+            if (res) {
+                onChange?.()
+            }
+        })
+    }
+
+    const onLoadData = ({ key, children }: any) => {
+        return new Promise<void>(resolve => {
+            getWechatPagesCsgroupUserApi({
+                adAccountId,
+                tencentCorpId: data.tencentCorpId,
+                corpName: data.corpName,
+                departmentId: key
+            }).then((res) => {
+                setTreeData(origin =>
+                    updateTreeData(origin, key, res.data.map((item: any) => ({ title: item.userName, key: item.userId, isLeaf: true })), children?.length),
+                );
+                resolve();
+            })
+        });
+    }
+
+
+    return <Modal
+        title={<strong>设置客服组</strong>}
+        open={visible}
+        onCancel={onClose}
+        onOk={handleOk}
+        className='modalResetCss'
+        width={750}
+        bodyStyle={{ padding: 0 }}
+        confirmLoading={updateCustomerServiceGroup.loading}
+    >
+        <Spin spinning={getCorpDepartmentList.loading || loading}>
+            <div className='settingsEnterprise'>
+                <div className='settingsEnterprise-left'>
+                    <div>
+                        <NTree
+                            treeData={treeData}
+                            loadData={onLoadData}
+                            onSelect={(selectedKeys) => {
+                                console.log('selected', selectedKeys);
+                                setUserInfoList(selectedKeys.map((item: any) => ({ userId: item.value, userName: item.label, ...item })))
+                            }}
+                            selectedKeys={userInfoList.map(({ userId, userName, ...item }) => ({ value: userId, label: userName, ...item }))}
+                        />
+                    </div>
+                </div>
+                <div className='settingsEnterprise-right'>
+                    <div>
+                        <div className='settingsEnterprise-right-title'>已选 <span>{userInfoList?.length || 0}</span> 个客服</div>
+                        <div className='settingsEnterprise-right-list'>
+                            {userInfoList.map(item => {
+                                return <div className='list-item' key={item.userId}>
+                                    <div style={item?.state === 1 ? { color: 'red' } : {}}>
+                                        <UserOutlined />
+                                        <Typography.Text ellipsis style={item?.state === 1 ? { color: 'red' } : {}}>{item?.userName || item.msg || item.userId}</Typography.Text>
+                                    </div>
+                                    <a onClick={() => {
+                                        setUserInfoList(userInfoList.filter(item2 => item2.userId !== item.userId))
+                                    }}><CloseOutlined /></a>
+                                </div>
+                            })}
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </Spin>
+    </Modal>
+};
+
+export default React.memo(SettingsEnterprise);

+ 70 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/tableConfig.tsx

@@ -0,0 +1,70 @@
+import { Button, Space, TableProps } from "antd"
+import React from "react"
+import UserInfo from "./userInfo"
+import { EditOutlined } from "@ant-design/icons"
+
+let columns = (handleEdit: (data: any) => void): TableProps<any>['columns'] => {
+
+    return [
+        {
+            title: '客服组名称',
+            dataIndex: 'groupName',
+            key: 'groupName',
+            width: 300,
+            render(value, records) {
+                return <span style={{ fontSize: 12 }}>{value}(ID:{records.groupId})</span>
+            }
+        },
+        {
+            title: '客服人数',
+            dataIndex: 'groupMemberCnt',
+            key: 'groupMemberCnt',
+            width: 75,
+            align: 'center',
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value}</span>
+            }
+        },
+        {
+            title: '创建时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            width: 150,
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value}</span>
+            }
+        },
+        {
+            title: '企业名称',
+            dataIndex: 'corpName',
+            key: 'corpName',
+            width: 110,
+            align: 'center',
+            ellipsis: true,
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value}</span>
+            }
+        },
+        {
+            title: '企业ID',
+            dataIndex: 'tencentCorpId',
+            key: 'tencentCorpId',
+            width: 310,
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value}</span>
+            }
+        },
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            render(_, record) {
+                return <Space>
+                    <UserInfo userInfoList={record.userInfoList} createTime={record.createTime} groupMemberCnt={record.groupMemberCnt}/>
+                    <Button icon={<EditOutlined />} style={{ border: 'none', fontSize: 12 }} size='small' onClick={() => handleEdit(record)}>修改</Button>
+                </Space>
+            }
+        }
+    ]
+}
+export default columns

+ 41 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/userInfo.tsx

@@ -0,0 +1,41 @@
+import { UnorderedListOutlined } from '@ant-design/icons';
+import { Button, Modal } from 'antd';
+import React from 'react';
+import '../../tencentAdPutIn/index.less';
+
+/**
+ * UserInfo是一个React函数组件,用于展示用户信息页面
+ * 该组件没有参数和返回值,它通过返回一个包含用户信息页面的JSX元素来工作
+ * 选择使用函数组件而不是类组件是因为用户信息页面通常不涉及复杂的组件状态管理
+ * 此外,通过使用函数组件,可以利用React的Hooks特性,进一步简化组件逻辑
+ */
+const UserInfo: React.FC<{ createTime: string, groupMemberCnt: number, userInfoList: { userId: string, userName: string, msg: string, state: 0 | 1 }[] }> = ({ createTime, groupMemberCnt, userInfoList }) => {
+    // 返回一个包含用户信息页面布局的JSX元素
+    // 这里使用了简单的HTML元素来构建页面结构,包括一个标题和一段描述
+
+    /************************************/
+    const [visible, setVisible] = React.useState<boolean>(false);
+    /************************************/
+
+    return <>
+        <Button icon={<UnorderedListOutlined />} style={{ border: 'none', fontSize: 12 }} size='small' onClick={() => setVisible(true)}>详情</Button>
+        {visible && <Modal
+            title={<strong>客服组详情</strong>}
+            open={visible}
+            onCancel={() => setVisible(false)}
+            footer={null}
+            className='modalResetCss'
+        >
+            <p>创建时间:{createTime}</p>
+            <p>客服认识:{groupMemberCnt}</p>
+            <div style={{ display: 'flex' }}>
+                <p>已选客服:</p>
+                <div>{userInfoList.map(item => {
+                    return <div key={item.userId} style={item.state === 1 ? { color: 'red' } : {}}>{item.userName || item.msg + `(${item.userId})`}</div>
+                })}</div>
+            </div>
+        </Modal>}
+    </>;
+};
+
+export default React.memo(UserInfo);

+ 180 - 0
src/pages/launchSystemV3/tencenTasset/enterpriseWechat/wechatTieUp.tsx

@@ -0,0 +1,180 @@
+import { useAjax } from '@/Hook/useAjax';
+import { delCorpRelationApi, getCorpRelationListApi } from '@/services/adqV3/global';
+import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
+import { Button, message, Popconfirm, Table } from 'antd';
+import React, { useEffect, useState } from 'react';
+import AddWechatTieUp from './addWechatTieUp';
+
+
+/**
+ * 企微关系链
+ * @returns 
+ */
+const WechatTieUp: React.FC<{ adAccountId?: number }> = ({ adAccountId }) => {
+
+    /**********************************/
+    const [queryParamsNew, setQueryParamsNew] = useState<{ pageNum: number, pageSize: number }>({ pageNum: 1, pageSize: 10 })
+    const [visible, setVisible] = useState<boolean>(false)
+
+    const getCorpRelationList = useAjax((params) => getCorpRelationListApi(params))
+    const delCorpRelation = useAjax((params) => delCorpRelationApi(params))
+    /**********************************/
+
+    useEffect(() => {
+        if (adAccountId) {
+            getCorpRelationList.run({ adAccountId, ...queryParamsNew })
+        }
+    }, [adAccountId, queryParamsNew]);
+
+    /**
+     * 删除
+     * @param id 
+     */
+    const handleDel = (id: number) => {
+        delCorpRelation.run(id).then((res) => {
+            if (res) {
+                message.success('删除成功')
+                getCorpRelationList.refresh()
+            }
+        })
+    }
+
+    return <>
+        <div className="flexStart" style={{ gap: 8, marginBottom: 16 }}>
+            <Button type="primary" icon={<SearchOutlined />} loading={getCorpRelationList.loading} onClick={() => getCorpRelationList.refresh()}>刷新</Button>
+            <Button
+                type="primary"
+                icon={<PlusOutlined />}
+                onClick={() => {
+                    setVisible(true)
+                }}
+            >新增</Button>
+        </div>
+
+        <Table
+            columns={[
+                {
+                    title: '企业名称',
+                    dataIndex: 'corpName',
+                    key: 'corpName',
+                    width: 150,
+                    ellipsis: true,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '企业ID',
+                    dataIndex: 'corpId',
+                    key: 'corpId',
+                    width: 300,
+                    ellipsis: true,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '投放端企业ID',
+                    dataIndex: 'tencentCorpId',
+                    key: 'tencentCorpId',
+                    width: 300,
+                    ellipsis: true,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '广告账户',
+                    dataIndex: 'adAccountId',
+                    key: 'adAccountId',
+                    align: 'center',
+                    width: 90,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '创建时间',
+                    dataIndex: 'createTime',
+                    key: 'createTime',
+                    width: 140,
+                    ellipsis: true,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '创建人',
+                    dataIndex: 'createByName',
+                    key: 'createByName',
+                    align: 'center',
+                    width: 80,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '更新时间',
+                    dataIndex: 'updateTime',
+                    key: 'updateTime',
+                    width: 140,
+                    ellipsis: true,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '更新人',
+                    dataIndex: 'updateByName',
+                    key: 'updateByName',
+                    align: 'center',
+                    width: 80,
+                    render(value) {
+                        return <span style={{ fontSize: 12 }}>{value}</span>
+                    },
+                },
+                {
+                    title: '操作',
+                    dataIndex: 'cz',
+                    key: 'cz',
+                    width: 300,
+                    render(_, record) {
+                        return <Popconfirm
+                            title="确定删除?"
+                            onConfirm={() => handleDel(record.id)}
+                        >
+                            <a style={{ color: 'red', fontSize: 12 }}>删除</a>
+                        </Popconfirm>
+                    },
+                },
+            ]}
+            dataSource={getCorpRelationList.data?.records || []}
+            loading={getCorpRelationList.loading}
+            scroll={{ x: 1000 }}
+            pagination={{
+                current: queryParamsNew.pageNum,
+                pageSize: queryParamsNew.pageSize,
+                total: getCorpRelationList.data?.total,
+                onChange: (pageNum, pageSize) => setQueryParamsNew({ pageNum, pageSize }),
+            }}
+            size="small"
+            bordered
+            rowKey="id"
+        />
+
+        {/* 新增 */}
+        {visible && <AddWechatTieUp
+            adAccountId={adAccountId as number}
+            visible={visible}
+            onChange={() => {
+                getCorpRelationList.refresh()
+                setVisible(false)
+            }}
+            onClose={() => {
+                setVisible(false)
+            }}
+        />}
+    </>;
+};
+
+export default React.memo(WechatTieUp);

+ 90 - 0
src/pages/launchSystemV3/tencenTasset/manageComponent/addComponents.tsx

@@ -0,0 +1,90 @@
+import React from 'react';
+import '../../tencentAdPutIn/index.less'
+import { Button, Form, message, Modal, Space } from 'antd';
+
+interface Props {
+    visible?: boolean;
+    onClose?: () => void;
+    onChange?: () => void;
+}
+
+/**
+ * 新建组件
+ * @param param0 
+ * @returns 
+ */
+const AddComponents: React.FC<Props> = ({ visible, onChange, onClose }) => {
+
+    /****************************************/
+    const [form] = Form.useForm();
+    /****************************************/
+
+    // <Dropdown menu={{
+    //     items: [
+    //         {
+    //             key: '1',
+    //             label: (
+    //                 <a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
+    //                     1st menu item
+    //                 </a>
+    //             ),
+    //         }
+    //     ]
+    // }}>
+    //     <Button icon={<PlusOutlined />} type="primary">新建组件</Button>
+    // </Dropdown>
+
+    const handleOk = (values: any) => {
+        // const { mediaType, dynamicGroup } = values
+        // if (mediaType === 2 && dynamicGroup.length > adLength) {
+        //     message.error({
+        //         content: `创意组分配规则选择“顺序分配到广告”时,创意组总数必须小于等于广告总数。当前广告总数:${adLength},创意组总数:${dynamicGroup.length}`,
+        //         duration: 8
+        //     })
+        //     return
+        // }
+        // onChange?.({ mediaType, dynamicMaterialDTos: { dynamicGroup } })
+    }
+
+    return <Modal
+        title={<strong style={{ fontSize: 20 }}>新建组件</strong>}
+        open={visible}
+        onCancel={onClose}
+        footer={null}
+        width={900}
+        className={`modalResetCss`}
+        bodyStyle={{ padding: '0 0 40px', position: 'relative', borderRadius: '0 0 8px 8px' }}
+        maskClosable={false}
+    >
+        <Form
+            form={form}
+            name="newComponents"
+            labelAlign='left'
+            layout="vertical"
+            colon={false}
+            style={{ backgroundColor: '#f1f4fc', maxHeight: 650, overflow: 'hidden', overflowY: 'auto', padding: 10, borderRadius: '0 0 8px 8px' }}
+            scrollToFirstError={{
+                behavior: 'smooth',
+                block: 'center'
+            }}
+            onFinishFailed={({ errorFields }) => {
+                message.error(errorFields?.[0]?.errors?.[0])
+            }}
+            onFinish={handleOk}
+            initialValues={{
+                dynamicGroup: [undefined]
+            }}
+        >
+            <Form.Item className="submit_pull">
+                <Space>
+                    <Button onClick={onClose}>取消</Button>
+                    <Button type="primary" htmlType="submit" className="modalResetCss">
+                        确定
+                    </Button>
+                </Space>
+            </Form.Item>
+        </Form>
+    </Modal>
+};
+
+export default AddComponents;

+ 426 - 0
src/pages/launchSystemV3/tencenTasset/manageComponent/const.ts

@@ -0,0 +1,426 @@
+/** 二级视频组件类型 */
+export const DEFAULT_COMPONENT_SUB_VIDEO_TYPE = [
+    {
+        label: '16:9 视频',
+        value: 'VIDEO_16X9'
+    },
+    {
+        label: '9:16 视频',
+        value: 'VIDEO_9X16'
+    },
+    {
+        label: '4:3 视频',
+        value: 'VIDEO_4X3'
+    },
+    {
+        label: '橱窗视频',
+        value: 'VIDEO_SHOWCASE'
+    },
+    {
+        label: '橱窗视频',
+        value: 'SHORT_VIDEO_4X3'
+    }
+]
+
+/** 二级图片组件类型 */
+export const DEFAULT_COMPONENT_SUB_IMAGE_TYPE = [
+    {
+        label: '16:9单图',
+        value: 'IMAGE_16X9'
+    },
+    {
+        label: '9:16单图',
+        value: 'IMAGE_9X16'
+    },
+    {
+        label: '1:1单图',
+        value: 'IMAGE_1X1'
+    },
+    {
+        label: '20:7banner图',
+        value: 'IMAGE_20X7'
+    },
+    {
+        label: '3:4单图',
+        value: 'IMAGE_3X4'
+    },
+    {
+        label: '4:3单图',
+        value: 'IMAGE_4X3'
+    },
+    {
+        label: '3:2单图',
+        value: 'IMAGE_3X2'
+    },
+    {
+        label: '7:2通栏大图',
+        value: 'IMAGE_7X2'
+    },
+    {
+        label: '5:4单图',
+        value: 'IMAGE_5X4'
+    },
+    {
+        label: '4:5单图',
+        value: 'IMAGE_4X5'
+    },
+    {
+        label: '1:1三图',
+        value: 'IMAGE_LIST_1X1_3'
+    },
+    {
+        label: '1:1四图',
+        value: 'IMAGE_LIST_1X1_4'
+    },
+    {
+        label: '1:1六图',
+        value: 'IMAGE_LIST_1X1_6'
+    },
+    {
+        label: '1:1九图',
+        value: 'IMAGE_LIST_1X1_9'
+    },
+    {
+        label: '3:2三图',
+        value: 'IMAGE_LIST_3X2_3'
+    }
+]
+
+/** 默认展示图片组件类型 */
+export const DEFAULT_COMPONENT_SUB_SHOW_IMAGE = [
+    "SEARCH_IMAGE_1X1",
+    "SEARCH_IMAGE_BIG_20X7",
+    "SEARCH_IMAGE_16X9",
+    "SEARCH_IMAGE_LIST_1X1",
+    "SEARCH_IMAGE_LIST_9X16",
+    "IMAGE_16X9",
+    "IMAGE_9X16",
+    "IMAGE_1X1",
+    "IMAGE_LIST_1X1_1",
+    "IMAGE_20X7",
+    "IMAGE_100X9",
+    "IMAGE_3X4",
+    "IMAGE_4X3",
+    "IMAGE_3X2",
+    "IMAGE_7X2",
+    "IMAGE_5X4",
+    "IMAGE_4X5",
+    "IMAGE_LIST_1X1_3",
+    "IMAGE_LIST_1X1_4",
+    "IMAGE_LIST_1X1_6",
+    "IMAGE_LIST_1X1_9",
+    "IMAGE_LIST_3X2_3",
+    "ELEMENT_STORY",
+    "IMAGE_LIST_16X9_1",
+    "IMAGE_SHOWCASE"
+]
+
+/** 默认展示视频组件类型 */
+export const DEFAULT_COMPONENT_SUB_SHOW_VIDEO = [
+    "SEARCH_VIDEO_16X9_IMAGE_16X9",
+    "SEARCH_VIDEO_9X16_IMAGE_9X16",
+    "VIDEO_16X9",
+    "VIDEO_9X16",
+    "VIDEO_4X3",
+    "SHORT_VIDEO_4X3",
+    "APP_PROMOTION_VIDEO_16X9",
+    "VIDEO_SHOWCASE"
+]
+
+/** 潜力状态 */
+export enum COMMON_POTENTIAL_STATUS_ENUM {
+    COMMON_POTENTIAL_STATUS_DEFAULT = '潜力暂无判断',
+    COMMON_POTENTIAL_STATUS_LOW = '潜力低',
+    COMMON_POTENTIAL_STATUS_HIGH = '潜力高'
+}
+
+
+/** 来源 */
+export enum COMPONENT_GENERATION_TYPE_ENUM {
+    COMPONENT_GENERATION_TYPE_USER_CREATE = '客户素材',
+    COMPONENT_GENERATION_TYPE_SYSTEM_DERIVE = 'AIGC推荐'
+}
+
+/** 所有二级组件类型 */
+export const COMPONENT_SUB_TYPE = [
+    {
+        "label": "16:9 视频",
+        "value": "VIDEO_16X9"
+    },
+    {
+        "label": "9:16 视频",
+        "value": "VIDEO_9X16"
+    },
+    {
+        "label": "4:3 视频",
+        "value": "VIDEO_4X3"
+    },
+    {
+        "label": "橱窗视频",
+        "value": "VIDEO_SHOWCASE"
+    },
+    {
+        "label": "4:3 视频",
+        "value": "SHORT_VIDEO_4X3"
+    },
+    {
+        "label": "16:9 单图",
+        "value": "IMAGE_16X9"
+    },
+    {
+        "label": "9:16 单图",
+        "value": "IMAGE_9X16"
+    },
+    {
+        "label": "1:1 单图",
+        "value": "IMAGE_1X1"
+    },
+    {
+        "label": "3:2 单图",
+        "value": "IMAGE_3X2"
+    },
+    {
+        "label": "3:4 单图",
+        "value": "IMAGE_3X4"
+    },
+    {
+        "label": "4:3 单图",
+        "value": "IMAGE_4X3"
+    },
+    {
+        "label": "5:4 单图",
+        "value": "IMAGE_5X4"
+    },
+    {
+        "label": "4:5 单图",
+        "value": "IMAGE_4X5"
+    },
+    {
+        "label": "20:7banner 图",
+        "value": "IMAGE_20X7"
+    },
+    {
+        "label": "7:2 通栏大图",
+        "value": "IMAGE_7X2"
+    },
+    {
+        "label": "橱窗图片",
+        "value": "IMAGE_SHOWCASE"
+    },
+    {
+        "label": "100:9 PC 横版通栏",
+        "value": "IMAGE_100X9"
+    },
+    {
+        "label": "9:16 四图",
+        "value": "IMAGE_LIST_9X16_4"
+    },
+    {
+        "label": "1:1 三图",
+        "value": "IMAGE_LIST_1X1_3"
+    },
+    {
+        "label": "1:1 四图",
+        "value": "IMAGE_LIST_1X1_4"
+    },
+    {
+        "label": "1:1 六图",
+        "value": "IMAGE_LIST_1X1_6"
+    },
+    {
+        "label": "3:2 三图",
+        "value": "IMAGE_LIST_3X2_3"
+    },
+    {
+        "label": "1:1 一图",
+        "value": "IMAGE_LIST_1X1_1"
+    },
+    {
+        "label": "16:9 一图",
+        "value": "IMAGE_LIST_16X9_1"
+    },
+    {
+        "label": "1:1 九图",
+        "value": "IMAGE_LIST_1X1_9"
+    },
+    {
+        "label": "集装箱创意组合组件",
+        "value": "ELEMENT_STORY"
+    },
+    {
+        "label": "文案",
+        "value": "DESCRIPTION"
+    },
+    {
+        "label": "标题",
+        "value": "TITLE"
+    },
+    {
+        "label": "行动按钮",
+        "value": "ACTION_BUTTON"
+    },
+    {
+        "label": "标签",
+        "value": "LABEL"
+    },
+    {
+        "label": "数据展示",
+        "value": "SHOW_DATA"
+    },
+    {
+        "label": "浮层卡片-图文复合",
+        "value": "FLOATING_ZONE_IMAGE_TEXT"
+    },
+    {
+        "label": "浮层卡片-单图",
+        "value": "FLOATING_ZONE_IMAGE"
+    },
+    {
+        "label": "弹幕",
+        "value": "BARRAGE"
+    },
+    {
+        "label": "礼包组件",
+        "value": "APP_GIFT_PACK_CODE"
+    },
+    {
+        "label": "卖点图",
+        "value": "SHOP_IMAGE"
+    },
+    {
+        "label": "挂件",
+        "value": "MARKETING_PENDANT"
+    },
+    {
+        "label": "选择按钮",
+        "value": "CHOSEN_BUTTON"
+    },
+    {
+        "label": "倒计时",
+        "value": "COUNT_DOWN"
+    },
+    {
+        "label": "轮播文案",
+        "value": "LIVING_DESC"
+    },
+    {
+        "label": "朋友圈文字链",
+        "value": "TEXT_LINK"
+    },
+    {
+        "label": "视频结束页",
+        "value": "END_PAGE"
+    },
+    {
+        "label": "试玩页",
+        "value": "WXGAME_PLAYABLE_PAGE"
+    },
+    {
+        "label": "首评回复组件",
+        "value": "SOCIAL_SKILL"
+    },
+    {
+        "label": "图文链接组件",
+        "value": "MINI_CARD_LINK"
+    },
+    {
+        "label": "浮层卡片-图文轮播",
+        "value": "FLOATING_ZONE_IMAGE_TEXT_LIST"
+    },
+    {
+        "label": "客服问答组件",
+        "value": "CONSULT_LINK"
+    },
+    {
+        "label": "商品卡片组件",
+        "value": "SHOP_PRODUCT_CARD"
+    },
+    {
+        "label": "自定义",
+        "value": "BRAND"
+    },
+    {
+        "label": "品牌简介页",
+        "value": "BRAND_PAGE"
+    },
+    {
+        "label": "搜一搜超级品专",
+        "value": "BRAND_SEARCH"
+    },
+    {
+        "label": "视频号",
+        "value": "BRAND_WECHAT_CHANNEL"
+    },
+    {
+        "label": "公众号",
+        "value": "BRAND_WECHAT"
+    },
+    {
+        "label": "企业微信",
+        "value": "BRAND_WECOM"
+    },
+    {
+        "label": "官方落地页",
+        "value": "JUMP_INFO_OFFICIAL"
+    },
+    {
+        "label": "自定义",
+        "value": "JUMP_INFO_H5"
+    },
+    {
+        "label": "微信小程序",
+        "value": "JUMP_INFO_WECHAT_MINI_PROGRAM"
+    },
+    {
+        "label": "微信客服",
+        "value": "JUMP_INFO_WECHAT_CONSULT"
+    },
+    {
+        "label": "企业微信",
+        "value": "JUMP_INFO_WECOM_CONSULT"
+    },
+    {
+        "label": "视频号观看直播",
+        "value": "JUMP_INFO_WECHAT_CHANNELS_WATCH_LIVE"
+    },
+    {
+        "label": "视频号视频详情页",
+        "value": "JUMP_INFO_WECHAT_CHANNELS_FEED"
+    },
+    {
+        "label": "微信公众号详情页",
+        "value": "JUMP_INFO_WECHAT_OFFICIAL_ACCOUNT_DETAIL"
+    },
+    {
+        "label": "微信小游戏",
+        "value": "JUMP_INFO_WECHAT_MINI_GAME"
+    },
+    {
+        "label": "安卓默认下载落地页",
+        "value": "JUMP_INFO_ANDROID_APP"
+    },
+    {
+        "label": "IOS 默认下载落地页",
+        "value": "JUMP_INFO_IOS_APP"
+    },
+    {
+        "label": "一键下载",
+        "value": "JUMP_INFO_ANDROID_DIRECT_DOWNLOAD"
+    },
+    {
+        "label": "厂商直达",
+        "value": "JUMP_INFO_APP_MARKET"
+    },
+    {
+        "label": "应用直达",
+        "value": "JUMP_INFO_APP_DEEP_LINK"
+    },
+    {
+        "label": "视频号小店商品详情页",
+        "value": "JUMP_INFO_WECHAT_CHANNELS_SHOP_PRODUCT"
+    },
+    {
+        "label": "QQ 小游戏",
+        "value": "JUMP_INFO_QQ_MINI_GAME"
+    }
+]

+ 45 - 0
src/pages/launchSystemV3/tencenTasset/manageComponent/index.less

@@ -0,0 +1,45 @@
+.manageComponent {
+    display: flex;
+    min-width: 1200px;
+    height: calc(100vh - 98px);
+    flex-direction: column;
+    gap: 6px;
+
+    .cardResetCss .ant-tabs-nav {
+        margin-bottom: 0;
+    }
+
+    .manageComponent-content {
+        flex: 1;
+        overflow: hidden;
+
+        >.ant-spin-nested-loading {
+            height: 100%;
+            overflow: hidden;
+
+            >.ant-spin-container {
+                height: 100%;
+                overflow: hidden;
+            }
+        }
+
+        .content_scroll {
+            height: 100%;
+            overflow: hidden;
+        }
+    }
+
+    .manageComponent-header {
+        padding: 10px 16px;
+        border-bottom: 1px solid #e8e8e8;
+        display: flex;
+        gap: 8px;
+        align-items: center;
+        flex-wrap: wrap;
+    }
+
+    .manageComponent-footer {
+        padding: 10px 16px;
+        border-top: 1px solid #e8e8e8;
+    }
+}

+ 187 - 3
src/pages/launchSystemV3/tencenTasset/manageComponent/index.tsx

@@ -1,4 +1,17 @@
-import React from "react"
+import React, { useEffect, useRef, useState } from "react"
+import '../../tencentAdPutIn/index.less'
+import { Button, Card, DatePicker, Dropdown, Input, Pagination, Select, Space, Spin, Table, Tabs } from "antd"
+import { getCreativeComponentListApi, GetCreativeComponentProps } from "@/services/adqV3/global"
+import './index.less'
+import { PlusOutlined } from "@ant-design/icons"
+import { useAjax } from "@/Hook/useAjax"
+import SelectAdAccount from "@/components/SelectAdAccount"
+import { COMMON_POTENTIAL_STATUS_ENUM, COMPONENT_GENERATION_TYPE_ENUM, DEFAULT_COMPONENT_SUB_IMAGE_TYPE, DEFAULT_COMPONENT_SUB_SHOW_IMAGE, DEFAULT_COMPONENT_SUB_SHOW_VIDEO, DEFAULT_COMPONENT_SUB_VIDEO_TYPE } from "./const"
+import moment from "moment"
+import { useDebounce, useSize } from "ahooks"
+import TableConfig from "./tableConfig"
+const COMPONENT_SUB_IMAGE_ENUM = DEFAULT_COMPONENT_SUB_IMAGE_TYPE.map(item => item.value)
+const COMPONENT_SUB_VIDEO_ENUM = DEFAULT_COMPONENT_SUB_VIDEO_TYPE.map(item => item.value)
 
 /**
  * 创意组件
@@ -6,9 +19,180 @@ import React from "react"
  */
 const ManageComponent: React.FC = () => {
 
+    /*************************************/
+    const [queryParams, setQueryParams] = React.useState<GetCreativeComponentProps>({ pageNum: 1, pageSize: 20, activeKey: 'IMAGE' })
+    const [idSting, setIdSting] = useState<string>();
+    const debouncedIdSting = useDebounce(idSting, { wait: 500 });
+    const ref = useRef<HTMLDivElement>(null);
+    const size = useSize(ref);
 
-    return <div>
-        111111111111111
+    const getCreativeComponentList = useAjax((params) => getCreativeComponentListApi(params))
+    /*************************************/
+
+    useEffect(() => {
+        if (queryParams?.adAccountId) {
+            const { activeKey, componentSubType, ...params } = queryParams
+            if (debouncedIdSting) {
+                params[activeKey === 'IMAGE' ? 'imageId' : 'videoId'] = debouncedIdSting.split(/[,,\n\s]+/ig).filter((item: any) => item)
+            } else {
+                delete params?.[activeKey === 'IMAGE' ? 'imageId' : 'videoId']
+            }
+            getCreativeComponentList.run({ ...params, componentSubType: componentSubType || (activeKey === 'IMAGE' ? DEFAULT_COMPONENT_SUB_SHOW_IMAGE : DEFAULT_COMPONENT_SUB_SHOW_VIDEO) })
+        }
+    }, [queryParams, debouncedIdSting])
+
+    return <div className="manageComponent">
+        <Card
+            className="cardResetCss"
+            bodyStyle={{ paddingBottom: 0, paddingTop: 0 }}
+        >
+            <Space size={40}>
+                <Tabs
+                    items={[
+                        { label: '图片', key: 'IMAGE', children: false },
+                        { label: '视频', key: 'VIDEO', children: false }
+                    ]}
+                    activeKey={queryParams?.activeKey}
+                    onChange={(e) => {
+                        setQueryParams({
+                            ...queryParams,
+                            activeKey: e
+                        })
+                    }}
+
+                />
+                <SelectAdAccount
+                    isReturnFirstValue
+                    value={queryParams?.adAccountId ? [queryParams.adAccountId] : undefined}
+                    type="radio"
+                    onChange={(value) => {
+                        setQueryParams({
+                            ...queryParams,
+                            adAccountId: value as number,
+                        })
+                    }}
+                    allowClear={false}
+                />
+            </Space>
+        </Card>
+        <Card
+            className="cardResetCss"
+            style={{ flex: 1, overflow: 'hidden' }}
+            bodyStyle={{
+                height: '100%',
+                overflow: 'hidden',
+                display: 'flex',
+                flexDirection: 'column',
+                width: '100%',
+                padding: 0
+            }}
+        >
+            <div className="manageComponent-header">
+                <Button icon={<PlusOutlined />} type="primary">新建组件</Button>
+                <Input.TextArea
+                    style={{ width: 160 }}
+                    value={idSting}
+                    rows={1}
+                    placeholder="素材ID(多个,,空格换行)"
+                    allowClear
+                    onChange={(e) => setIdSting(e.target.value)}
+                />
+                <Select
+                    showSearch
+                    placeholder="二级组件类型"
+                    filterOption={(input, option) =>
+                        ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                    }
+                    style={{ minWidth: 160 }}
+                    maxTagCount={1}
+                    mode="multiple"
+                    allowClear
+                    value={queryParams?.componentSubType}
+                    onChange={(e) => {
+                        setQueryParams({ ...queryParams, componentSubType: e, pageNum: 1 })
+                    }}
+                    options={queryParams?.activeKey === 'IMAGE' ? DEFAULT_COMPONENT_SUB_IMAGE_TYPE : DEFAULT_COMPONENT_SUB_VIDEO_TYPE}
+                />
+                <Select
+                    showSearch
+                    placeholder="已删除?"
+                    filterOption={(input, option) =>
+                        ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                    }
+                    style={{ width: 120 }}
+                    value={queryParams?.isDeleted}
+                    allowClear
+                    onChange={(e) => {
+                        setQueryParams({ ...queryParams, isDeleted: e, pageNum: 1 })
+                    }}
+                    options={[
+                        { label: '已删除', value: true },
+                        { label: '未删除', value: false }
+                    ]}
+                />
+                <Select
+                    showSearch
+                    placeholder="组件潜力"
+                    filterOption={(input, option) =>
+                        ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                    }
+                    style={{ minWidth: 120 }}
+                    maxTagCount={1}
+                    mode="multiple"
+                    value={queryParams?.potentialStatus}
+                    onChange={(e) => {
+                        setQueryParams({ ...queryParams, potentialStatus: e, pageNum: 1 })
+                    }}
+                    options={Object.keys(COMMON_POTENTIAL_STATUS_ENUM).map(key => ({ label: COMMON_POTENTIAL_STATUS_ENUM[key as keyof typeof COMMON_POTENTIAL_STATUS_ENUM], value: key }))}
+                />
+                <Select
+                    showSearch
+                    placeholder="来源"
+                    filterOption={(input, option) =>
+                        ((option?.label ?? '') as any).toLowerCase().includes(input.toLowerCase())
+                    }
+                    style={{ minWidth: 120 }}
+                    maxTagCount={1}
+                    mode="multiple"
+                    value={queryParams?.generationType}
+                    onChange={(e) => {
+                        setQueryParams({ ...queryParams, generationType: e, pageNum: 1 })
+                    }}
+                    options={Object.keys(COMPONENT_GENERATION_TYPE_ENUM).map(key => ({ label: COMPONENT_GENERATION_TYPE_ENUM[key as keyof typeof COMPONENT_GENERATION_TYPE_ENUM], value: key }))}
+                />
+                <DatePicker.RangePicker
+                    placeholder={['创建时间开始', '创建时间结束']}
+                    value={queryParams?.createTimeMin && queryParams?.createTimeMax ? [moment(queryParams?.createTimeMin), moment(queryParams?.createTimeMax)] as any : undefined}
+                    onChange={(_, option) => setQueryParams({ ...queryParams, createTimeMin: option[0], createTimeMax: option[1], pageNum: 1 })}
+                />
+            </div>
+            <div className="manageComponent-content" ref={ref}>
+                <Table
+                    columns={TableConfig(queryParams?.activeKey as any)}
+                    dataSource={getCreativeComponentList.data?.records || []}
+                    loading={getCreativeComponentList.loading}
+                    scroll={{ x: 1000, y: (size?.height || 0) - 36 }}
+                    pagination={false}
+                    size="small"
+                    bordered
+                    rowKey="componentId"
+                />
+            </div>
+            <div className="manageComponent-footer">
+                <Pagination
+                    size="small"
+                    total={getCreativeComponentList?.data?.total || 0}
+                    showSizeChanger
+                    showQuickJumper
+                    pageSize={getCreativeComponentList?.data?.size || 30}
+                    current={getCreativeComponentList?.data?.current || 1}
+                    onChange={(page: number, pageSize: number) => {
+                        ref.current?.querySelector('.ant-table-body')?.scrollTo({ top: 0 })
+                        setTimeout(() => setQueryParams({ ...queryParams, pageNum: page, pageSize }), 50)
+                    }}
+                />
+            </div>
+        </Card>
     </div>
 }
 

+ 140 - 0
src/pages/launchSystemV3/tencenTasset/manageComponent/tableConfig.tsx

@@ -0,0 +1,140 @@
+import { ColumnsType } from "antd/es/table";
+import React from "react";
+import { COMPONENT_GENERATION_TYPE_ENUM, COMPONENT_SUB_TYPE } from "./const";
+import { Popconfirm, Tag } from "antd";
+import moment from "moment";
+import Image1X1 from "../../components/AdsComponent/Image1X1";
+
+const TableConfig = (activeKey: 'IMAGE' | 'VIDEO'): ColumnsType<any> => {
+
+    const columns: ColumnsType<any> = [
+        {
+            title: activeKey === 'IMAGE' ? '图片' : '视频' + '组件',
+            dataIndex: 'componentSubType',
+            key: 'componentSubType',
+            width: 380,
+            render(value, records) {
+                if (['IMAGE_16X9', 'IMAGE_1X1', 'IMAGE_9X16'].includes(value)) {
+                    return <Image1X1 imageUrl={records?.componentValue?.image?.value?.imageUrl} />
+                } else if (['IMAGE_LIST_1X1_9'].includes(value)) {
+                    // records?.componentValue?.imageList?.value?.list imageId imageUrl
+                }
+                return <div className='image-9x16'>联系技术添加</div>
+            },
+        },
+        {
+            title: '二级组件类型',
+            dataIndex: 'componentSubType',
+            key: 'componentSubType',
+            width: 100,
+            ellipsis: true,
+            align: 'center',
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{COMPONENT_SUB_TYPE.find(item => item.value === value)?.label || '--'}</span>
+            },
+        },
+        {
+            title: '组件名称',
+            dataIndex: 'componentCustomName',
+            key: 'componentCustomName',
+            width: 180,
+            ellipsis: true,
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value}</span>
+            },
+        },
+        {
+            title: '组件ID',
+            dataIndex: 'componentId',
+            key: 'componentId',
+            width: 90,
+            ellipsis: true,
+            align: 'center',
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value}</span>
+            },
+        },
+        {
+            title: '来源',
+            dataIndex: 'generationType',
+            key: 'generationType',
+            width: 90,
+            ellipsis: true,
+            align: 'center',
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{COMPONENT_GENERATION_TYPE_ENUM['COMPONENT_GENERATION_TYPE_' + value as keyof typeof COMPONENT_GENERATION_TYPE_ENUM] || '--'}</span>
+            },
+        },
+        {
+            title: '广告账号',
+            dataIndex: 'accountId',
+            key: 'accountId',
+            width: 90,
+            ellipsis: true,
+            align: 'center',
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value || '--'}</span>
+            },
+        },
+        {
+            title: '业务单元ID',
+            dataIndex: 'organizationId',
+            key: 'organizationId',
+            width: 90,
+            ellipsis: true,
+            align: 'center',
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value || '--'}</span>
+            },
+        },
+        {
+            title: '创建时间',
+            dataIndex: 'createdTime',
+            key: 'createdTime',
+            width: 95,
+            ellipsis: true,
+            align: 'center',
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value ? moment.unix(value).format('YYYY-MM-DD') : '--'}</span>
+            },
+        },
+        {
+            title: '最后修改时间',
+            dataIndex: 'lastModifiedTime',
+            key: 'lastModifiedTime',
+            width: 95,
+            ellipsis: true,
+            align: 'center',
+            render(value) {
+                return <span style={{ fontSize: 12 }}>{value ? moment.unix(value).format('YYYY-MM-DD') : '--'}</span>
+            },
+        },
+        {
+            title: '是否删除',
+            dataIndex: 'isDeleted',
+            key: 'isDeleted',
+            width: 70,
+            ellipsis: true,
+            align: 'center',
+            render(value) {
+                return value ? <Tag color="error" style={{ margin: 0 }}>是</Tag> : <Tag color="success" style={{ margin: 0 }}>否</Tag>
+            },
+        },
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            render(_, record) {
+                return <Popconfirm
+                    title="确定删除?"
+                    onConfirm={() => {}}
+                >
+                    <a style={{ color: 'red', fontSize: 12 }}>删除</a>
+                </Popconfirm>
+            },
+        },
+    ]
+    return columns
+}
+
+export default TableConfig

+ 1 - 0
src/pages/launchSystemV3/tencentAdPutIn/create/Material/index.less

@@ -111,6 +111,7 @@
     border-radius: 6px;
     background-color: rgb(242, 246, 254);
     position: relative;
+    overflow: hidden;
 
     .content {
         width: 100px;

+ 223 - 0
src/services/adqV3/global.ts

@@ -1001,4 +1001,227 @@ export async function getApplicationDetailApi(id: number) {
     return request(api + `/adq/promote/application/getById/${id}`, {
         method: 'GET'
     })
+}
+
+/**
+ * 查询企业微信组件客服组
+ * @param data 
+ * @returns 
+ */
+export async function getCustomerServiceGroupListApi(data: { adAccountId: number, pageNum: number, pageSize: number, tencentCorpId?: string }) {
+    return request(api + `/adq/v3/launch/tools/selectCustomerServiceGroup`, {
+        method: 'POST',
+        data
+    })
+}
+
+export interface updateCustomerServiceGroupProps {
+    adAccountId: number,
+    tencentCorpId: string,
+    groupId: number,
+    groupName: string,
+    userIdList: string[]
+}
+/**
+ * 修改企业微信组件客服组
+ * @param data 
+ * @returns 
+ */
+export async function updateCustomerServiceGroupApi(data: updateCustomerServiceGroupProps) {
+    return request(api + `/adq/v3/launch/tools/updateCustomerServiceGroup`, {
+        method: 'POST',
+        data
+    })
+}
+
+export interface getWechatPagesCsgroupUserProps {
+    adAccountId: number,
+    tencentCorpId: string,
+    corpName: string,
+    departmentId?: number
+}
+/**
+ * 获取企业微信组件客服组成员
+ * @param data 
+ * @returns 
+ */
+export async function getWechatPagesCsgroupUserApi(data: getWechatPagesCsgroupUserProps) {
+    return request(api + `/adq/v3/launch/tools/wechatPagesCsgroupUser/get`, {
+        method: 'POST',
+        data
+    })
+}
+
+/**
+ * 获取企业部门列表
+ * @param data 
+ * @returns 
+ */
+export async function getCorpDepartmentListApi(params: { corpName: string }) {
+    return request(api + `/adq/v3/launch/tools/corpDepartmentList/get`, {
+        method: 'POST',
+        params
+    })
+}
+
+/**
+ * 查询企微关系列表
+ * @param params 
+ * @returns 
+ */
+export async function getCorpRelationListApi(data: { adAccountId: number }) {
+    return request(api + `/adq/corpRelation/listOfPage`, {
+        method: 'POST',
+        data
+    })
+}
+
+/**
+ * 查询所有企微关系
+ * @param data 
+ * @returns 
+ */
+export async function getCorpRelationAllApi(data: { adAccountId: number }) {
+    return request(api + `/adq/corpRelation/listAll`, {
+        method: 'POST',
+        data
+    })
+}
+
+/**
+ * 删除企微关系
+ * @param id 
+ * @returns 
+ */
+export async function delCorpRelationApi(id: number) {
+    return request(api + `/adq/corpRelation/delById/${id}`, {
+        method: 'DELETE'
+    })
+}
+
+/**
+ * 新增企微关系
+ * @param params 
+ * @returns 
+ */
+export async function addCorpRelationListApi(data: { adAccountId: number, corpId: string, corpName: string, tencentCorpId: string }) {
+    return request(api + `/adq/corpRelation/add`, {
+        method: 'POST',
+        data
+    })
+}
+
+/**
+ * 获取所有企微列表
+ * @returns 
+ */
+export async function getCorpListAllApi() {
+    return request(api + `/adq/corpRelation/corpListAll`, {
+        method: 'GET',
+    })
+}
+
+export interface AddPageCustomerGroupProps {
+    adAccountId: number,
+    customerGroupId: number,
+    customerGroupName: string,
+    pageId: number,
+    pageName: string,
+    tencentCorpId: string
+}
+/**
+ * 新增落地页客服关系
+ * @param data 
+ * @returns 
+ */
+export async function addPageCustomerGroupApi(data: AddPageCustomerGroupProps) {
+    return request(api + `/adq/pageCustomerGroup/relation/add`, {
+        method: 'POST',
+        data
+    })
+}
+
+/**
+ * 删除落地页客服关系
+ * @param id 
+ * @returns 
+ */
+export async function delPageCustomerGroupApi(id: number) {
+    return request(api + `/adq/pageCustomerGroup/relation/delById/${id}`, {
+        method: 'DELETE'
+    })
+}
+
+export interface GetPageCustomerGroupListProps {
+    adAccountId: number,
+    pageNum: number,
+    pageSize: number,
+    pageId?: number,
+    customerGroupId?: number,
+    customerGroupName?: string,
+    pageName?: string,
+    tencentCorpId?: string
+}
+/**
+ * 查询落地页客服关系
+ * @param data 
+ * @returns 
+ */
+export async function getPageCustomerGroupListApi(data: GetPageCustomerGroupListProps) {
+    return request(api + `/adq/pageCustomerGroup/relation/listOfPage`, {
+        method: 'POST',
+        data
+    })
+}
+
+
+export interface GetCreativeComponentProps {
+    pageNum: number,
+    pageSize: number,
+    adAccountId?: number,
+    componentId?: number[],
+    componentSubType?: string[]
+    componentType?: string[]
+    createTimeMin?: string
+    createTimeMax?: string
+    generationType?: string[]
+    imageId?: string[]
+    videoId?: string[]
+    isDeleted?: boolean
+    potentialStatus?: string
+    activeKey?: string
+}
+
+/**
+ * 分页查询创意组件
+ * @param data 
+ * @returns 
+ */
+export async function getCreativeComponentListApi(data: GetCreativeComponentProps) {
+    return request(api + `/adq/creative/component/listOfPage`, {
+        method: 'POST',
+        data
+    })
+}
+
+export interface AddBatchCreativeComponent {
+    adAccountId: number,
+    componentType: 'image' | 'imageList' | 'video' | 'videoList' | 'description',
+    creativeComponentDetailDTOS: {
+        materialId?: string,
+        materialIdList?: string[]
+        coverId?: string
+        text?: string
+    }[]
+}
+/**
+ * 批量创建创意组件
+ * @param data 
+ * @returns 
+ */
+export async function addBatchCreativeComponentApi(data: AddBatchCreativeComponent) {
+    return request(api + `/adq/creative/component/addBatch`, {
+        method: 'POST',
+        data
+    })
 }