wjx 1 周之前
父節點
當前提交
e828a2b432
共有 20 個文件被更改,包括 1557 次插入365 次删除
  1. 22 0
      src/pages/weComTask/API/businessPlan/create.ts
  2. 13 0
      src/pages/weComTask/API/global.ts
  3. 115 149
      src/pages/weComTask/components/selectCwTag/index.tsx
  4. 243 0
      src/pages/weComTask/components/selectCwTag/index1.tsx
  5. 5 1
      src/pages/weComTask/components/selectExternalAccount/index.less
  6. 30 0
      src/pages/weComTask/page/businessPlan/create/components/friends/friendsShowTable.tsx
  7. 235 193
      src/pages/weComTask/page/businessPlan/create/components/friends/index.tsx
  8. 1 1
      src/pages/weComTask/page/businessPlan/create/components/friends/previewFriendsStrategy.tsx
  9. 5 5
      src/pages/weComTask/page/businessPlan/create/components/friends/strategy.tsx
  10. 73 1
      src/pages/weComTask/page/businessPlan/create/const.tsx
  11. 188 6
      src/pages/weComTask/page/businessPlan/create/index.tsx
  12. 113 0
      src/pages/weComTask/page/businessPlan/create/tableConfig.tsx
  13. 3 1
      src/pages/weComTask/page/businessPlan/create/typings.d.ts
  14. 181 0
      src/pages/weComTask/page/businessPlan/taskList/components/momentTask/index.tsx
  15. 60 0
      src/pages/weComTask/page/businessPlan/taskList/components/momentTask/momentSendLog.tsx
  16. 132 0
      src/pages/weComTask/page/businessPlan/taskList/components/momentTask/momentTableConfig.tsx
  17. 68 0
      src/pages/weComTask/page/businessPlan/taskList/components/momentTask/momentXfCorpTabls.tsx
  18. 4 1
      src/pages/weComTask/page/businessPlan/taskList/log.tsx
  19. 42 1
      src/pages/weComTask/page/businessPlan/taskList/tableConfig.tsx
  20. 24 6
      src/utils/utils.ts

+ 22 - 0
src/pages/weComTask/API/businessPlan/create.ts

@@ -261,4 +261,26 @@ export async function inheritLogCount(data: any) {
         method: 'POST',
         data
     });
+}
+
+/**
+ * 朋友圈下发企微号
+ * @param data 
+ * @returns 
+ */
+export async function getMomentCorpUserListApi(data: any) {
+    return request({
+        url: `/corp/moment/corpUserList`,
+        method: 'POST',
+        data
+    })
+}
+
+
+export async function getSendLogApi(data: any) {
+    return request({
+        url: `/corp/moment/jobList`,
+        method: 'POST',
+        data
+    })
 }

+ 13 - 0
src/pages/weComTask/API/global.ts

@@ -60,6 +60,19 @@ export async function api_get_external_tag_group_list(params: { corpId: string }
     })
 }
 
+/**
+ * 获取企业下的标签组列表
+ * @param data 
+ * @returns 
+ */
+export async function getTagGroupListApi(data: any) {
+    return request({
+        url: '/corp/external/tag/group/list',
+        method: 'POST',
+        data
+    });
+}
+
 /**获取背景图ID列表*/
 export async function api_get_img_typeList() {
     return request({

+ 115 - 149
src/pages/weComTask/components/selectCwTag/index.tsx

@@ -1,56 +1,10 @@
+import { Avatar, Button, Checkbox, Input, message, Modal, Select, Space, Spin, Table, Tag, Tree, TreeDataNode, TreeProps, Typography } from "antd";
+import React, { useEffect, useMemo, useState } from "react";
+import style from '../selectExternalAccount/index.less'
+import { CheckOutlined, UserOutlined, CloseOutlined } from '@ant-design/icons'
 import { useAjax } from "@/Hook/useAjax";
-import { Alert, Input, Modal, Space, Typography, Spin, Button, App, TreeDataNode, Tree, TreeProps, Switch, Checkbox, Tag } from "antd";
-import React, { useEffect, useMemo, useState } from "react"
-import style from './index.less'
-import { addTaskApi } from "../../API/businessPlan/create";
-
-interface Props {
-    editSelectedRow: any[],
-    corpId: string,
-    type: 'ADD' | 'DEL',
-    visible?: boolean
-    onClose?: () => void
-    onChange?: () => void
-}
-
-/**
- * 添加本地标签给客户
- * @param param0 
- * @returns 
- */
-const SelectCwTag: React.FC<Props> = (props) => {
-
-    /**********************************/
-    const [sysTagsList, setSysTagsList] = useState<TreeDataNode[]>([])
-
-    // const getChatGroupList = useAjax((params) => getChatGroupListApi(params))
-    const getChatGroupList = useAjax((params) => addTaskApi(params))
-    /**********************************/
-
-    useEffect(() => {
-        getChatGroupList.run({ corpId: props.corpId }).then(res => {
-            setSysTagsList(res?.data?.map((item: { corpExternalTagList: { tagName: any; tagId: any }[]; tagGroupName: any; tagGroupId: any }) => {
-                let children = item?.corpExternalTagList?.map((c: { tagName: any; tagId: any }) => ({ title: c?.tagName, key: c?.tagId, value: c?.tagId }))
-                return { title: item?.tagGroupName, value: 'group' + '_' + item?.tagGroupId, key: 'group' + '_' + item?.tagGroupId, children }
-            }) || [])
-        })
-    }, [props.corpId])
-
-    return getChatGroupList.loading ? <div style={{
-        position: 'fixed',
-        top: 0,
-        left: 0,
-        width: '100%',
-        height: '100vh',
-        background: 'rgba(0,0,0,0.5)',
-        zIndex: 1000,
-        textAlign: 'center',
-        lineHeight: '100vh',
-        color: '#FFF'
-    }}>
-        <Spin size="large" /> <span style={{ marginLeft: 20, fontSize: 20 }}>加载中。。。</span>
-    </div> : <SelectCwTagEle {...props} tagsList={sysTagsList} />
-}
+import { getTagGroupListApi } from "../../API/global";
+const { Text, Title } = Typography;
 
 const getParentKey = (key: React.Key, tree: TreeDataNode[]): React.Key => {
     let parentKey: React.Key;
@@ -66,65 +20,68 @@ const getParentKey = (key: React.Key, tree: TreeDataNode[]): React.Key => {
     }
     return parentKey!;
 };
+/**
+ * 朋友圈企业标签选择
+ * @param param0 
+ * @returns 
+ */
+const SelectCwTag: React.FC<EXTERNAL_ACCOUNT_PROPS.SelectExternalAccountProps> = ({ corpUsers, visible, onClose, onChange }) => {
 
-interface EleProps extends Props {
-    tagsList: TreeDataNode[]
-}
-const SelectCwTagEle: React.FC<EleProps> = ({ tagsList, corpId, type, editSelectedRow, visible, onClose, onChange }) => {
-
-    /**********************************/
-    const [checkedList, setCheckedList] = useState<any[]>([]);
-
-    const { message } = App.useApp()
-    const [searchValue, setSearchValue] = useState<string>('');
-    const [groupAll, setGroupAll] = useState<boolean>(true)
+    /***************************************/
+    const [data, setData] = useState<TASK_CREATE.corpUsersProps[]>(corpUsers)
+    const [selectAdz, setSelectAdz] = useState<number>(1)   // 选择广告主
+    const [queryForm, setQueryForm] = useState<any>({ pageNum: 1, pageSize: 200, corpId: corpUsers?.[selectAdz - 1]?.corpId })
     const [dataList, setDataList] = useState<{ key: React.Key; title: string }[]>([]);
     const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
     const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
-    /**********************************/
+    const [tagsList, setTagsList] = useState<TreeDataNode[]>([]);
+    const [searchValue, setSearchValue] = useState<string>('');
+    const [groupAll, setGroupAll] = useState<boolean>(true)
+
+    const getTagGroupList = useAjax((params) => getTagGroupListApi(params))
+    /***************************************/
 
     useEffect(() => {
-        const dataList: { key: React.Key; title: string }[] = [];
-        const generateList = (data: TreeDataNode[]) => {
-            for (let i = 0; i < data.length; i++) {
-                const node = data[i];
-                const { key, title } = node;
-                dataList.push({ key, title: title as string });
-                if (node.children) {
-                    generateList(node.children);
+        getTagGroupList.run({ ...queryForm }).then(res => {
+            const tagsList = res?.data?.records?.map((item: { corpExternalTagList: { tagName: any; tagId: any }[]; tagGroupName: any; tagGroupId: any }) => {
+                let children = item?.corpExternalTagList?.map((c: { tagName: any; tagId: any }) => ({ title: c?.tagName, key: c?.tagId, value: c?.tagId }))
+                return { title: item?.tagGroupName, value: 'group' + '_' + item?.tagGroupId, key: 'group' + '_' + item?.tagGroupId, children }
+            }) || []
+            setTagsList(tagsList)
+
+            const dataList: { key: React.Key; title: string }[] = [];
+            const generateList = (data: TreeDataNode[]) => {
+                for (let i = 0; i < data.length; i++) {
+                    const node = data[i];
+                    const { key, title } = node;
+                    dataList.push({ key, title: title as string });
+                    if (node.children) {
+                        generateList(node.children);
+                    }
                 }
-            }
-        };
-        generateList(tagsList)
-        setDataList(dataList)
-    }, [tagsList])
+            };
+            generateList(tagsList)
+            setDataList(dataList)
+        }).catch(() => {
+            setDataList([])
+        })
+    }, [queryForm])
 
+    const handleSelectAdz = (value: number) => {
+        if (value === selectAdz) {
+            return
+        }
+        setSelectAdz(value)
+        setQueryForm(data => data?.corpId === corpUsers?.[value - 1]?.corpId ? data : { ...queryForm, corpId: corpUsers?.[selectAdz - 1]?.corpId })
+    }
 
     const handleOk = () => {
-        if (checkedList?.length > 0) {
-            let params = {
-                corpId,
-                tagIdList: checkedList.map(item => item.key),
-                chatIdList: editSelectedRow.map(item => item.chatId)
-            }
-            // if (type === 'ADD') {
-            //     addChatLocalTags.run(params).then(res => {
-            //         if (res.data) {
-            //             message.success('操作成功')
-            //             onChange?.()
-            //         }
-            //     })
-            // } else if (type === 'DEL') {
-            //     delChatLocalTags.run(params).then(res => {
-            //         if (res.data) {
-            //             message.success('操作成功')
-            //             onChange?.()
-            //         }
-            //     })
-            // }
-        } else {
-            message.error('请选择本地标签')
-        }
+        onChange?.(data)
+        // if (data?.every(item => item?.friendsTagContent?.length)) {
+            
+        // } else {
+        //     message.error('请选择企微标签')
+        // }
     }
 
     const onExpand = (newExpandedKeys: React.Key[]) => {
@@ -144,12 +101,12 @@ const SelectCwTagEle: React.FC<EleProps> = ({ tagsList, corpId, type, editSelect
         setAutoExpandParent(true)
     }
 
-    const onCheck: TreeProps['onCheck'] = (checkedKeys, info) => {
-        setCheckedList(info.checkedNodes?.filter(item => !item?.children))
+    const onCheck: TreeProps['onCheck'] = (_, info) => {
+        const newData = JSON.parse(JSON.stringify(data))
+        newData[selectAdz - 1].friendsTagContent = info.checkedNodes?.filter(item => !item?.children)
+        setData(newData)
     };
 
-
-
     const treeData = useMemo(() => {
         if (!searchValue) return tagsList;
         const loop = (data: TreeDataNode[]): TreeDataNode[] => data.reduce((pre, cur) => {
@@ -185,60 +142,69 @@ const SelectCwTagEle: React.FC<EleProps> = ({ tagsList, corpId, type, editSelect
         }, [] as TreeDataNode[])
 
         return loop(tagsList);
-    }, [searchValue, groupAll]);
+    }, [searchValue, groupAll, tagsList]);
 
     return <Modal
+        title={<strong>朋友圈企微标签选择</strong>}
         open={visible}
-        title={type === 'ADD' ? '添加群新标签' : '删除群标签'}
         onCancel={onClose}
         onOk={handleOk}
-        width={750}
-        destroyOnClose
-        styles={{ body: { paddingBottom: 0 } }}
+        width={1000}
+        className={`${style.SelectPackage}`}
+        styles={{
+            body: {
+                padding: '0 10px 0 10px'
+            }
+        }}
     >
-        <Space direction="vertical" style={{ width: '100%' }}>
-            <Alert message={<span>已选 <span style={{ fontSize: 14, fontWeight: 'bold' }}>{editSelectedRow.length}</span> 个群</span>} type="error" />
-            <Space align="center">
-                <Input.Search placeholder="搜索标签" onSearch={onSearch} allowClear /><Checkbox checked={groupAll} onChange={(e) => setGroupAll(e.target.checked)}>组内标签条件不满足过滤</Checkbox>
-            </Space>
-            <div style={{ display: 'flex', borderTop: '1px solid #0e0e0e08' }}>
-                <div style={{ height: 350, overflow: 'hidden', width: 450, backgroundColor: 'rgba(247, 247, 247, 0.6)' }}>
-                    <Tree
-                        checkable
-                        height={350}
-                        onCheck={onCheck}
-                        onExpand={onExpand}
-                        selectable={false}
-                        expandedKeys={expandedKeys}
-                        treeData={treeData}
-                        checkedKeys={checkedList.map(item => item.key)}
-                        autoExpandParent={autoExpandParent}
-                    />
+        <div className={style.content}>
+            <div className={style.left}>
+                <h4 className={style.title}>客服号</h4>
+                <div className={style.accountIdList}>
+                    {corpUsers?.map((item, index) => {
+                        const friendsTagContent = data[index]?.friendsTagContent || []
+                        return <div key={index} onClick={() => { handleSelectAdz(index + 1) }} className={`${style.accItem} ${selectAdz === index + 1 && style.select} `}>
+                            <div><Text ellipsis={{ tooltip: true }}>{item?.name}({item.corpName})</Text></div>
+                            {friendsTagContent?.length > 0 && <CheckOutlined style={{ color: '#1890ff' }} />}
+                        </div>
+                    })}
                 </div>
-                <div style={{ width: 'calc(100% - 450px)', height: 350, borderLeft: '1px solid #0e0e0e08', padding: '10px 10px 0', boxSizing: 'border-box' }}>
-                    <div style={{ display: 'flex', justifyContent: 'space-between' }}>
-                        <Typography.Text type='success' >已选标签:({checkedList?.length || 0})</Typography.Text>
-                        <Button size='small' type='primary' onClick={() => {
-                            setCheckedList([])
-                        }}>清空选择</Button>
-                    </div>
-                    <div style={{ height: 310, overflowY: 'auto', marginTop: 10 }}>
-                        {checkedList?.map(item => <Tag
-                            key={item.key}
-                            closable
-                            style={{ marginBottom: 4 }}
-                            onClose={(e) => {
-                                e.preventDefault();
-                                setCheckedList(checkedList.filter(d => d?.key !== item?.key))
-                            }}
-                        >
-                            <Typography.Text ellipsis={{ tooltip: true }} style={{ maxWidth: 90 }}>{item.title}</Typography.Text>
-                        </Tag>)}
+            </div>
+            <div className={style.right} style={{ paddingLeft: 0, paddingRight: 0, paddingBottom: 0 }}>
+                <Space style={{ marginBottom: 10, paddingLeft: 10, paddingRight: 10, paddingBottom: 10 }} align="center">
+                    <Input.Search placeholder="搜索标签" onSearch={onSearch} allowClear /><Checkbox checked={groupAll} onChange={(e) => setGroupAll(e.target.checked)}>组内标签条件不满足过滤</Checkbox>
+                </Space>
+                <Spin spinning={getTagGroupList.loading}>
+                    <div style={{ height: 400, overflow: 'hidden', width: '100%', paddingLeft: 10, boxSizing: 'border-box', backgroundColor: 'rgba(247, 247, 247, 0.6)' }}>
+                        <Tree
+                            checkable
+                            height={400}
+                            onCheck={onCheck}
+                            onExpand={onExpand}
+                            selectable={false}
+                            expandedKeys={expandedKeys}
+                            treeData={treeData}
+                            checkedKeys={data?.[selectAdz - 1]?.friendsTagContent?.map(item => item.key)}
+                            autoExpandParent={autoExpandParent}
+                        />
                     </div>
+                </Spin>
+            </div>
+            <div className={style.center}>
+                <Title level={5}>已选:{data[selectAdz - 1]?.friendsTagContent?.length || 0}</Title>
+                <div className={style.select_content}>
+                    {data[selectAdz - 1]?.friendsTagContent?.map(item => <div key={item.key}>
+                        <Text ellipsis={{ tooltip: { mouseEnterDelay: 0.5 } }} className={style.marketingAssetName}>{item.title}({item?.value})</Text>
+                        <CloseOutlined className={style.close} onClick={() => {
+                            let newData = JSON.parse(JSON.stringify(data))
+                            newData[selectAdz - 1].friendsTagContent = newData[selectAdz - 1]?.friendsTagContent?.filter((i: any) => i?.key !== item.key)
+                            setData(newData)
+                        }} />
+                    </div>)}
                 </div>
             </div>
-        </Space>
+        </div>
     </Modal>
 }
 
-export default React.memo(SelectCwTag)
+export default React.memo(SelectCwTag);

+ 243 - 0
src/pages/weComTask/components/selectCwTag/index1.tsx

@@ -0,0 +1,243 @@
+import { useAjax } from "@/Hook/useAjax";
+import { Alert, Input, Modal, Space, Typography, Spin, Button, App, TreeDataNode, Tree, TreeProps, Switch, Checkbox, Tag } from "antd";
+import React, { useEffect, useMemo, useState } from "react"
+import style from './index.less'
+import { getTagGroupListApi } from "../../API/global";
+
+interface Props {
+    editSelectedRow: any[],
+    corpId: string,
+    type: 'ADD' | 'DEL',
+    visible?: boolean
+    onClose?: () => void
+    onChange?: () => void
+}
+
+/**
+ * 添加本地标签给客户
+ * @param param0 
+ * @returns 
+ */
+const SelectCwTag: React.FC<Props> = (props) => {
+
+    /**********************************/
+    const [sysTagsList, setSysTagsList] = useState<TreeDataNode[]>([])
+
+    const getTagGroupList = useAjax((params) => getTagGroupListApi(params))
+    /**********************************/
+
+    useEffect(() => {
+        getTagGroupList.run({ corpId: props.corpId, pageNum: 1, pageSize: 1000 }).then(res => {
+            setSysTagsList(res?.data?.records?.map((item: { corpExternalTagList: { tagName: any; tagId: any }[]; tagGroupName: any; tagGroupId: any }) => {
+                const children = item?.corpExternalTagList?.map((c: { tagName: any; tagId: any }) => ({ title: c?.tagName, key: c?.tagId, value: c?.tagId }))
+                return { title: item?.tagGroupName, value: 'group' + '_' + item?.tagGroupId, key: 'group' + '_' + item?.tagGroupId, children }
+            }) || [])
+        })
+    }, [props.corpId])
+
+    return getTagGroupList.loading ? <div style={{
+        position: 'fixed',
+        top: 0,
+        left: 0,
+        width: '100%',
+        height: '100vh',
+        background: 'rgba(0,0,0,0.5)',
+        zIndex: 1000,
+        textAlign: 'center',
+        lineHeight: '100vh',
+        color: '#FFF'
+    }}>
+        <Spin size="large" /> <span style={{ marginLeft: 20, fontSize: 20 }}>加载中。。。</span>
+    </div> : <SelectCwTagEle {...props} tagsList={sysTagsList} />
+}
+
+const getParentKey = (key: React.Key, tree: TreeDataNode[]): React.Key => {
+    let parentKey: React.Key;
+    for (let i = 0; i < tree.length; i++) {
+        const node = tree[i];
+        if (node.children) {
+            if (node.children.some((item) => item.key === key)) {
+                parentKey = node.key;
+            } else if (getParentKey(key, node.children)) {
+                parentKey = getParentKey(key, node.children);
+            }
+        }
+    }
+    return parentKey!;
+};
+
+interface EleProps extends Props {
+    tagsList: TreeDataNode[]
+}
+const SelectCwTagEle: React.FC<EleProps> = ({ tagsList, corpId, type, editSelectedRow, visible, onClose, onChange }) => {
+
+    /**********************************/
+    const [checkedList, setCheckedList] = useState<any[]>([]);
+
+    const { message } = App.useApp()
+    const [searchValue, setSearchValue] = useState<string>('');
+    const [groupAll, setGroupAll] = useState<boolean>(true)
+    const [dataList, setDataList] = useState<{ key: React.Key; title: string }[]>([]);
+    const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
+    const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
+    /**********************************/
+
+    useEffect(() => {
+        const dataList: { key: React.Key; title: string }[] = [];
+        const generateList = (data: TreeDataNode[]) => {
+            for (let i = 0; i < data.length; i++) {
+                const node = data[i];
+                const { key, title } = node;
+                dataList.push({ key, title: title as string });
+                if (node.children) {
+                    generateList(node.children);
+                }
+            }
+        };
+        generateList(tagsList)
+        setDataList(dataList)
+    }, [tagsList])
+
+
+    const handleOk = () => {
+        if (checkedList?.length > 0) {
+            let params = {
+                corpId,
+                tagIdList: checkedList.map(item => item.key),
+                chatIdList: editSelectedRow.map(item => item.chatId)
+            }
+            // if (type === 'ADD') {
+            //     addChatLocalTags.run(params).then(res => {
+            //         if (res.data) {
+            //             message.success('操作成功')
+            //             onChange?.()
+            //         }
+            //     })
+            // } else if (type === 'DEL') {
+            //     delChatLocalTags.run(params).then(res => {
+            //         if (res.data) {
+            //             message.success('操作成功')
+            //             onChange?.()
+            //         }
+            //     })
+            // }
+        } else {
+            message.error('请选择本地标签')
+        }
+    }
+
+    const onExpand = (newExpandedKeys: React.Key[]) => {
+        setExpandedKeys(newExpandedKeys);
+        setAutoExpandParent(false);
+    };
+
+    const onSearch = (value?: string) => {
+        const newExpandedKeys = dataList.map((item) => {
+            if (value && item.title.indexOf(value) > -1) {
+                return getParentKey(item.key, tagsList);
+            }
+            return null;
+        }).filter((item, i, self): item is React.Key => !!(item && self.indexOf(item) === i));
+        setExpandedKeys(newExpandedKeys);
+        setSearchValue(value || '')
+        setAutoExpandParent(true)
+    }
+
+    const onCheck: TreeProps['onCheck'] = (checkedKeys, info) => {
+        setCheckedList(info.checkedNodes?.filter(item => !item?.children))
+    };
+
+
+
+    const treeData = useMemo(() => {
+        if (!searchValue) return tagsList;
+        const loop = (data: TreeDataNode[]): TreeDataNode[] => data.reduce((pre, cur) => {
+            const strTitle = cur.title as string;
+            const index = strTitle.indexOf(searchValue);
+            if (index > -1) {
+                const beforeStr = strTitle.substring(0, index);
+                const afterStr = strTitle.slice(index + searchValue.length);
+                const title = <span>
+                    {beforeStr}
+                    <span className={style['site-tree-search-value']}>{searchValue}</span>
+                    {afterStr}
+                </span>;
+                if (!groupAll) {
+                    pre.push({ ...cur, title })
+                } else {
+                    if (cur.children) {
+                        let children = loop(cur.children)
+                        pre.push({ ...cur, title, children })
+                    } else {
+                        pre.push({ ...cur, title })
+                    }
+                }
+            } else {
+                if (cur.children) {
+                    let children = loop(cur.children)
+                    if (children?.length > 0) {
+                        pre.push({ ...cur, children })
+                    }
+                }
+            }
+            return pre
+        }, [] as TreeDataNode[])
+
+        return loop(tagsList);
+    }, [searchValue, groupAll]);
+
+    return <Modal
+        open={visible}
+        title={type === 'ADD' ? '添加群新标签' : '删除群标签'}
+        onCancel={onClose}
+        onOk={handleOk}
+        width={750}
+        destroyOnClose
+        styles={{ body: { paddingBottom: 0 } }}
+    >
+        <Space direction="vertical" style={{ width: '100%' }}>
+            <Alert message={<span>已选 <span style={{ fontSize: 14, fontWeight: 'bold' }}>{editSelectedRow.length}</span> 个群</span>} type="error" />
+            <Space align="center">
+                <Input.Search placeholder="搜索标签" onSearch={onSearch} allowClear /><Checkbox checked={groupAll} onChange={(e) => setGroupAll(e.target.checked)}>组内标签条件不满足过滤</Checkbox>
+            </Space>
+            <div style={{ display: 'flex', borderTop: '1px solid #0e0e0e08' }}>
+                <div style={{ height: 350, overflow: 'hidden', width: 450, backgroundColor: 'rgba(247, 247, 247, 0.6)' }}>
+                    <Tree
+                        checkable
+                        height={350}
+                        onCheck={onCheck}
+                        onExpand={onExpand}
+                        selectable={false}
+                        expandedKeys={expandedKeys}
+                        treeData={treeData}
+                        checkedKeys={checkedList.map(item => item.key)}
+                        autoExpandParent={autoExpandParent}
+                    />
+                </div>
+                <div style={{ width: 'calc(100% - 450px)', height: 350, borderLeft: '1px solid #0e0e0e08', padding: '10px 10px 0', boxSizing: 'border-box' }}>
+                    <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                        <Typography.Text type='success' >已选标签:({checkedList?.length || 0})</Typography.Text>
+                        <Button size='small' type='primary' onClick={() => {
+                            setCheckedList([])
+                        }}>清空选择</Button>
+                    </div>
+                    <div style={{ height: 310, overflowY: 'auto', marginTop: 10 }}>
+                        {checkedList?.map(item => <Tag
+                            key={item.key}
+                            closable
+                            style={{ marginBottom: 4 }}
+                            onClose={(e) => {
+                                e.preventDefault();
+                                setCheckedList(checkedList.filter(d => d?.key !== item?.key))
+                            }}
+                        >
+                            <Typography.Text ellipsis={{ tooltip: true }} style={{ maxWidth: 90 }}>{item.title}</Typography.Text>
+                        </Tag>)}
+                    </div>
+                </div>
+            </div>
+        </Space>
+    </Modal>
+}
+
+export default React.memo(SelectCwTag)

+ 5 - 1
src/pages/weComTask/components/selectExternalAccount/index.less

@@ -109,7 +109,7 @@
             }
 
             .select_content {
-                height: calc(500 - 28px);
+                height: 422px;
                 overflow: hidden;
                 overflow-y: auto;
 
@@ -150,4 +150,8 @@
             }
         }
     }
+}
+
+.site-tree-search-value {
+    color: #f50;
 }

+ 30 - 0
src/pages/weComTask/page/businessPlan/create/components/friends/friendsShowTable.tsx

@@ -0,0 +1,30 @@
+import LookPyq from '@/pages/weComTask/components/previewMsg/lookPyq';
+import { Popover } from 'antd';
+import React from 'react';
+
+interface Props {
+    data: any
+    name?: React.ReactNode
+}
+const FriendsShowTable: React.FC<Props> = ({ name, data }) => {
+
+    return <Popover
+        placement="left"
+        content={<LookPyq
+            content={{ text: data?.text?.content, data: data?.attachmentList }}
+        />}
+        styles={{ body: { width: 360, overflow: 'hidden', overflowY: 'auto', maxHeight: 480 } }}
+    >
+        <div
+            style={{
+                backgroundColor: 'rgba(0,0,0,0.1)',
+                padding: '4px 8px',
+                borderRadius: 8,
+                overflow: 'hidden',
+                cursor: 'help'
+            }}
+        >{name || '朋友圈内容'}</div>
+    </Popover>
+};
+
+export default React.memo(FriendsShowTable);

+ 235 - 193
src/pages/weComTask/page/businessPlan/create/components/friends/index.tsx

@@ -9,7 +9,7 @@ import { getGroupData, headerJsMustStyle, headerJsStyle, TIME_TYPE, welcomeConte
 import ExcelJS from 'exceljs';
 import FilterUserText from '@/pages/weComTask/components/filterUser/filterUserText';
 import { RcFile } from 'antd/es/upload';
-import { groupBy, readFileAsBuffer } from '@/utils/utils';
+import { cutByBytes, groupBy, readFileAsBuffer } from '@/utils/utils';
 import { saveAs } from 'file-saver';
 import SettingsFriends from './settingsFriends';
 import PreviewFriendsStrategy from './previewFriendsStrategy';
@@ -27,6 +27,7 @@ const Friends: React.FC = () => {
     const [downloadLoading, setDownloadLoading] = useState<boolean>(false)
     /***************************************************/
 
+    // 下载配置表格
     const exportExcel = async () => {
         setDownloadLoading(true)
         const workbook = new ExcelJS.Workbook();  // 创建空工作簿
@@ -51,10 +52,10 @@ const Friends: React.FC = () => {
 
         worksheet.columns = [
             { key: 'A1', width: 20, header: '账号' },
-            { key: 'A2', width: 20, header: '群发标题' },
+            { key: 'A2', width: 20, header: '朋友圈标题' },
             { key: 'A3', width: 30, header: '策略信息' },
             { key: 'A4', width: 40, header: '发送对象' },
-            { key: 'A5', width: 70, header: '群发内容' },
+            { key: 'A5', width: 70, header: '朋友圈内容' },
             { key: 'A6', width: 30, header: '图文链接' },
             { key: 'A7', width: 30, header: '小程序APPID' },
             { key: 'A8', width: 30, header: '小程序路径' },
@@ -67,24 +68,156 @@ const Friends: React.FC = () => {
             // 设置表头字体样式
             worksheet.getCell(headerRowIndex, col).style = index >= 5 ? headerJsMustStyle as any : headerJsStyle as any;
         })
-
-
-        const data = getGroupData(settings)
-        console.log('数据', data)
-
-        const corpUserListLength = settings?.corpUsers?.length || 0;
-        const dataLength = data.length || 0;
         // 合并单元集合
         const mergeCells = [];
         // 解放填写限制区域
         const unLockArea = [];
-        let dataRow = 1
+
+        let dataRow = 0
         // 数据内容
         settings?.corpUsers?.forEach((item, i) => {
-            // 1、客服号 群发标题
+            let strategyIndex = 0
+
+            settings?.friendsStrategy?.strategySettings?.forEach((strategyItem, index) => {
+                const friendsContentDTO = settings?.friendsContent?.friendsContentDTO?.[index]
+                const startRow = 2 + dataRow
+                const endRow = startRow + (friendsContentDTO?.contentDTO?.length || 0) - 1
+                mergeCells.push({
+                    startRow,
+                    startColumn: 3,
+                    endRow,
+                    endColumn: 3,
+                })
+                mergeCells.push({
+                    startRow,
+                    startColumn: 4,
+                    endRow,
+                    endColumn: 4,
+                })
+                friendsContentDTO.contentDTO.forEach((contentItem, contentIndex) => {
+                    strategyIndex++;
+                    let linkPlaceholder = '<空>'
+                    let miniprogramPlaceholder = '<空>'
+
+                    const mediaItem = JSON.parse(JSON.stringify(contentItem?.attachmentList || []))
+                    if (contentItem?.text?.content) {
+                        mediaItem.push({
+                            mediaType: 'text',
+                            textContent: contentItem?.text?.content
+                        })
+                    }
+
+                    const contentRichText = mediaItem.map(item => {
+                        switch (item.mediaType) {
+                            case 'link':
+                                linkPlaceholder = '请输入'
+                                return { text: `链接:${item.linkTitle}_${item.linkDesc}\n`, font: { color: { argb: "FF0000" } } }
+                            case 'miniprogram':
+                                miniprogramPlaceholder = '请输入'
+                                return { text: `小程序:${item.miniprogramTitle}\n`, font: { color: { argb: "FF0000" } } }
+                            case 'file':
+                                return { text: `文件:${item.fileUrl}\n` }
+                            case 'video':
+                                return { text: `视频:${item.videoUrl}\n` }
+                            case 'image':
+                                return { text: `图片:${item.imageUrl}\n` }
+                            case 'text':
+                                return { text: `文本:${item.textContent}\n` }
+                            default:
+                                return { text: `该类型没有请联系管理员` }
+                        }
+                    })
+
+                    const richText = {
+                        richText: [
+                            { text: `(内容${contentIndex + 1})\n` },
+                            ...contentRichText
+                        ]
+                    }
+                    dataRow++;
+
+                    if (linkPlaceholder !== '<空>') {
+                        unLockArea.push(`${dataRow}_6`)
+                    }
+                    if (miniprogramPlaceholder !== '<空>') {
+                        unLockArea.push(`${dataRow}_7`, `${dataRow}_8`)
+                    }
+
+                    worksheet.addRow([
+                        // 账号
+                        `${item.name}(ID:${item.corpUserId})`,
+                        // 朋友圈标题
+                        `${settings?.friendsStrategy?.momentSendName}`,
+                        // 策略信息
+                        {
+                            richText: [
+                                { text: `(策略${index + 1})\n`, font: { bold: true } },
+                                { text: `名称:${strategyItem?.strategyName || '<空>'}\n` },
+                                {
+                                    text: (`执行类型:${TIME_TYPE[strategyItem?.timeRepeatType]}\n`) +
+                                        (strategyItem?.sendDay ? `执行时间:${strategyItem?.sendDay}\n` : '') +
+                                        (strategyItem?.startTime ? `执行日期:${strategyItem?.startTime}~${strategyItem?.endTime ? strategyItem?.endTime : '长期执行'}\n` : '') +
+                                        (strategyItem?.sendTime ? '执行时间:' + strategyItem?.sendTime + '\n' : '') +
+                                        (strategyItem?.repeatArray ? `执行天数:${strategyItem?.repeatArray.join('、')}\n` : '')
+                                }
+                            ]
+                        },
+                        // 发送对象
+                        {
+                            richText: [
+                                { text: `发送对象:${item?.friendsTagContent?.length > 0 ? cutByBytes(item?.friendsTagContent?.map(i => i.title).join('、'), 160) : '全部'}\n`, font: { bold: true } },
+                                { text: `发送模式:${welcomeContentData?.find(i => i.value === friendsContentDTO?.sendMode)?.label}\n` },
+                            ]
+                        },
+                        // 群发内容
+                        richText,
+                        // 图文链接
+                        linkPlaceholder,
+                        // 小程序APPID
+                        miniprogramPlaceholder,
+                        // 小程序路径
+                        miniprogramPlaceholder
+                    ])
+
+                    worksheet.getRow(dataRow + headerRowIndex).height = 100; // 设置行高
+                    worksheet.getRow(dataRow + headerRowIndex).alignment = {
+                        vertical: 'middle',
+                        wrapText: true
+                    }
+                    // 分组设置背景色
+                    if (i % 2 === 0) {
+                        worksheet.getRow(dataRow + headerRowIndex).eachCell(function (cell, rowNumber) {
+                            if (rowNumber <= 8) {
+                                cell.border = {
+                                    top: { style: 'thin', color: { argb: 'D4D4D4' } },
+                                    bottom: { style: 'thin', color: { argb: 'D4D4D4' } },
+                                    left: { style: 'thin', color: { argb: 'D4D4D4' } },
+                                    right: { style: 'thin', color: { argb: 'D4D4D4' } }
+                                }
+                                cell.fill = {
+                                    type: 'pattern',
+                                    pattern: 'solid',
+                                    fgColor: { argb: 'f0f0f0' }
+                                }
+                            }
+                        })
+                    } else {
+                        worksheet.getRow(dataRow + headerRowIndex).eachCell(function (cell, rowNumber) {
+                            if (rowNumber <= 8) {
+                                cell.border = {
+                                    top: { style: 'thin', color: { argb: 'D4D4D4' } },
+                                    bottom: { style: 'thin', color: { argb: 'D4D4D4' } },
+                                    left: { style: 'thin', color: { argb: 'D4D4D4' } },
+                                    right: { style: 'thin', color: { argb: 'D4D4D4' } }
+                                }
+                            }
+                        })
+                    }
+                })
+            })
 
-            const startRow = 2 + (i * dataLength)
-            const endRow = startRow + dataLength - 1
+            const startRow = 2 + (i * strategyIndex)
+            const endRow = startRow + strategyIndex - 1
             mergeCells.push({
                 startRow,
                 startColumn: 1,
@@ -97,178 +230,27 @@ const Friends: React.FC = () => {
                 endRow,
                 endColumn: 2,
             })
+        })
+        console.log('mergeCells--->', mergeCells)
+        // 合并单元格
+        mergeCells.forEach(({ startRow, startColumn, endRow, endColumn }) => {
+            worksheet.mergeCells(startRow, startColumn, endRow, endColumn); // 合并单元格
+        })
 
-            let strategyIndex = 0
-
-            data.forEach((dataItem, index, row) => {
-                if (strategyIndex !== dataItem.strategyIndex) {
-                    const startRow = i * row.length + (index + 2)
-                    mergeCells.push({
-                        startRow,
-                        startColumn: 3,
-                        endRow: startRow + dataItem.strategyDataCount - 1,
-                        endColumn: 3,
-                    })
-                }
-                if (dataItem.sendDataRowSpan) {
-                    const startRow = i * row.length + (index + 2)
-                    mergeCells.push({
-                        startRow,
-                        startColumn: 4,
-                        endRow: startRow + dataItem.sendDataRowSpan - 1,
-                        endColumn: 4,
-                    })
-                }
-                const mediaItem = JSON.parse(JSON.stringify(dataItem?.content?.attachmentList || []))
-                if (dataItem?.content?.text?.content) {
-                    mediaItem.push({
-                        msgType: 'TASK_CONTENT_TEXT',
-                        textContent: dataItem?.content?.text?.content
-                    })
-                }
-                let linkPlaceholder = '<空>'
-                let miniprogramPlaceholder = '<空>'
-                const contentRichText = mediaItem.map(item => {
-                    // 'TASK_CONTENT_TEXT' | 'TASK_CONTENT_IMAGE' | 'TASK_CONTENT_LINK' | 'TASK_STATUS_MINIPROGRAM' | 'TASK_STATUS_VIDEO' | 'TASK_STATUS_FILE'
-                    switch (item.msgType) {
-                        case 'TASK_CONTENT_LINK':
-                            linkPlaceholder = '请输入'
-                            return { text: `链接:${item.link.title}_${item.link.desc}\n`, font: { color: { argb: "FF0000" } } }
-                        case 'TASK_STATUS_MINIPROGRAM':
-                            miniprogramPlaceholder = '请输入'
-                            return { text: `小程序:${item.miniprogram.title}\n`, font: { color: { argb: "FF0000" } } }
-                        case 'TASK_STATUS_FILE':
-                            return { text: `文件:${item.file.fileUrl}\n` }
-                        case 'TASK_STATUS_VIDEO':
-                            return { text: `视频:${item.video.videoUrl}\n` }
-                        case 'TASK_CONTENT_IMAGE':
-                            return { text: `图片:${item.image.picUrl}\n` }
-                        case 'TASK_CONTENT_TEXT':
-                            return { text: `文本:${item.textContent}\n` }
-                        default:
-                            return { text: `该类型没有请联系管理员` }
+        // 设置可填写区域
+        for (let rowNum = 1; rowNum <= dataRow; rowNum++) {
+            for (let colNum = 1; colNum <= 3; colNum++) {
+                if (unLockArea.includes(`${rowNum}_${colNum + 5}`)) {
+                    const cell = worksheet.getCell(rowNum + 1, colNum + 5);
+                    cell.protection = { locked: false };
+                    cell.fill = {
+                        type: 'pattern',
+                        pattern: 'solid',
+                        fgColor: { argb: 'ffadd2' }
                     }
-                })
-
-                const richText = {
-                    richText: [
-                        { text: `(内容${dataItem?.contentIndex})\n`, font: { bold: true } },
-                        ...contentRichText
-                    ]
-                }
-
-                if (linkPlaceholder !== '<空>') {
-                    unLockArea.push(`${dataRow}_6`)
                 }
-                if (miniprogramPlaceholder !== '<空>') {
-                    unLockArea.push(`${dataRow}_7`, `${dataRow}_8`)
-                }
-                dataRow++;
-                strategyIndex = dataItem.strategyIndex
-                worksheet.addRow([
-                    // 账号
-                    `${item.name}(ID:${item.corpUserId})`,
-                    // 群发标题
-                    `${dataItem?.groupSendName}`,
-                    // 策略信息
-                    {
-                        richText: [
-                            { text: `(策略${dataItem?.strategyIndex})\n`, font: { bold: true } },
-                            { text: `名称:${dataItem?.strategyData?.strategyName || '<空>'}\n` },
-                            {
-                                text: (`执行类型:${TIME_TYPE[dataItem?.strategyData?.timeRepeatType]}\n`) +
-                                    (dataItem?.strategyData?.sendDay ? `执行时间:${dataItem?.strategyData?.sendDay}\n` : '') +
-                                    (dataItem?.strategyData?.startTime ? `执行日期:${dataItem?.strategyData?.startTime}~${dataItem?.strategyData?.endTime ? dataItem?.strategyData?.endTime : '长期执行'}\n` : '') +
-                                    (dataItem?.strategyData?.sendTime ? '执行时间:' + dataItem?.strategyData?.sendTime + '\n' : '') +
-                                    (dataItem?.strategyData?.repeatArray ? `执行天数:${dataItem?.strategyData?.repeatArray.join('、')}\n` : '')
-                            }
-                        ]
-                    },
-                    // 发送对象
-                    {
-                        richText: [
-                            { text: `(发送对象${dataItem?.sendDataIndex})\n`, font: { bold: true } },
-                            { text: `发送模式:${welcomeContentData?.find(i => i.value === dataItem?.sendMode)?.label}\n` },
-                            {
-                                text: `类型:${dataItem?.sendData?.externalUserType === 'all' ? '全部' : '指定'}\n` +
-                                    (dataItem?.sendData?.externalUserType === 'specify' && dataItem?.sendData?.externalUserFilter ? FilterUserText({
-                                        data: dataItem?.sendData?.externalUserFilter?.configContent,
-                                        configName: dataItem?.sendData?.externalUserFilter?.configName,
-                                        bookCityList: bookPlatForm?.map(item => ({ label: item.platformName, value: item.platformKey })),
-                                        bookPlatForm,
-                                        bookList
-                                    }) : '')
-                            }
-                        ]
-                    },
-                    // 群发内容
-                    richText,
-                    // 图文链接
-                    linkPlaceholder,
-                    // 小程序APPID
-                    miniprogramPlaceholder,
-                    // 小程序路径
-                    miniprogramPlaceholder
-                ])
-            })
-        })
-
-        // Array(corpUserListLength * dataLength).fill(0).forEach((_, index) => {
-        //     worksheet.getRow(index + headerRowIndex + 1).height = 100; // 设置行高
-        //     worksheet.getRow(index + headerRowIndex + 1).alignment = {
-        //         vertical: 'middle',
-        //         wrapText: true
-        //     }
-        //     // 分组设置背景色
-        //     if (index % (dataLength * 2) < dataLength) {
-        //         worksheet.getRow(index + headerRowIndex + 1).eachCell(function (cell, rowNumber) {
-        //             if (rowNumber <= 8) {
-        //                 cell.border = {
-        //                     top: { style: 'thin', color: { argb: 'D4D4D4' } },
-        //                     bottom: { style: 'thin', color: { argb: 'D4D4D4' } },
-        //                     left: { style: 'thin', color: { argb: 'D4D4D4' } },
-        //                     right: { style: 'thin', color: { argb: 'D4D4D4' } }
-        //                 }
-        //                 cell.fill = {
-        //                     type: 'pattern',
-        //                     pattern: 'solid',
-        //                     fgColor: { argb: 'f0f0f0' }
-        //                 }
-        //             }
-        //         })
-        //     } else {
-        //         worksheet.getRow(index + headerRowIndex + 1).eachCell(function (cell, rowNumber) {
-        //             if (rowNumber <= 8) {
-        //                 cell.border = {
-        //                     top: { style: 'thin', color: { argb: 'D4D4D4' } },
-        //                     bottom: { style: 'thin', color: { argb: 'D4D4D4' } },
-        //                     left: { style: 'thin', color: { argb: 'D4D4D4' } },
-        //                     right: { style: 'thin', color: { argb: 'D4D4D4' } }
-        //                 }
-        //             }
-        //         })
-        //     }
-        // })
-
-        // // 合并单元格
-        // mergeCells.forEach(({ startRow, startColumn, endRow, endColumn }) => {
-        //     worksheet.mergeCells(startRow, startColumn, endRow, endColumn); // 合并单元格
-        // })
-
-        // // 设置可填写区域
-        // for (let rowNum = 1; rowNum <= corpUserListLength * dataLength; rowNum++) {
-        //     for (let colNum = 1; colNum <= 3; colNum++) {
-        //         if (unLockArea.includes(`${rowNum}_${colNum + 5}`)) {
-        //             const cell = worksheet.getCell(rowNum + 1, colNum + 5);
-        //             cell.protection = { locked: false };
-        //             cell.fill = {
-        //                 type: 'pattern',
-        //                 pattern: 'solid',
-        //                 fgColor: { argb: 'ffadd2' }
-        //             }
-        //         }
-        //     }
-        // }
+            }
+        }
         // 生成文件内容
         workbook.xlsx.writeBuffer().then(buffer => {
             // 通过 Blob 下载
@@ -278,6 +260,71 @@ const Friends: React.FC = () => {
         });
     }
 
+    // 读取Excel
+    const readExcelContent = async (file: RcFile) => {
+        const buffer: any = await readFileAsBuffer(file);
+        const workbook = new ExcelJS.Workbook();
+        await workbook.xlsx.load(buffer);  // 加载 Excel 内容
+
+        const worksheet = workbook.worksheets[0];  // 获取第一个工作表
+        const data = [];
+
+        worksheet.eachRow((row) => {
+            const rowData = {};
+            row.eachCell((cell, colNumber) => {
+                if (cell.isHyperlink) {
+                    rowData[`col${colNumber}`] = cell.text || '';
+                } else {
+                    rowData[`col${colNumber}`] = cell.value || '';
+                }
+            });
+            data.push(rowData);
+        });
+
+        data.shift();  // 删除表头
+        const NULLDATA = ['请输入', '']
+        if (data?.some(item => (NULLDATA.includes(item.col6) || !item?.col6 || NULLDATA.includes(item.col7) || !item?.col7 || NULLDATA.includes(item.col8) || !item?.col8))) {
+            message.error('请正确填写Excel内容!')
+            return
+        }
+
+        if (settings?.corpUsers?.reduce((pre) => pre + settings?.friendsStrategy?.strategySettings.reduce((sPre, _, index) => sPre + settings?.friendsContent?.friendsContentDTO?.[index]?.contentDTO?.length, 0), 0) !== data?.length) {
+            message.error('数量对不上,请正确填写Excel内容!')
+            return
+        }
+
+        const groupData = groupBy(data, (item) => [item['col1']]).reduce((acc, cur) => {
+            const id = cur[0]['col1'].split('ID:')[1].replace(/\)$/, '');
+            acc[id] = cur
+            return acc;
+        }, {});
+        // console.log('读取的Excel数据', data, groupData)
+
+        const corpUsers = settings?.corpUsers?.map(item => {
+            const friendsMsgContent = []
+            groupData[item.corpUserId]?.forEach((i: any) => {
+                const strategyIndex = i.col3.richText[0].text.split('(策略')[1]?.split(')')[0]
+                const contentIndex = i.col5.richText[0].text.split('(内容')[1]?.split(')')[0]
+                const layer1 = strategyIndex - 1;
+                if (!friendsMsgContent[layer1]) friendsMsgContent[layer1] = [];
+                friendsMsgContent[strategyIndex - 1][contentIndex - 1] = {
+                    linkUrl: i.col6 === '<空>' ? undefined : i.col6?.toString()?.trim(),
+                    miniprogramAppid: i.col7 === '<空>' ? undefined : i.col7?.toString()?.trim(),
+                    miniprogramPage: i.col8 === '<空>' ? undefined : i.col8?.toString()?.trim()
+                }
+            })
+            return {
+                ...item,
+                friendsMsgContent
+            }
+        })
+        // console.log('corpUsers', corpUsers)
+        setSettings({
+            ...settings,
+            corpUsers
+        })
+    }
+
     return <>
         <div className={`${style.settingsBody_content_row}`}>
             <div className={`${style.settingsBody_content_col}`}>
@@ -307,7 +354,7 @@ const Friends: React.FC = () => {
                                 loading={downloadLoading}
                                 onClick={() => { exportExcel() }}
                             >下载</Button>
-                            {/* <Upload
+                            <Upload
                                 accept={'.xlsx'}
                                 action="#"
                                 showUploadList={false}
@@ -320,8 +367,8 @@ const Friends: React.FC = () => {
                                 <Button
                                     type="link"
                                     style={{ padding: 0, fontSize: 12 }}
-                                >{settings?.corpUsers?.every(item => item?.groupMsgContent?.length) ? 'Excel内容已上传,重新上传' : '上传Excel'}</Button>
-                            </Upload> */}
+                                >{settings?.corpUsers?.every(item => item?.friendsMsgContent?.length) ? 'Excel内容已上传,重新上传' : '上传Excel'}</Button>
+                            </Upload>
                         </>}
                         <Button
                             type="link"
@@ -329,6 +376,7 @@ const Friends: React.FC = () => {
                             style={{ padding: 0, fontSize: 12 }}
                             onClick={() => {
                                 const corpUsers = settings?.corpUsers?.map(item => {
+                                    delete item?.friendsTagContent
                                     delete item?.friendsMsgContent
                                     return item
                                 })
@@ -383,21 +431,15 @@ const Friends: React.FC = () => {
                 setNewVisible(false)
             }}
             onChange={(values, type) => {
-                const corpUsers = settings?.corpUsers?.map(item => {
-                    delete item?.friendsMsgContent
-                    return item
-                })
-                if (!values?.friendsContent && settings?.corpUsers?.some(item => item?.friendsMsgContent?.length)) {
+                if (!values?.friendsContent && settings?.corpUsers?.some(item => item?.friendsTagContent?.length)) {
                     setSettings({
                         ...settings,
                         ...values,
-                        corpUsers
                     })
                 } else {
                     setSettings({
                         ...settings,
                         ...values,
-                        corpUsers
                     })
                 }
 

+ 1 - 1
src/pages/weComTask/page/businessPlan/create/components/friends/previewFriendsStrategy.tsx

@@ -57,7 +57,7 @@ const PreviewFriendsStrategy: React.FC<{ friendsStrategy: { [x: string]: any },
             id='basicInfo'
         >
             <div className='block_tm'>
-                <Form.Item label={<strong>朋友圈标题</strong>} name="momentJobTitle">
+                <Form.Item label={<strong>朋友圈标题</strong>} name="momentSendName">
                     <Input placeholder="请输入标题" />
                 </Form.Item>
             </div>

+ 5 - 5
src/pages/weComTask/page/businessPlan/create/components/friends/strategy.tsx

@@ -101,7 +101,7 @@ const Strategy = forwardRef(({ value, onChange }: TASK_CREATE.StrategyProps, ref
         });
     };
 
-    const filedUpdateChange = ({ momentJobTitle, strategySettings }: any) => {
+    const filedUpdateChange = ({ momentSendName, strategySettings }: any) => {
         const isChecked = (content: NewStepsItem[]) => {
             return content.every(item => {
                 if (item.children) {
@@ -126,7 +126,7 @@ const Strategy = forwardRef(({ value, onChange }: TASK_CREATE.StrategyProps, ref
             }
         })
 
-        const isChecked1 = !!momentJobTitle
+        const isChecked1 = !!momentSendName
         const isChecked2 = isChecked(content)
         const stepsData = [
             { title: '朋友圈配置', description: '朋友圈标题', checked: isChecked1, id: 'basicInfo' },
@@ -164,7 +164,7 @@ const Strategy = forwardRef(({ value, onChange }: TASK_CREATE.StrategyProps, ref
                 }}
                 initialValues={{
                     taskType: 'novel',
-                    strategySettings: [{ sendData: [{ externalUserType: 'all' }] }],
+                    strategySettings: [{ }],
                 }}
                 onFieldsChange={() => {
                     filedUpdateChange(form.getFieldsValue())
@@ -173,7 +173,7 @@ const Strategy = forwardRef(({ value, onChange }: TASK_CREATE.StrategyProps, ref
             >
                 <Card title={<strong>基础信息配置</strong>} style={{ background: '#fff', marginBottom: 10 }} hoverable id='basicInfo'>
 
-                    <Form.Item label={<strong>朋友圈标题</strong>} name="momentJobTitle" rules={[{ required: true, message: '请输入标题!' }]}>
+                    <Form.Item label={<strong>朋友圈标题</strong>} name="momentSendName" rules={[{ required: true, message: '请输入标题!' }]}>
                         <Input placeholder="请输入标题" style={{ width: 358 }} allowClear />
                     </Form.Item>
                 </Card>
@@ -206,7 +206,7 @@ const Strategy = forwardRef(({ value, onChange }: TASK_CREATE.StrategyProps, ref
                                 </Card>
                             })}
                             <Form.Item>
-                                <Button type="primary" onClick={() => add({ sendData: [{ externalUserType: 'all' }] })} block icon={<PlusOutlined />}>
+                                <Button type="primary" onClick={() => add({})} block icon={<PlusOutlined />}>
                                     新增策略组
                                 </Button>
                             </Form.Item>

+ 73 - 1
src/pages/weComTask/page/businessPlan/create/const.tsx

@@ -337,7 +337,7 @@ export const restoreGroupData = (groupSendTaskAddDTO: { [x: string]: any }, conf
                             videoUrl: videoUrl,
                             mediaType: 'video'
                         }]
-                    } else if (msgType === 'TASK_STATUS_FILE') { 
+                    } else if (msgType === 'TASK_STATUS_FILE') {
                         const { file: { fileUrl } } = content
                         return [{
                             fileUrl: fileUrl,
@@ -387,6 +387,78 @@ export const restoreGroupData = (groupSendTaskAddDTO: { [x: string]: any }, conf
 }
 
 
+export const momentAttachmentList = (attachmentList: any[]) => {
+    return attachmentList.map((item: any) => {
+        switch (item.msgType) {
+            case 'TASK_CONTENT_IMAGE':
+                return {
+                    mediaType: 'image',
+                    imageUrl: item.image.picUrl,
+                    mediaFormat: item.image.mediaFormat,
+                    mediaSize: item.image.mediaSize,
+                    mediaPlayTime: item.image.mediaPlayTime,
+                    mediaWidth: item.image.mediaWidth,
+                    mediaHeight: item.image.mediaHeight
+                }
+            case 'TASK_STATUS_VIDEO':
+                return {
+                    mediaType: 'video',
+                    videoUrl: item.video.videoUrl,
+                    mediaFormat: item.image.mediaFormat,
+                    mediaSize: item.image.mediaSize,
+                    mediaPlayTime: item.image.mediaPlayTime,
+                    mediaWidth: item.image.mediaWidth,
+                    mediaHeight: item.image.mediaHeight
+                }
+            case 'TASK_STATUS_FILE':
+                return { mediaType: 'file', fileUrl: item.file.fileUrl }
+            case "TASK_CONTENT_LINK":
+                return { mediaType: 'link', linkDesc: item.link.desc, linkPicurl: item.link.picUrl, linkTitle: item.link.title, linkUrl: item.link.url }
+            case 'TASK_STATUS_MINIPROGRAM':
+                return {
+                    mediaType: 'miniprogram',
+                    miniprogramAppid: item.miniprogram.appId,
+                    miniprogramPage: item.miniprogram.page,
+                    miniprogramPicurl: item.miniprogram.picUrl,
+                    miniprogramTitle: item.miniprogram.title
+                }
+        }
+    })
+}
+/**
+ * 重置朋友圈数据为可编辑数据
+ * @param momentCreateDTO 
+ * @returns 
+ */
+export const restoreMomentData = (momentCreateDTO: { [x: string]: any }) => {
+    const { momentSendName, strategyList } = momentCreateDTO
+
+    const friendsContentDTO: { [x: string]: any }[] = []
+    const strategySettings = strategyList.map(item => {
+        const { taskDetail, ...params } = item
+        friendsContentDTO.push({
+            ...taskDetail,
+            contentDTO: taskDetail.contentDTO.map(content => {
+                return {
+                    text: content.text,
+                    attachmentList: momentAttachmentList(content.attachmentList)
+                }
+            })
+        })
+        return params
+    })
+
+    return {
+        friendsStrategy: {
+            momentSendName,
+            strategySettings
+        },
+        friendsContent: {
+            friendsContentDTO
+        }
+    }
+}
+
 // excel 配置
 // // 复杂表头示例(两级表头)
 // import XLSX from "xlsx-js-style";

+ 188 - 6
src/pages/weComTask/page/businessPlan/create/index.tsx

@@ -8,7 +8,7 @@ import { MediaContentProps, welcomeMsgJobTypeApi } from '@/pages/weComTask/API/w
 import SelectCorpUser from '../../corpUserManage/selectCorpUser';
 import { SearchOutlined, PlusOutlined, RedoOutlined, SaveOutlined, CheckOutlined } from '@ant-design/icons';
 import { getGroupData, getHighGroupData, getUserInDataData } from './const';
-import { highMassSendingColumns, massSendingColumns, userInheritColumns, welcomeColumns } from './tableConfig';
+import { friendsColumns, highMassSendingColumns, massSendingColumns, userInheritColumns, welcomeColumns } from './tableConfig';
 import SubmitModal from './submitModal';
 import { addTaskApi } from '@/pages/weComTask/API/businessPlan/create';
 import { useNavigate } from 'react-router-dom';
@@ -18,6 +18,7 @@ import UserInherit from './components/userInherit';
 import HighMassSending from './components/highMassSending';
 import { toJS } from 'mobx';
 import Friends from './components/friends';
+import SelectCwTag from '@/pages/weComTask/components/selectCwTag';
 
 export const DispatchTaskCreate = React.createContext<TASK_CREATE.DispatchTaskCreate | null>(null);
 /**
@@ -39,6 +40,7 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
     const [previewData, setPreviewData] = useState<TASK_CREATE.previewDataProps>({})
     const [subVisible, setSubVisible] = useState<boolean>(false) // 选择设置名称弹窗控制
     const [eaVisible, setEaVisible] = useState<boolean>(false)
+    const [qwVisible, setQwVisible] = useState<boolean>(false)
 
     const welcomeMsgJobType = useAjax(() => welcomeMsgJobTypeApi())//获取欢迎语类型
 
@@ -303,6 +305,83 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
             newPreviewData.highMassSending = highMassSending
         }
 
+        if ((settings?.friendsContent && Object.keys(settings?.friendsContent).length) && (settings?.friendsStrategy && Object.keys(settings?.friendsStrategy).length)) {
+            if (settings?.friendsContent?.friendsContentDTO?.some(item => item?.contentDTO?.some(i => i?.attachmentList?.some(a => ['link', 'miniprogram'].includes(a?.mediaType)))) && settings?.corpUsers?.every(item => !item?.friendsMsgContent?.length)) {
+                message.error('朋友圈需要配置小程序、链接,请上传配置好的群发Excel文件')
+                return
+            }
+            const friends = []
+            let rowLength = 1
+            if (!settings?.corpUsers?.every((item, i) => {
+                let uIndex = 0
+                const uLength = settings?.friendsStrategy?.strategySettings?.reduce((pre, _, i) => settings?.friendsContent?.friendsContentDTO?.[i]?.contentDTO?.length + pre, 0)
+                return settings?.friendsStrategy?.strategySettings?.every((strategyItem, strategyIndex) => {
+                    const friendsContentDTO = settings?.friendsContent?.friendsContentDTO?.[strategyIndex]
+                    let sIndex = 0
+                    return friendsContentDTO.contentDTO.every((contentItem, contentIndex) => {
+                        const msgContent = item.friendsMsgContent?.[strategyIndex]?.[contentIndex]
+                        if (contentItem?.attachmentList?.length && contentItem?.attachmentList?.some(item => (item?.mediaType === 'link' ? !msgContent?.linkUrl : item?.mediaType === 'miniprogram' ? (!msgContent?.miniprogramAppid || !msgContent?.miniprogramPage) : false))) {
+                            message.error('朋友圈内容配置错误,请检查')
+                            return false
+                        }
+
+                        const mediaItem = JSON.parse(JSON.stringify(contentItem?.attachmentList || []))
+                        if (contentItem?.text?.content) {
+                            mediaItem.push({
+                                mediaType: 'text',
+                                textContent: contentItem?.text?.content
+                            })
+                        }
+                        const contentReactNode = mediaItem.map(item => {
+                            switch (item.mediaType) {
+                                case 'link':
+                                    return `<span style="color: red">链接</span>`
+                                case 'miniprogram':
+                                    return `<span style="color: red">小程序</span>`
+                                case 'file':
+                                    return `<span>文件</span>`
+                                case 'video':
+                                    return `<span>视频</span>`
+                                case 'image':
+                                    return `<span>图片</span>`
+                                case 'text':
+                                    return `<span>文本</span>`
+                                default:
+                                    return `<span style="color: red">请联系管理员</span>`
+                            }
+                        })
+
+                        friends.push({
+                            corpUserId: item.corpUserId,
+                            corpUserName: item.name,
+                            bizType: settings?.bizType,
+                            channel: settings?.channel,
+                            platform: settings?.platform,
+                            templateProductId: settings?.templateProductId,
+                            momentSendName: settings?.friendsStrategy?.momentSendName,
+                            strategyData: strategyItem,
+                            strategyIndex: strategyIndex,
+                            sendData: item?.friendsTagContent,
+                            contentReactNode,
+                            contentIndex,
+                            sendMode: friendsContentDTO?.sendMode,
+                            content: contentItem,
+                            friendsContent: msgContent,
+                            id: rowLength,
+                            userRowSpan: uIndex === 0 ? uLength : 0,
+                            strategyRowSpan: sIndex === 0 ? friendsContentDTO.contentDTO.length : 0
+                        })
+                        rowLength++;
+                        uIndex++;
+                        sIndex++;
+                        return true
+                    })
+                })
+            })) {
+                return
+            }
+            newPreviewData.friends = friends
+        }
         if (newPreviewData && Object.keys(newPreviewData).length) {
             setPreviewData(newPreviewData)
         } else {
@@ -311,7 +390,7 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
     }
 
     const onSubmit = (values: any) => {
-        const { bizType, platform, templateProductId, corpUsers, welcomeMsgTemplateDTO, massSendingContent, massSendingStrategy, highMassSendingContent, highMassSendingStrategy, userInherit } = settings
+        const { bizType, platform, templateProductId, corpUsers, welcomeMsgTemplateDTO, massSendingContent, massSendingStrategy, highMassSendingContent, highMassSendingStrategy, userInherit, friendsContent, friendsStrategy } = settings
         const params = {
             ...values,
             bizType,
@@ -345,7 +424,12 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
                         }))
                     }
                 }
-
+                if (item?.friendsMsgContent) {
+                    params.momentSendContent = item.friendsMsgContent
+                }
+                if (item?.friendsTagContent) {
+                    params.externalUserTagList = item.friendsTagContent.map(item => item.value)
+                }
                 return params
             })
         }
@@ -507,7 +591,69 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
                 })
             }
         }
-        console.log('params---->', params)
+        // 朋友圈
+        if (friendsStrategy && Object.keys(friendsStrategy).length) {
+            params.momentCreateDTO = {
+                momentSendName: friendsStrategy.momentSendName,
+                strategyList: friendsStrategy.strategySettings.map((item, index) => {
+                    const { sendMode, contentDTO } = settings?.friendsContent?.friendsContentDTO?.[index]
+
+                    return {
+                        ...item,
+                        taskDetail: {
+                            sendMode,
+                            contentDTO: contentDTO.map(item => {
+
+                                return {
+                                    text: item.text,
+                                    attachmentList: item.attachmentList.map((item: any) => {
+                                        switch (item.mediaType) {
+                                            case 'image':
+                                                return {
+                                                    image: {
+                                                        picUrl: item.imageUrl,
+                                                        mediaFormat: item.mediaFormat,
+                                                        mediaSize: item.mediaSize,
+                                                        mediaPlayTime: item.mediaPlayTime,
+                                                        mediaWidth: item.mediaWidth,
+                                                        mediaHeight: item.mediaHeight
+                                                    }, msgType: 'TASK_CONTENT_IMAGE'
+                                                }
+                                            case 'video':
+                                                return {
+                                                    video: {
+                                                        videoUrl: item.videoUrl,
+                                                        mediaFormat: item.mediaFormat,
+                                                        mediaSize: item.mediaSize,
+                                                        mediaPlayTime: item.mediaPlayTime,
+                                                        mediaWidth: item.mediaWidth,
+                                                        mediaHeight: item.mediaHeight
+                                                    }, msgType: 'TASK_STATUS_VIDEO'
+                                                }
+                                            case 'file':
+                                                return { file: { fileUrl: item.fileUrl }, msgType: 'TASK_STATUS_FILE' }
+                                            case 'link':
+                                                return { link: { desc: item.linkDesc, picUrl: item.linkPicurl, title: item.linkTitle, url: item.linkUrl }, msgType: 'TASK_CONTENT_LINK' }
+                                            case 'miniprogram':
+                                                return {
+                                                    miniprogram: {
+                                                        appId: item.miniprogramAppid,
+                                                        page: item.miniprogramPage,
+                                                        picUrl: item.miniprogramPicurl,
+                                                        title: item.miniprogramTitle
+                                                    },
+                                                    msgType: 'TASK_STATUS_MINIPROGRAM'
+                                                }
+                                        }
+                                    })
+                                }
+                            })
+                        }
+                    }
+                })
+            }
+        }
+        
         addTask.run(params).then(res => {
             console.log(res)
             if (res?.data) {
@@ -636,6 +782,18 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
                             <CheckOutlined style={{ marginLeft: 5 }} />
                         </> : '高级群发外部客户选择'}
                     </Button>}
+
+                    {(settings?.friendsStrategy && Object.keys(settings?.friendsStrategy).length > 0 && settings?.corpUsers?.length > 0) && <Button
+                        onClick={() => {
+                            setQwVisible(true)
+                        }}
+                        type={settings?.corpUsers?.some(item => item?.friendsTagContent?.length) ? 'primary' : 'default'}
+                    >
+                        {settings?.corpUsers?.some(item => item?.friendsTagContent?.length) ? <>
+                            重选企业标签
+                            <CheckOutlined style={settings?.corpUsers?.every(item => item?.friendsTagContent?.length) ? { marginLeft: 5 } : { marginLeft: 5, color: 'red' }} />
+                        </> : '选择企业标签'}
+                    </Button>}
                 </Space>
 
                 <div className={style.settingsBody}>
@@ -657,7 +815,7 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
                                 {/* 高级群发 */}
                                 <HighMassSending />
                                 {/* 朋友圈 */}
-                                {/* <Friends /> */}
+                                <Friends />
                                 {/* 客户继承 */}
                                 <UserInherit />
                             </div>
@@ -688,7 +846,7 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
                     defaultActiveKey="1"
                     items={Object.keys(previewData).map(key => ({
                         key: key,
-                        label: { 'userInherit': '客户继承', 'massSending': '客户群发', 'welcome': '欢迎语', 'highMassSending': '高级群发' }[key],
+                        label: { 'userInherit': '客户继承', 'massSending': '客户群发', 'welcome': '欢迎语', 'highMassSending': '高级群发', 'friends': '朋友圈' }[key],
                         children: key === 'userInherit' ? <>
                             <Table
                                 dataSource={previewData[key]}
@@ -725,6 +883,15 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
                                 scroll={{ y: 550 }}
                                 pagination={false}
                             />
+                        </> : key === 'friends' ? <>
+                            <Table
+                                dataSource={previewData[key]}
+                                columns={friendsColumns()}
+                                rowKey={'id'}
+                                bordered={true}
+                                scroll={{ y: 550 }}
+                                pagination={false}
+                            />
                         </> : undefined
                     }))}
                     tabBarExtraContent={<Space size={10}>
@@ -766,6 +933,21 @@ const Create: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookLis
                 })
             }}
         />}
+
+        {qwVisible && <SelectCwTag
+            corpUsers={settings?.corpUsers || []}
+            visible={qwVisible}
+            onClose={() => {
+                setQwVisible(false)
+            }}
+            onChange={(value) => {
+                setQwVisible(false)
+                setSettings({
+                    ...settings,
+                    corpUsers: value
+                })
+            }}
+        />}
     </div>
 };
 

+ 113 - 0
src/pages/weComTask/page/businessPlan/create/tableConfig.tsx

@@ -5,6 +5,7 @@ import { businessPlanData, TIME_TYPE, welcomeContentData } from "./const";
 import FilterUser from "@/pages/weComTask/components/filterUser";
 import ShowContentTable from "./components/massSending/showContentTable";
 import WelcomeShowTable from "./components/welcome/welcomeShowTable";
+import FriendsShowTable from "./components/friends/friendsShowTable";
 const { Title, Text, Paragraph } = Typography;
 
 export const userInheritColumns = (): ColumnsType<AnyObject> => {
@@ -434,4 +435,116 @@ export const welcomeColumns = (bookPlatForm: { platformName: string, id: number,
             },
         }
     ]
+}
+
+
+export const friendsColumns = (): ColumnsType<AnyObject> => {
+    return [
+        {
+            title: '账号',
+            dataIndex: 'corpUserName',
+            key: 'corpUserName',
+            width: 150,
+            render(value, record) {
+                return <Text>{value}({record.corpUserId})</Text>
+            },
+            onCell: (record) => {
+                return { rowSpan: record.userRowSpan }
+            }
+        },
+        {
+            title: '朋友圈信息',
+            dataIndex: 'momentSendName',
+            key: 'momentSendName',
+            width: 130,
+            render(value, record) {
+                return <>
+                    <Title level={5} style={{ margin: 0 }}>标题:{value}</Title>
+                    {record?.bizType && <Paragraph style={{ margin: 0 }}>业务类型:{record?.bizType || '<空>'}</Paragraph>}
+                    {record?.platform && <Paragraph style={{ margin: 0 }}>书城:{record?.platform || '<空>'}</Paragraph>}
+                    {record?.templateProductId && <Paragraph style={{ margin: 0 }}>适用产品:{record?.templateProductId || '<空>'}</Paragraph>}
+                </>
+            },
+            onCell: (record) => {
+                return { rowSpan: record.userRowSpan }
+            }
+        },
+        {
+            title: '策略信息',
+            dataIndex: 'strategyData',
+            key: 'strategyData',
+            width: 200,
+            render(value, record) {
+                return <>
+                    <Title level={5} style={{ margin: 0 }}>策略{record.strategyIndex + 1}</Title>
+                    <Paragraph style={{ margin: 0 }}>名称:{value?.strategyName || '<空>'}</Paragraph>
+                    <Paragraph style={{ margin: 0 }}>执行类型:{TIME_TYPE[value?.timeRepeatType]}</Paragraph>
+                    {value?.sendDay && <Paragraph style={{ margin: 0 }}>执行时间:{value?.sendDay}</Paragraph>}
+                    {value?.startTime && <Paragraph style={{ margin: 0 }}>执行日期:{value?.startTime}~{value?.endTime ? value?.endTime : '长期执行'}</Paragraph>}
+                    {value?.sendTime && <Paragraph style={{ margin: 0 }}>执行时间:{value?.sendTime}</Paragraph>}
+                    {value?.repeatArray && <Paragraph style={{ margin: 0 }}>执行天数:{value?.repeatArray.join('、')}</Paragraph>}
+                </>
+            },
+            onCell: (record) => {
+                return { rowSpan: record.strategyRowSpan }
+            }
+        },
+        {
+            title: '发送对象',
+            dataIndex: 'sendData',
+            key: 'sendData',
+            width: 200,
+            render(value, record) {
+                return <>
+                    <Title level={5} style={{ margin: 0 }}>发送对象{record.sendDataIndex}</Title>
+                    <Paragraph style={{ margin: 0 }}>内容发送模式:{welcomeContentData?.find(i => i.value === record?.sendMode)?.label}</Paragraph>
+                    <Paragraph style={{ margin: 0 }} ellipsis={{ rows: 3, tooltip: true }}>企微标签:{value?.length > 0 ? value?.map(i => i.title).join('、') : '全部'}</Paragraph>
+                </>
+            },
+            onCell: (record) => {
+                return { rowSpan: record.strategyRowSpan }
+            }
+        },
+        {
+            title: '发送内容',
+            dataIndex: 'content',
+            key: 'content',
+            width: 200,
+            render(_, record) {
+                return <>
+                    <FriendsShowTable
+                        data={record?.content}
+                        name={<><span>内容{record.contentIndex + 1}:</span><span dangerouslySetInnerHTML={{ __html: record.contentReactNode?.map((item) => item).join('、') }}></span></>}
+                    />
+                </>
+            },
+        },
+        {
+            title: '图文链接',
+            dataIndex: 'linkUrl',
+            key: 'linkUrl',
+            width: 200,
+            render(_, record) {
+                return <Paragraph style={{ margin: 0, color: '#000', fontWeight: !!record?.friendsContent?.linkUrl ? 'bold' : 'normal' }}>{record?.friendsContent?.linkUrl || '--'}</Paragraph>
+            },
+        },
+        {
+            title: '小程序APPID',
+            dataIndex: 'miniprogramAppid',
+            key: 'miniprogramAppid',
+            width: 200,
+            render(_, record) {
+                return <Paragraph style={{ margin: 0, color: '#000', fontWeight: !!record?.friendsContent?.miniprogramAppid ? 'bold' : 'normal' }}>{record?.friendsContent?.miniprogramAppid || '--'}</Paragraph>
+            },
+        },
+        {
+            title: '小程序路径',
+            dataIndex: 'miniprogramPage',
+            key: 'miniprogramPage',
+            width: 200,
+            render(_, record) {
+                return <Paragraph style={{ margin: 0, color: '#000', fontWeight: !!record?.friendsContent?.miniprogramPage ? 'bold' : 'normal' }}>{record?.friendsContent?.miniprogramPage || '--'}</Paragraph>
+            },
+        }
+    ]
 }

+ 3 - 1
src/pages/weComTask/page/businessPlan/create/typings.d.ts

@@ -25,6 +25,7 @@ declare namespace TASK_CREATE {
         userInherit?: any[];
         massSending?: any[];
         highMassSending?: any[];
+        friends?: any[];
     }
     interface WelcomeProps<T> extends DefaultProps, DefaultChangeProps<T> {
         value?: { [x: string]: any };
@@ -66,7 +67,8 @@ declare namespace TASK_CREATE {
         highGroupMsgContent?: any[],
         externalUserTransferContent?: any[],
         externalUserList?: any[]
-        friendsMsgContent?: any[]
+        friendsTagContent?: any[],
+        friendsMsgContent?: any[],
     }
     // 配置保存字段
     interface SettingsProps {

+ 181 - 0
src/pages/weComTask/page/businessPlan/taskList/components/momentTask/index.tsx

@@ -0,0 +1,181 @@
+import { Card, Popover, Table, Tabs, Tag, Typography } from 'antd';
+import React, { useState } from 'react';
+import { momentAttachmentList, STATUS_ZJ, TIME_TYPE_ZJ } from '../../../create/const';
+import PreviewTime from '@/pages/weComTask/components/previewTime';
+import { QuestionCircleFilled } from '@ant-design/icons';
+import useNewToken from '@/Hook/useNewToken';
+import LookPyq from '@/pages/weComTask/components/previewMsg/lookPyq';
+import MomentXfCorpTabls from './momentXfCorpTabls';
+import MomentSendLog from './momentSendLog';
+const { Text } = Typography;
+
+interface Props {
+    momentTaskVOList: { [x: string]: any }[]
+}
+const MomentTask: React.FC<Props> = ({ momentTaskVOList }) => {
+
+
+    return <Table
+        dataSource={momentTaskVOList}
+        columns={[
+            {
+                title: '任务名称',
+                dataIndex: 'momentJobTitle',
+                key: 'momentJobTitle',
+                width: 100,
+                ellipsis: true,
+                align: 'center'
+            },
+            {
+                title: '企业ID',
+                dataIndex: 'corpId',
+                key: 'corpId',
+                width: 100,
+                ellipsis: true,
+                align: 'center'
+            },
+            {
+                title: '客服号',
+                dataIndex: 'corpMomentVisibleCorpUserVOList',
+                key: 'corpMomentVisibleCorpUserVOList',
+                width: 100,
+                ellipsis: true,
+                align: 'center',
+                render(value) {
+                    return value?.map(item => item.relationName)?.join('、')
+                },
+            },
+            {
+                title: '状态',
+                dataIndex: 'jobStatus',
+                key: 'jobStatus',
+                width: 100,
+                ellipsis: true,
+                align: 'center',
+                render(value) {
+                    return STATUS_ZJ[value] || '--'
+                },
+            },
+            {
+                title: '执行时间',
+                dataIndex: 'timeRepeatType',
+                key: 'timeRepeatType',
+                width: 100,
+                ellipsis: true,
+                align: 'center',
+                render(value, records: any) {
+                    return <>
+                        {TIME_TYPE_ZJ[value] || '--'}
+                        {value !== 'TIME_TYPE_SINGLE_TIMELY' && <Popover
+                            placement="left"
+                            content={<div>
+                                <PreviewTime
+                                    {...records}
+                                />
+                            </div>}
+                            styles={{ body: { width: 300, overflow: 'hidden', overflowY: 'auto', maxHeight: 400 } }}
+                        >
+                            <a style={{ color: '#000' }}><QuestionCircleFilled /></a>
+                        </Popover>}
+                    </>
+                },
+            },
+            {
+                title: '企微标签',
+                dataIndex: 'momentTagVOList',
+                key: 'momentTagVOList',
+                width: 100,
+                align: 'center',
+                render(value) {
+                    return value?.length > 0 ? <Text ellipsis>{value.reduce((pre, cur) => pre ? pre + '、' + cur?.tagList?.map(item => item.tagName).join('、') : cur?.tagList?.map(item => item.tagName).join('、'), '')}</Text> : '全部'
+                },
+            },
+            {
+                title: '创建人',
+                dataIndex: 'createUserName',
+                key: 'createUserName',
+                width: 100,
+                ellipsis: true,
+                align: 'center'
+            },
+            {
+                title: '创建时间',
+                dataIndex: 'createTime',
+                key: 'createTime',
+                width: 135,
+                ellipsis: true,
+                align: 'center'
+            },
+        ]}
+        scroll={{ x: 1000, y: 700 }}
+        rowKey={'id'}
+        size='small'
+        bordered
+        pagination={{
+            total: momentTaskVOList?.length || 0,
+            showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+            showSizeChanger: true,
+            showLessItems: true,
+            defaultPageSize: 20
+        }}
+        expandable={{
+            fixed: 'left',
+            expandRowByClick: true,
+            expandedRowRender: (record) => <ExpandedRow record={record} />
+        }}
+    />
+};
+
+export default React.memo(MomentTask);
+
+
+export const ExpandedRow: React.FC<{ record: any }> = ({ record }) => {
+
+    const { corpMomentContentList, id, corpId } = record
+    const { token } = useNewToken()
+    const [activeKey, setActiveKey] = useState<string>('2')
+
+    return <div style={{ display: 'flex', gap: 10, maxHeight: 450 }}>
+        <div style={{ width: 350, maxHeight: 450, overflow: 'hidden', overflowY: 'auto' }}>
+            <PreviewTime
+                {...record}
+            />
+            <h2>朋友圈内容</h2>
+            {corpMomentContentList?.map((item, index) => {
+                return <div key={index} style={{ marginTop: 5, backgroundColor: '#f1f1f1', borderRadius: 6, padding: '5px 10px 16px' }}>
+                    <h3 style={{ textAlign: 'center' }}>内容{index + 1}</h3>
+                    <LookPyq
+                        content={{ text: item?.text?.content, data: item?.attachmentList?.length > 0 ? momentAttachmentList(item.attachmentList) as any : undefined }}
+                    />
+                </div>
+            })}
+        </div>
+        <div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', gap: 10, height: '100%' }}>
+            <Tabs
+                destroyInactiveTabPane={false}
+                onChange={(key) => {
+                    setActiveKey(key)
+                }}
+                activeKey={activeKey}
+                type="card"
+                className="aaa"
+                items={[
+                    {
+                        key: '2',
+                        label: '下发企微号',
+                        children: <Card styles={{ body: { maxHeight: 400 } }}>
+                            {activeKey == '2' && <MomentXfCorpTabls corpId={corpId} taskId={id} />}
+                        </Card>
+                    },
+                    {
+                        key: '4',
+                        label: '发送记录',
+                        children: <Card styles={{ body: { minHeight: 185 } }}>
+                            {activeKey == '4' && <MomentSendLog corpMomentId={id} />}
+                        </Card>
+                    }
+                ]}
+            />
+        </div>
+    </div>
+}

+ 60 - 0
src/pages/weComTask/page/businessPlan/taskList/components/momentTask/momentSendLog.tsx

@@ -0,0 +1,60 @@
+import { useAjax } from "@/Hook/useAjax"
+import { Card, Space, Table, Tag } from "antd"
+import React, { useEffect, useState } from "react"
+import { sendLogTableConfig } from "./momentTableConfig"
+import { getSendLogApi } from "@/pages/weComTask/API/businessPlan/create"
+
+
+interface Props {
+    corpMomentId?: number
+}
+/**
+ * 发送记录
+ * @returns 
+ */
+const MomentSendLog: React.FC<Props> = ({ corpMomentId }) => {
+
+    /***************************/
+    const [queryForm, setQueryForm] = useState<{ pageSize: number, pageNum: number }>({ pageNum: 1, pageSize: 20 })
+    const getSendLog = useAjax((params: any) => getSendLogApi(params))//添加朋友圈
+    /***************************/
+
+    useEffect(() => {
+        if (corpMomentId) {
+            getSendLog.run({ ...queryForm, corpMomentId })
+        }
+    }, [queryForm, corpMomentId])
+
+    return <div>
+        <Space>
+            <span>发送记录</span>
+            <a onClick={() => getSendLog.refresh()}>刷新</a>
+        </Space>
+        <Table
+            style={{ marginBottom: 1 }}
+            dataSource={getSendLog?.data?.data?.records}
+            loading={getSendLog?.loading}
+            columns={sendLogTableConfig()}
+            scroll={{ x: 400, y: 500 }}
+            rowKey={(s) => {
+                return s.id
+            }}
+            size='small'
+            pagination={{
+                total: getSendLog?.data?.data?.total,
+                showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+                showSizeChanger: true,
+                showLessItems: true,
+                defaultCurrent: 1,
+                defaultPageSize: 20,//默认初始的每页条数
+                current: getSendLog?.data?.data?.current || 1,
+                pageSize: getSendLog?.data?.data?.size || 20,
+                onChange: (page, pageSize) => {
+                    setQueryForm({ ...queryForm, pageNum: page, pageSize })
+                }
+            }}
+        />
+    </div>
+}
+
+export default React.memo(MomentSendLog)

+ 132 - 0
src/pages/weComTask/page/businessPlan/taskList/components/momentTask/momentTableConfig.tsx

@@ -0,0 +1,132 @@
+import { Badge } from "antd"
+import { ColumnsType } from "antd/es/table"
+
+export function WelcomeMsgCorpUserListConfig(): ColumnsType<any> {
+
+    let arr: ColumnsType<any> = [
+        {
+            title: '企微号',
+            dataIndex: 'corpUserName',
+            key: 'corpUserName',
+            align: 'center',
+            width: 80,
+            ellipsis: true,
+        },
+        {
+            title: '所属企业',
+            dataIndex: 'corpName',
+            key: 'corpName',
+            align: 'center',
+            width: 120,
+            ellipsis: true,
+        },
+        {
+            title: '预计送达数',
+            dataIndex: 'sendCount',
+            key: 'sendCount',
+            align: 'center',
+            width: 90,
+            ellipsis: true,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '消息ID',
+            dataIndex: 'jobId',
+            key: 'jobId',
+            align: 'center',
+            width: 65,
+            ellipsis: true,
+        },
+        {
+            title: '发送成功',
+            dataIndex: 'sendSuccessCount',
+            key: 'sendSuccessCount',
+            align: 'center',
+            width: 80,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '发送失败',
+            dataIndex: 'sendFailedCount',
+            key: 'sendFailedCount',
+            align: 'center',
+            width: 80,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '评论数',
+            dataIndex: 'commentCount',
+            key: 'commentCount',
+            align: 'center',
+            width: 65,
+            ellipsis: true,
+        },
+        {
+            title: '点赞数',
+            dataIndex: 'likeCount',
+            key: 'likeCount',
+            align: 'center',
+            width: 65,
+            ellipsis: true,
+        },
+        {
+            title: '发送时间',
+            dataIndex: 'sendTime',
+            key: 'sendTime',
+            align: 'center',
+            width: 80,
+        },
+        {
+            title: '发送状态',
+            dataIndex: 'sendStatus',
+            key: 'sendStatus',
+            align: 'center',
+            width: 80,
+            render: (a) => {
+                return <Badge text={a === 1 ? '已确认发送' : '未确认发送'} status={a === 1 ? 'success' : 'error'} />
+            }
+        }
+    ]
+    return arr
+}
+
+
+
+export function sendLogTableConfig(): ColumnsType<any> {
+    
+    return [
+        {
+            title: '消息ID',
+            dataIndex: 'jobId',
+            key: 'jobId',
+            width: 70,
+            ellipsis: true
+        },
+        {
+            title: '发送时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            width: 110
+        },
+        {
+            title: '任务日志',
+            dataIndex: 'jobLog',
+            key: 'jobLog',
+            width: 250,
+            ellipsis: true
+        },
+        {
+            title: '任务状态',
+            dataIndex: 'jobStatus',
+            key: 'jobStatus',
+            width: 110,
+            ellipsis: true
+        },
+    ]
+}

+ 68 - 0
src/pages/weComTask/page/businessPlan/taskList/components/momentTask/momentXfCorpTabls.tsx

@@ -0,0 +1,68 @@
+import { useAjax } from "@/Hook/useAjax"
+import { useUpdateEffect } from "ahooks"
+import { Space, Table, Tag } from "antd"
+import React, { useEffect, useState } from "react"
+import { getMomentCorpUserListApi } from "@/pages/weComTask/API/businessPlan/create"
+import { WelcomeMsgCorpUserListConfig } from "./momentTableConfig"
+
+interface Props {
+    corpId: string
+    taskId: number
+}
+/**
+ * 群发下发企微号列表
+ * @param param0 
+ * @returns 
+ */
+const MomentXfCorpTabls: React.FC<Props> = ({ corpId, taskId }) => {
+
+    /******************************/
+    const [queryForms, setQueryForms] = useState<any>({ pageNum: 1, pageSize: 20, corpId, corpMomentId: taskId })
+
+    const getMomentCorpUserList = useAjax((params) => getMomentCorpUserListApi(params))
+    /******************************/
+
+    useEffect(() => {
+        setQueryForms({ ...queryForms, corpMomentId: taskId, corpId })
+    }, [corpId, taskId])
+
+    useUpdateEffect(() => {
+        if (queryForms.corpId && queryForms.corpMomentId) {
+            getMomentCorpUserList.run(queryForms)
+        }
+    }, [queryForms])
+
+    return <div>
+        <Space>
+            <span>下发企微号</span>
+            <a onClick={() => getMomentCorpUserList.refresh()}>刷新</a>
+        </Space>
+        {/* 表 */}
+        <Table
+            style={{ marginBottom: 1 }}
+            dataSource={getMomentCorpUserList?.data?.data?.records}
+            loading={getMomentCorpUserList?.loading}
+            columns={WelcomeMsgCorpUserListConfig()}
+            scroll={{ x: 400, y: 500 }}
+            rowKey={(s) => {
+                return s.id
+            }}
+            size='small'
+            pagination={{
+                total: getMomentCorpUserList?.data?.data?.total,
+                showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+                showSizeChanger: true,
+                showLessItems: true,
+                defaultCurrent: 1,
+                defaultPageSize: 200,//默认初始的每页条数
+                current: getMomentCorpUserList?.data?.data?.current || 1,
+                pageSize: getMomentCorpUserList?.data?.data?.size || 20,
+                onChange: (page, pageSize) => {
+                    setQueryForms({ ...queryForms, pageNum: page, pageSize })
+                }
+            }}
+        />
+    </div>
+}
+
+export default React.memo(MomentXfCorpTabls)

+ 4 - 1
src/pages/weComTask/page/businessPlan/taskList/log.tsx

@@ -6,6 +6,7 @@ import WelcomeTask from './components/welcomeTask';
 import ExternalUserTransferTask from './components/externalUserTransferTask';
 import GroupTask from './components/groupTask';
 import HighGroupTask from './components/highGroupTask';
+import MomentTask from './components/momentTask';
 
 
 interface Props {
@@ -53,7 +54,7 @@ const Log: React.FC<Props> = ({ data, bookPlatForm, bookList, visible, onClose }
                 <Tabs
                     items={Object.keys(previewData).filter(key => key === 'welcomeMsgTemplateVO' ? previewData[key] : previewData[key]?.length).map(key => ({
                         key: key,
-                        label: { 'externalUserTransferTasksVOList': '客户继承', 'groupSendTaskVOList': '客户群发', 'welcomeMsgTemplateVO': '欢迎语', 'messageSendTaskVOS': '高级群发' }[key],
+                        label: { 'externalUserTransferTasksVOList': '客户继承', 'groupSendTaskVOList': '客户群发', 'welcomeMsgTemplateVO': '欢迎语', 'messageSendTaskVOS': '高级群发', 'momentsTaskVOList': '朋友圈' }[key],
                         children: key === 'externalUserTransferTasksVOList' ? <>
                             <ExternalUserTransferTask externalUserTransferTasksVOList={previewData[key]} />
                         </> : key === 'groupSendTaskVOList' ? <>
@@ -62,6 +63,8 @@ const Log: React.FC<Props> = ({ data, bookPlatForm, bookList, visible, onClose }
                             <WelcomeTask welcomeMsgTemplateVO={previewData[key]} />
                         </> : key === 'messageSendTaskVOS' ? <>
                             <HighGroupTask groupSendTaskVOList={previewData[key]} />
+                        </> : key === 'momentsTaskVOList' ? <>
+                            <MomentTask momentTaskVOList={previewData[key]} />
                         </> : undefined
                     }))}
                 />

+ 42 - 1
src/pages/weComTask/page/businessPlan/taskList/tableConfig.tsx

@@ -3,11 +3,13 @@ import { AnyObject } from "antd/es/_util/type";
 import { ColumnsType } from "antd/es/table";
 import style from './index.less'
 import { QuestionCircleFilled } from '@ant-design/icons';
-import { restoreGroupData, restoreUserInheritData } from "../create/const";
+import { restoreGroupData, restoreMomentData, restoreUserInheritData } from "../create/const";
 import PreviewUserInherit from "../create/components/userInherit/previewUserlnherit";
 import PreviewMassSendingStrategy from "../create/components/massSending/previewMassSendingStrategy";
 import ShowContent from "../create/components/massSending/showContent";
 import PreviewWelcome from "../create/components/welcome/previewWelcome";
+import PreviewFriendsStrategy from "../create/components/friends/previewFriendsStrategy";
+import ShowFriendsContent from "../create/components/friends/showFriendsContent";
 
 const { Text, Paragraph } = Typography;
 const taskListColumns = (
@@ -74,6 +76,45 @@ const taskListColumns = (
                 </div> : <Text type="danger">当前没有欢迎语配置</Text>
             }
         },
+        {
+            title: '朋友圈配置',
+            dataIndex: 'momentCreateDTO',
+            key: 'momentCreateDTO',
+            width: 150,
+            ellipsis: true,
+            render: (value) => {
+                if (value && Object.keys(value)?.length > 0) {
+                    const data = restoreMomentData(value)
+                    return <div className={style.nameBox}>
+                        <div>
+                            <Text ellipsis>{value?.momentSendName || '<空>'}</Text>
+                        </div>
+                        <Popover
+                            placement="left"
+                            content={<div>
+                                <PreviewFriendsStrategy friendsStrategy={data?.friendsStrategy} />
+                            </div>}
+                            styles={{ body: { width: 360, overflow: 'hidden', overflowY: 'auto', maxHeight: 400 } }}
+                        >
+                            <a><QuestionCircleFilled /></a>
+                        </Popover>
+                        <Popover
+                            placement="left"
+                            content={<div>
+                                <ShowFriendsContent
+                                    strategySettings={data?.friendsStrategy?.strategySettings}
+                                    friendsContent={data?.friendsContent}
+                                />
+                            </div>}
+                            styles={{ body: { width: 360, overflow: 'hidden', overflowY: 'auto', maxHeight: 400 } }}
+                        >
+                            <a><QuestionCircleFilled /></a>
+                        </Popover>
+                    </div>
+                }
+                return <Text type="danger">当前没有朋友圈配置</Text>
+            }
+        },
         {
             title: '群发配置',
             dataIndex: 'groupSendTaskAddDTO',

+ 24 - 6
src/utils/utils.ts

@@ -231,15 +231,15 @@ export const randomString = (flag: boolean, min: number, max: number) => {
 export const groupBy = (array: any[], f: (item: any) => any[], isObject?: boolean) => {
   const groups: any = {};
   array.forEach(function (o) { //注意这里必须是forEach 大写
-      const group = JSON.stringify(f(o));
-      groups[group] = groups[group] || [];
-      groups[group].push(o);
+    const group = JSON.stringify(f(o));
+    groups[group] = groups[group] || [];
+    groups[group].push(o);
   });
   if (isObject) {
-      return groups
+    return groups
   }
   return Object.keys(groups).map(function (group) {
-      return groups[group];
+    return groups[group];
   });
 }
 
@@ -374,4 +374,22 @@ export const readFileAsBuffer = (file) => {
     reader.onload = (e) => resolve(e.target.result);
     reader.readAsArrayBuffer(file);
   });
-};
+};
+
+/**
+ * 字符串截取
+ * @param str 
+ * @param bytes 
+ * @returns 
+ */
+export const cutByBytes = (str: string, bytes: number) => {
+  let result = '';
+  let byteCount = 0;
+  for (let char of str) {
+    const charSize = char.charCodeAt(0) > 255 ? 3 : 1;
+    if (byteCount + charSize > bytes) break;
+    result += char;
+    byteCount += charSize;
+  }
+  return result !== str ? result + '...' : str;
+}