wjx hace 22 horas
padre
commit
6774f87b5e
Se han modificado 46 ficheros con 4983 adiciones y 64 borrados
  1. 100 0
      src/pages/weComTask/API/SelectGroupChat/index.tsx
  2. 52 2
      src/pages/weComTask/API/businessPlan/create.ts
  3. 31 2
      src/pages/weComTask/API/global.ts
  4. 12 0
      src/pages/weComTask/API/groupChat/index.ts
  5. 26 0
      src/pages/weComTask/API/groupLeaderManage/index.ts
  6. 51 1
      src/pages/weComTask/page/corpUserManage/index.tsx
  7. 3 1
      src/pages/weComTask/page/corpUserManage/selectCorpUser.tsx
  8. 73 0
      src/pages/weComTask/page/corpUserManage/settingsGroup.tsx
  9. 14 1
      src/pages/weComTask/page/groupChat/taskList/details.tsx
  10. 2 2
      src/pages/weComTask/page/groupChatSend/official/create/components/Strategy/settingsStrategy.tsx
  11. 1 1
      src/pages/weComTask/page/groupChatSend/official/create/components/content/settingsContent.tsx
  12. 34 0
      src/pages/weComTask/page/groupChatSend/official/create/const.ts
  13. 22 4
      src/pages/weComTask/page/groupChatSend/official/create/index.tsx
  14. 51 0
      src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupErrorCountList.tsx
  15. 35 0
      src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupReSend.tsx
  16. 336 0
      src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupTableConfig.tsx
  17. 76 0
      src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupTaskNotes.tsx
  18. 143 0
      src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupXfCorpTabls.tsx
  19. 185 0
      src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/index.tsx
  20. 24 6
      src/pages/weComTask/page/groupChatSend/official/taskList/index.tsx
  21. 8 24
      src/pages/weComTask/page/groupChatSend/official/taskList/log.tsx
  22. 43 5
      src/pages/weComTask/page/groupChatSend/official/taskList/tableConfig.tsx
  23. 68 0
      src/pages/weComTask/page/groupChatSend/robot/create/components/Strategy/index.tsx
  24. 211 0
      src/pages/weComTask/page/groupChatSend/robot/create/components/Strategy/previewStrategy.tsx
  25. 239 0
      src/pages/weComTask/page/groupChatSend/robot/create/components/Strategy/settingsSendObject.tsx
  26. 384 0
      src/pages/weComTask/page/groupChatSend/robot/create/components/Strategy/settingsStrategy.tsx
  27. 68 0
      src/pages/weComTask/page/groupChatSend/robot/create/components/content/index.tsx
  28. 81 0
      src/pages/weComTask/page/groupChatSend/robot/create/components/content/previewContent.tsx
  29. 238 0
      src/pages/weComTask/page/groupChatSend/robot/create/components/content/settingsContent.tsx
  30. 39 0
      src/pages/weComTask/page/groupChatSend/robot/create/const.ts
  31. 505 10
      src/pages/weComTask/page/groupChatSend/robot/create/index.tsx
  32. 227 0
      src/pages/weComTask/page/groupChatSend/robot/create/tableConfig.tsx
  33. 51 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupErrorCountList.tsx
  34. 35 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupReSend.tsx
  35. 336 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupTableConfig.tsx
  36. 76 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupTaskNotes.tsx
  37. 143 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupXfCorpTabls.tsx
  38. 212 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/index.tsx
  39. 228 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/shareTabls.tsx
  40. 15 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/index.less
  41. 216 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/index.tsx
  42. 55 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/log.tsx
  43. 123 0
      src/pages/weComTask/page/groupChatSend/robot/taskList/tableConfig.tsx
  44. 41 0
      src/pages/weComTask/page/groupChatSend/robot/typings.d.ts
  45. 14 2
      src/pages/weComTask/page/groupLeaderManage/bindDetails.tsx
  46. 56 3
      src/pages/weComTask/page/groupLeaderManage/index.tsx

+ 100 - 0
src/pages/weComTask/API/SelectGroupChat/index.tsx

@@ -0,0 +1,100 @@
+import { useAjax } from '@/Hook/useAjax';
+import React, { useEffect, useState } from 'react';
+import { ApiParamsChatListProps, getGroupChatListApi } from '../global';
+import { App, Button, Flex, Input, Modal, Table, Typography } from 'antd';
+const { Text } = Typography;
+
+interface SelectGroupChatProps {
+    corpId: string;
+    value?: { chatId: string; chatName: string, corpName: string, corpId: string };
+    onChange?: (value?: { chatId: string; chatName: string, corpName: string, corpId: string }) => void;
+}
+
+/**
+ * 选择群
+ * @param param0 
+ * @returns 
+ */
+const SelectGroupChat: React.FC<SelectGroupChatProps> = ({ corpId, value, onChange }) => {
+
+    /****************************************/
+    const { message } = App.useApp();
+    const [queryParams, setQueryParams] = useState<ApiParamsChatListProps>({
+        corpId,
+        pageNum: 1,
+        pageSize: 20,
+    });
+    const [visible, setVisible] = useState<boolean>(false);
+    const [selectedRows, setselectedRows] = useState<any[]>([])
+
+    const getGroupChatList = useAjax((params) => getGroupChatListApi(params))
+    /****************************************/
+
+    useEffect(() => {
+        if (value) {
+            setselectedRows([{ chatId: value.chatId, chatName: value.chatName, corpId: value.corpId, corpName: value.corpName }]);
+        }
+    }, [value])
+
+    useEffect(() => {
+        if (visible) {
+            getGroupChatList.run({ ...queryParams, corpId })
+        }
+    }, [visible, queryParams]);
+
+    const handleOk = () => {
+        if (selectedRows?.length) {
+            onChange?.({ chatId: selectedRows[0]?.chatId, chatName: selectedRows[0]?.chatName, corpId: selectedRows[0]?.corpId, corpName: selectedRows[0]?.corpName });
+            setVisible(false);
+        } else {
+            message.error('请选择群聊');
+        }
+    };
+
+    return <Flex gap={10} align='center'>
+        {value?.chatId && <div style={{ maxWidth: 'calc(100% - 90px)', overflow: 'hidden' }}><Text ellipsis>{value?.chatName || value?.chatId}</Text></div>}
+        <Button type="primary" onClick={() => setVisible(true)}>选择素材群</Button>
+        {visible && <Modal
+            title={<strong>选择素材群</strong>}
+            open={visible}
+            onCancel={() => setVisible(false)}
+            onOk={handleOk}
+        >
+            <Input.Search placeholder="请输入群名称" onSearch={(value) => { setQueryParams({ ...queryParams, chatName: value, pageNum: 1 }); }} allowClear style={{ marginBottom: 10, width: 250 }} />
+
+            <Table
+                columns={[
+                    {
+                        title: '群名称',
+                        dataIndex: 'chatName',
+                        key: 'chatName',
+                        render: (text: string, record: any) => <span>{text || record?.chatId || '--'}</span>,
+                    }
+                ]}
+                scroll={{ y: 400 }}
+                bordered
+                dataSource={getGroupChatList.data?.data?.records}
+                rowKey="chatId"
+                pagination={{
+                    total: getGroupChatList.data?.data?.total,
+                    current: getGroupChatList?.data?.data?.current || 1,
+                    pageSize: getGroupChatList?.data?.data?.size || 20,
+                    onChange: (page: number, pageSize: number) => {
+                        setQueryParams({ ...queryParams, pageNum: page, pageSize })
+                    }
+                }}
+                rowSelection={{
+                    selectedRowKeys: selectedRows?.map((item: any) => item?.chatId),
+                    type: 'radio',
+                    onChange(_, selectedRows) {
+                        console.log(selectedRows)
+                        setselectedRows(selectedRows)
+                    },
+                }}
+            />
+
+        </Modal>}
+    </Flex>;
+};
+
+export default SelectGroupChat;

+ 52 - 2
src/pages/weComTask/API/businessPlan/create.ts

@@ -132,9 +132,9 @@ export async function api_post_welcomeMsg_welcomeMsgCorpUserList(data: WelcomeMs
 
 
 export interface GetSendTaskGroupuserListProps {
-    corpId: string,
     pageNum: number,
     pageSize: number,
+    corpId?: string,
     msgId?: string,
     sendTimeEndTime?: string
     sendTimeStartTime?: string
@@ -157,6 +157,19 @@ export async function getSendTaskGroupuserListApi(data: GetSendTaskGroupuserList
     });
 }
 
+/**
+ * 群群发获取下发企微号列表
+ * @param data 
+ * @returns 
+ */
+export async function getSendGroupTaskGroupuserListApi(data: GetSendTaskGroupuserListProps) {
+    return request({
+        url: api + `/corpOperation/corp/group/send/task/user/list`,
+        method: 'POST',
+        data
+    });
+}
+
 /**
  * 获取高级群发下发企微号列表
  * @param data 
@@ -260,7 +273,7 @@ export async function reSendGroupTaskApi(data: { id: number, corpUserId?: string
 }
 
 export interface GetSendTaskGroupMsgListProps {
-    corpId: string,
+    corpId?: string,
     pageNum: number,
     pageSize: number,
     taskId: number,
@@ -280,6 +293,43 @@ export async function getSendTaskGroupMsgListApi(data: GetSendTaskGroupMsgListPr
     });
 }
 
+/**
+ * 获取群群发记录列表
+ * @param data 
+ * @returns 
+ */
+export async function getSendGroupTaskGroupMsgListApi(data: GetSendTaskGroupMsgListProps) {
+    return request({
+        url: api + `/corpOperation/corp/group/send/task/msg/list`,
+        method: 'POST',
+        data
+    });
+}
+
+/**
+ * 转发记录
+ * @param data 
+ * @returns 
+ */
+export async function getSendGroupTaskRobotListApi(data: GetSendTaskGroupuserListProps) {
+    return request({
+        url: api + `/corpOperation/corp/group/send/task/robot/list`,
+        method: 'POST',
+        data
+    });
+}
+
+/**
+ * 转发消息重试
+ * @returns 
+ */
+export async function putMessageUpdateReloadApi(taskLogId: any) {
+    return request({
+        url: `/corp/group/send/taskLog/status/update/${taskLogId}`,
+        method: 'PUT',
+    });
+}
+
 /**
  * 获取高级群发记录列表
  * @param data 

+ 31 - 2
src/pages/weComTask/API/global.ts

@@ -73,6 +73,35 @@ export async function getTagGroupListApi(data: any) {
     });
 }
 
+export interface ApiParamsChatListProps {
+    pageNum: number,
+    pageSize: number,
+    corpId: string,//企业ID
+    corpUserId?: string[],//企微客服id
+    chatName?: string,//群名称
+    createTimeEnd?: string,//群创建结束时间
+    createTimeStart?: string,//群创建开始时间
+    managerName?: string,//群管理名称
+    ownerName?: string,//群主名称
+    userCountMax?: string | number,//群人数最大值
+    userCountMin?: string | number,//群人数最小值
+    chatIdList?: number[],
+    remark?: string
+}
+
+/**
+ * 企业微信群列表
+ * @param data 
+ * @returns 
+ */
+export async function getGroupChatListApi(data: ApiParamsChatListProps) {
+    return request({
+        url: '/corp/group/chat/list',
+        method: 'POST',
+        data
+    })
+}
+
 /**获取背景图ID列表*/
 export async function api_get_img_typeList() {
     return request({
@@ -141,7 +170,7 @@ export async function getAllBookApi() {
 export async function getGameListNewApi(data: { sourceSystem: string }) {
     return request({
         url: api + `/gameData/choice/game/list`,
-        method: 'POST', 
-        data 
+        method: 'POST',
+        data
     });
 }

+ 12 - 0
src/pages/weComTask/API/groupChat/index.ts

@@ -79,6 +79,18 @@ export async function getProjectTaskLogListApi(data: { pageNum: number, pageSize
     });
 }
 
+/**
+ * 总数
+ * @param taskId 
+ * @returns 
+ */
+export async function getProjectTaskLogCountApi(taskId: number) {
+    return request({
+        url: api + `/corpOperation/corp/pullGroup/project/count/${taskId}`,
+        method: 'POST'
+    });
+}
+
 /**
  * 任务日志详情列表
  * @param data 

+ 26 - 0
src/pages/weComTask/API/groupLeaderManage/index.ts

@@ -25,6 +25,19 @@ export function getCorpUserChatListApi(data: getCorpUserChatListProps) {
     })
 }
 
+/**
+ * 删除群主号
+ * @param data 
+ * @returns 
+ */
+export function delCorpUserChatApi(data: number[]) {
+    return request({
+        url: api + `/corpOperation/corpUserChat/deleteBatch`,
+        method: 'DELETE',
+        data
+    })
+}
+
 
 interface AddCorpUserChatProps {
     corpUserList: {
@@ -95,6 +108,19 @@ export function unbindCorpUserChatApi(data: BindCorpUserChatProps) {
     })
 }
 
+/**
+ * 同步当前群主号下的所有群聊的机器人
+ * @param data 
+ * @returns 
+ */
+export function syncCorpChatRobotApi(data: { corpId: string, corpUserId: string, robotIdList: string[] }) {
+    return request({
+        url: `/corp/group/chat/syncCorpChatRobot`,
+        method: 'PUT',
+        data
+    })
+}
+
 /**
  * 所有群主号
  * @param params 

+ 51 - 1
src/pages/weComTask/page/corpUserManage/index.tsx

@@ -1,6 +1,6 @@
 import { App, Button, Card, Input, Pagination, Select, Table, Tabs } from 'antd';
 import React, { useEffect, useRef, useState } from 'react';
-import { MenuUnfoldOutlined, MenuFoldOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
+import { MenuUnfoldOutlined, MenuFoldOutlined, PlusOutlined, SearchOutlined, UsergroupAddOutlined } from '@ant-design/icons';
 import style from './index.less'
 import GroupLeft from './groupLeft';
 import TeamMembers from '@/components/Team/teamMembers';
@@ -11,6 +11,7 @@ import SearchBox from '../../components/searchBox';
 import { WeTableConfig } from './tableConfig';
 import { useSize } from 'ahooks';
 import { getBindMpListApi } from '../../API/corpUserAssign';
+import SettingsGroup from './settingsGroup';
 
 /**
  * 企微号管理
@@ -26,6 +27,8 @@ const CorpUserManage: React.FC = () => {
     const [queryFormNew, setQueryFormNew] = useState<CORP_USER_ASSIGN_API.GetCorpUserProps>({ pageNum: 1, pageSize: 20 })
     const [activeKey, setActiveKey] = useState<string>('1')
     const [showLeft, setShowLeft] = useState<boolean>(false)
+    const [selectedRows, setselectedRows] = useState<any[]>([])
+    const [settingsGroupData, setSettingsGroupData] = useState<{ visible?: boolean, type: 'add' | 'del', data: any[] }>();
 
     const allOfMember = useAjax(() => getAdAccountAllOfMember())
     const getBindMpList = useAjax(() => getBindMpListApi())
@@ -99,6 +102,8 @@ const CorpUserManage: React.FC = () => {
                         <Button type="primary" onClick={() => {
                             setQueryForm({ ...queryFormNew, pageNum: 1 })
                         }} loading={getCorpUser.loading} icon={<SearchOutlined />}>搜索</Button>
+                        <Button type="primary" disabled={selectedRows?.length === 0} onClick={() => { setSettingsGroupData({ visible: true, data: selectedRows, type: 'add' }) }} icon={<UsergroupAddOutlined />}>添加到分组</Button>
+                        <Button type="primary" danger disabled={selectedRows?.length === 0} onClick={() => { setSettingsGroupData({ visible: true, data: selectedRows, type: 'del' }) }} icon={<UsergroupAddOutlined />}>移除分组</Button>
                     </>}
                 >
                     <>
@@ -189,6 +194,40 @@ const CorpUserManage: React.FC = () => {
                         rowKey={'corpUserId'}
                         loading={getCorpUser?.loading}
                         scroll={{ y: size?.height && ref.current ? size?.height - ref.current.querySelector('.ant-table-thead').clientHeight : 300 }}
+                        rowSelection={{
+                            selectedRowKeys: selectedRows?.map((item: any) => item?.corpUserId),
+                            onSelect: (record: { corpUserId: string }, selected: boolean) => {
+                                let newData = JSON.parse(JSON.stringify(selectedRows))
+                                if (selected) {
+                                    newData.push({ ...record })
+                                } else {
+                                    newData = newData.filter((item: { corpUserId: string }) => item.corpUserId !== record.corpUserId)
+                                }
+                                setselectedRows(newData)
+                            },
+                            onSelectAll: (selected: boolean, _: { corpUserId: string }[], changeRows: { corpUserId: string }[]) => {
+                                let newData = JSON.parse(JSON.stringify(selectedRows || '[]'))
+                                if (selected) {
+                                    changeRows.forEach((item: { corpUserId: string }) => {
+                                        const index = newData.findIndex((ite: { corpUserId: string }) => ite.corpUserId === item.corpUserId)
+                                        if (index === -1) {
+                                            newData.push(item)
+                                        }
+                                    })
+                                } else {
+                                    const newSelectAccData = newData.filter((item: { corpUserId: string }) => {
+                                        const index = changeRows.findIndex((ite: { corpUserId: string }) => ite.corpUserId === item.corpUserId)
+                                        if (index !== -1) {
+                                            return false
+                                        } else {
+                                            return true
+                                        }
+                                    })
+                                    newData = newSelectAccData
+                                }
+                                setselectedRows(newData)
+                            }
+                        }}
                     />
                 </div>
 
@@ -212,6 +251,17 @@ const CorpUserManage: React.FC = () => {
                 </div>
             </Card>
         </div>
+
+        {/* 设置分组 */}
+        {settingsGroupData?.visible && <SettingsGroup
+            {...settingsGroupData}
+            onClose={() => setSettingsGroupData(undefined)}
+            onChange={() => {
+                getCorpUser.refresh();
+                setSettingsGroupData(undefined)
+                setselectedRows([])
+            }}
+        />}
     </div>
 };
 

+ 3 - 1
src/pages/weComTask/page/corpUserManage/selectCorpUser.tsx

@@ -15,6 +15,7 @@ interface Props {
     value?: { corpUserId: string, name: string, corpName: string, corpId: string }[];
     onChange?: (value: { corpUserId: string, name: string, corpName: string, corpId: string }[]) => void;
     placeholder?: React.ReactNode
+    width?: number | string
 }
 
 /**
@@ -22,7 +23,7 @@ interface Props {
  * @param param0 
  * @returns 
  */
-const SelectCorpUser: React.FC<Props> = ({ value = [], onChange, placeholder, ...itr }) => {
+const SelectCorpUser: React.FC<Props> = ({ value = [], onChange, placeholder, width, ...itr }) => {
 
     /************************************************************/
     const { token } = useNewToken()
@@ -37,6 +38,7 @@ const SelectCorpUser: React.FC<Props> = ({ value = [], onChange, placeholder, ..
                 padding: `0px ${token.paddingXS}px`,
                 borderRadius: token.borderRadius,
                 height: token.controlHeight,
+                width: width || '100%',
             }}
             onClick={() => setOpen(true)}
         >

+ 73 - 0
src/pages/weComTask/page/corpUserManage/settingsGroup.tsx

@@ -0,0 +1,73 @@
+import { App, Modal, Select } from 'antd';
+import React, { useEffect, useState } from 'react';
+import { addAccountToGroupApi, delAccountToGroupApi, getGroupListApi } from '../../API/corpUserManage';
+import { useAjax } from '@/Hook/useAjax';
+
+interface SettingsGroupProps {
+    data: any[],
+    type: 'add' | 'del',
+    visible?: boolean,
+    onClose?: () => void,
+    onChange?: () => void
+}
+
+/**
+ * 批量设置分组
+ * @returns 
+ */
+const SettingsGroup: React.FC<SettingsGroupProps> = ({ data, type, visible, onClose, onChange }) => {
+
+    /*******************************************/
+    const { message } = App.useApp()
+    const [groupId, setGroupId] = useState<number>();
+    const getGroupList = useAjax(() => getGroupListApi())
+    const addAccountToGroup = useAjax((params) => addAccountToGroupApi(params))
+    const delAccountToGroup = useAjax((params) => delAccountToGroupApi(params))
+    /*******************************************/
+
+    useEffect(() => {
+        getGroupList.run()
+    }, [])
+
+    const handleOk = () => {
+        if (groupId) {
+            if (type === 'add') {
+                addAccountToGroup.run({ currGroupId: groupId, corpUserList: data.map(item => ({ corpId: item.corpId, corpUserId: item.corpUserId })) }).then(res => {
+                    onChange?.();
+                    message.success('设置分组成功')
+                })
+            } else {
+                delAccountToGroup.run({ currGroupId: groupId, corpUserList: data.map(item => ({ corpId: item.corpId, corpUserId: item.corpUserId })) }).then(res => {
+                    onChange?.();
+                    message.success('移除分组成功')
+                })
+            }
+
+        } else {
+            message.error('请选择分组')
+        }
+    }
+
+    return <Modal
+        title={<strong>{type === 'add' ? '添加到分组' : '移除分组'}</strong>}
+        open={visible}
+        onCancel={onClose}
+        onOk={handleOk}
+        confirmLoading={addAccountToGroup.loading || delAccountToGroup.loading}
+    >
+        <Select
+            value={groupId}
+            onChange={(e) => setGroupId(e)}
+            showSearch
+            placeholder="请选择分组"
+            filterOption={(input, option) =>
+                ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+            }
+            style={{ width: '100%' }}
+            allowClear
+            options={getGroupList?.data?.data?.map((item: { groupName: string, groupId: number }) => ({ label: item.groupName, value: item.groupId }))}
+        />
+    </Modal>
+};
+
+export default SettingsGroup;

+ 14 - 1
src/pages/weComTask/page/groupChat/taskList/details.tsx

@@ -1,5 +1,5 @@
 import { useAjax } from "@/Hook/useAjax"
-import { getProjectLogDetailsListApi, getProjectLogListApi, getProjectTaskLogListApi } from "@/pages/weComTask/API/groupChat"
+import { getProjectLogDetailsListApi, getProjectLogListApi, getProjectTaskLogCountApi, getProjectTaskLogListApi } from "@/pages/weComTask/API/groupChat"
 import FilterUserTooltip from "@/pages/weComTask/components/filterUser/filterUserTooltip";
 import { Badge, Button, Drawer, Flex, Modal, Popover, Space, Table, Typography } from "antd"
 import React, { useEffect, useState } from "react"
@@ -254,17 +254,30 @@ const ExpandedRow: React.FC<{ record: any }> = ({ record }) => {
     const [detalisData, setDetalisData] = useState<{ visible?: boolean, taskLogId?: number }>()
     const [queryForm, setQueryForm] = useState<{ pageNum: number, pageSize: number }>({ pageNum: 1, pageSize: 20 })
     const [failedUserDetails, setFailedUserDetails] = useState<{ visible?: boolean, list: any[] }>({ visible: false, list: [] })
+    const [total, setTotal] = useState<{ pullGroupCount?: number, pullGroupUserCount?: number, pullGroupUserSuccessCount?: number, pullSuccessCount?: number }>({})
+
     const getProjectTaskLogList = useAjax((params) => getProjectTaskLogListApi(params))
+    const getProjectTaskLogCount = useAjax((params) => getProjectTaskLogCountApi(params))
     /*****************************************/
 
     useEffect(() => {
         getProjectTaskLogList.run({ ...queryForm, taskId: record.id })
     }, [record.id, queryForm])
 
+    useEffect(() => {
+        getProjectTaskLogCount.run(record.id).then(res => {
+            setTotal(res?.data || {})
+        })
+    }, [record.id, queryForm])
+
     return <div style={{ maxHeight: 450 }}>
         <Flex gap={20} align="center" style={{ marginBottom: 10, marginTop: 5 }}>
             <Title level={4} style={{ margin: 0 }}>{record?.taskName} 任务列表</Title>
             <Button type="link" onClick={() => getProjectTaskLogList.refresh()}>刷新</Button>
+            <Text strong>预计拉群数:{total?.pullGroupCount || 0}</Text>
+            <Text strong>预计拉群客户数:{total?.pullGroupUserCount || 0}</Text>
+            <Text strong>拉群成功总客户数:{total?.pullGroupUserSuccessCount || 0}</Text>
+            <Text strong>拉群成功总数数:{total?.pullSuccessCount || 0}</Text>
         </Flex>
         <Table
             size='small'

+ 2 - 2
src/pages/weComTask/page/groupChatSend/official/create/components/Strategy/settingsStrategy.tsx

@@ -119,11 +119,11 @@ const SettingsStrategy: React.FC<Props> = ({ visible, onClose, value, onChange,
                 ((timeRepeatType === "TIME_TYPE_REPEAT_WEEK" || timeRepeatType === "TIME_TYPE_REPEAT_MONTH") && startTime && sendTime && repeatArray)
 
             const children = sendData?.map((sd, sdIndex) => {
-                const { sendGroupName, mpAccountId, corpId, externalUserType, externalUserFilter } = sd
+                const { sendGroupName, mpAccountId, corpIds, externalUserType, externalUserFilter } = sd
                 return {
                     title: `发送群对象${sdIndex + 1}`, id: `strategy_${index + 1}_sendData_${sdIndex + 1}`,
                     description: `名称、公众号、主体...`,
-                    checked: sendGroupName && mpAccountId && corpId && (externalUserType === 'all' ? true : externalUserFilter && Object.keys(externalUserFilter)?.length > 0)
+                    checked: sendGroupName && mpAccountId && corpIds?.length && (externalUserType === 'all' ? true : externalUserFilter && Object.keys(externalUserFilter)?.length > 0)
                 }
             })
             return {

+ 1 - 1
src/pages/weComTask/page/groupChatSend/official/create/components/content/settingsContent.tsx

@@ -184,7 +184,7 @@ const SettingsContent: React.FC<GROUP_CHAT_CREATE.FoundationProps<any>> = ({ vis
                                                                     return <>
                                                                         {fields.map(({ key, name, ...restField }, c_index) => {
                                                                             return <div key={c_index} id={`strategy_${index + 1}_sendData_${i + 1}_content_${c_index + 1}`}>
-                                                                                <h3 style={{ display: 'flex', justifyContent: 'space-between', margin: 0 }}>内容{index + 1}
+                                                                                <h3 style={{ display: 'flex', justifyContent: 'space-between', margin: 0 }}>内容{c_index + 1}
                                                                                     {fields?.length > 1 && <Button
                                                                                         type="dashed"
                                                                                         danger

+ 34 - 0
src/pages/weComTask/page/groupChatSend/official/create/const.ts

@@ -0,0 +1,34 @@
+/**
+ * 返回群发原始数据
+ * @param data 
+ */
+export const getGroupChatSendData = (data: { [x: string]: any }) => {
+    const { groupSendName, strategyList } = data
+    return {
+        groupSendName,
+        strategyList: strategyList.map(item => {
+            const { taskDetail, ...its } = item
+            return {
+                ...its,
+                sendData: taskDetail.map(td => {
+                    const { externalUserFilterName, externalUserFilter, ...itsd } = td
+                    const detail: { [x: string]: any } = {
+                        ...itsd,
+                        sendGroupName: externalUserFilterName
+                    }
+                    if (externalUserFilter) {
+                        detail.externalUserType = 'specify'
+                        const { configName, ...configContent } = externalUserFilter
+                        detail.externalUserFilter = {
+                            configName,
+                            configContent
+                        }
+                    } else {
+                        detail.externalUserType = 'all'
+                    }
+                    return detail
+                })
+            }
+        })
+    }
+}

+ 22 - 4
src/pages/weComTask/page/groupChatSend/official/create/index.tsx

@@ -16,6 +16,7 @@ import { PreviewColumns } from './tableConfig';
 import SubmitModal from '../../../businessPlan/create/submitModal';
 import { addTaskApi, getCreateDetailsApi, updateTaskApi } from '@/pages/weComTask/API/businessPlan/create';
 import { useNavigate } from 'react-router-dom';
+import { getGroupChatSendData } from './const';
 
 
 export const DispatchOfficialChatCreate = React.createContext<OFFICIAL_CHAT_CREATE.DispatchOfficialChatCreate | null>(null);
@@ -51,10 +52,27 @@ const OfficialCreate: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE
         const project = sessionStorage.getItem('PGSD_OFFICIALTASKID')
         if (project) {
             const { id, isCopy } = JSON.parse(project)
+            console.log('projectId-->', id, isCopy)
             if (!isCopy) {
                 setProjectId(id)
             }
-
+            getCreateDetails.run(id).then(res => {
+                sessionStorage.removeItem('PGSD_OFFICIALTASKID')
+                if (res?.data) {
+                    const { bizType, platform, templateProductId, channel, groupChatSendTaskAddDTO } = res.data
+                    let newSettings: OFFICIAL_CHAT_CREATE.SettingsProps = {
+                        bizType,
+                        platform: Number(platform) as any,
+                        templateProductId,
+                        channel
+                    }
+                    if (groupChatSendTaskAddDTO && Object.keys(groupChatSendTaskAddDTO).length > 0) {
+                        const data = getGroupChatSendData(groupChatSendTaskAddDTO)
+                        newSettings = { ...newSettings, strategyDTO: data }
+                    }
+                    setSettings(newSettings)
+                }
+            })
         } else {
             const task = localStorage.getItem('TASK_GROUP_CHAT_SEND_CREATE')
             if (task) {
@@ -288,10 +306,10 @@ const OfficialCreate: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE
             })
         }
     }
-
+    
     return <div className={style.create}>
         <Spin spinning={false}>
-            <Card title={<strong>官方群发{projectId ? getCreateDetails?.data?.data?.projectName + '任务编辑' : ''}配置区</strong>} className={`${style.card} ${style.config}`}>
+            <Card title={<strong>{projectId ? getCreateDetails?.data?.data?.projectName + '任务编辑' : ''}配置区</strong>} className={`${style.card} ${style.config}`}>
                 <Space wrap>
                     <Space.Compact>
                         <Button>业务类型</Button>
@@ -389,7 +407,7 @@ const OfficialCreate: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE
                         onConfirm={() => {
                             setSettings(undefined)
                             onPreviewReset()
-                            localStorage.removeItem('TASK_GROUP_CHAT_CREATE')
+                            localStorage.removeItem('TASK_GROUP_CHAT_SEND_CREATE')
                         }}
                     >
                         <Button icon={<RedoOutlined />} danger>清空配置/预设</Button>

+ 51 - 0
src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupErrorCountList.tsx

@@ -0,0 +1,51 @@
+import { useAjax } from "@/Hook/useAjax"
+import { Popover, Table } from "antd"
+import React from "react"
+import { groupErrorCountTableConfig } from "./groupTableConfig"
+import { getGroupFailUserListApi } from "@/pages/weComTask/API/businessPlan/create"
+
+
+
+
+interface Props {
+    failCount: any,
+    corpId: string,
+    msgId: string,
+    taskId: string
+}
+
+/**
+ * 获取群发记录中失败客户列表
+ * @returns 
+ */
+const GroupErrorCountList: React.FC<Props> = ({ failCount, corpId, msgId, taskId }) => {
+
+    /***********************************/
+    const getFailUserList = useAjax((params) => getGroupFailUserListApi(params))//获取朋友圈任务列表
+    /***********************************/
+
+    const getList = () => {
+        getFailUserList.run({ corpId, msgId, taskId })
+    }
+
+    return <>
+        {failCount ? <Popover
+            trigger={['click']}
+            styles={{ body: { width: 500 } }}
+            content={<Table
+                dataSource={getFailUserList?.data?.data?.map((item: any, index: number) => ({ ...item, id: item?.externalUserId + index }))}
+                rowKey={'id'}
+                scroll={{ y: 200 }}
+                loading={getFailUserList.loading}
+                columns={groupErrorCountTableConfig()}
+            />}
+            onOpenChange={(open) => {
+                if (open) getList()
+            }}
+        >
+            <a>{failCount}</a>
+        </Popover> : '--'}
+    </>
+}
+
+export default React.memo(GroupErrorCountList)

+ 35 - 0
src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupReSend.tsx

@@ -0,0 +1,35 @@
+import { useAjax } from "@/Hook/useAjax"
+import { reSendGroupTaskApi } from "@/pages/weComTask/API/businessPlan/create"
+import { App, Button } from "antd"
+import React from "react"
+
+
+interface Props {
+    id: number
+    corpUserId?: string
+    onChange?: () => void
+}
+/**
+ * 重新发送
+ * @returns 
+ */
+const GroupReSend: React.FC<Props> = ({ onChange, ...props }) => {
+
+    /*************************/
+    const { message } = App.useApp()
+    const reSendTask = useAjax((params) => reSendGroupTaskApi(params))
+    /*************************/
+
+    const sendHandle = () => {
+        reSendTask.run(props).then(res => {
+            if (res.data) {
+                message.success('发送成功')
+                onChange?.()
+            }
+        })
+    }
+
+    return <Button type="link" loading={reSendTask.loading} onClick={sendHandle} style={{ padding: 0 }}>重新发送</Button>
+}
+
+export default React.memo(GroupReSend)

+ 336 - 0
src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupTableConfig.tsx

@@ -0,0 +1,336 @@
+import useNewToken from "@/Hook/useNewToken"
+import PreviewMsg from "@/pages/weComTask/components/previewMsg"
+import { emoList } from "@/pages/weComTask/components/textEditor/Expression"
+import { copy } from "@/utils/utils"
+import { Avatar, Badge, Button, Space, Tooltip, Typography } from "antd"
+import { ColumnsType } from "antd/es/table"
+import { InfoCircleOutlined, QuestionCircleOutlined, UserOutlined } from "@ant-design/icons"
+import GroupErrorCountList from "./groupErrorCountList"
+import GroupReSend from "./groupReSend"
+import { GENDER_TYPE } from "@/pages/weComTask/page/businessPlan/create/const"
+
+export function groupMsgCorpUserListConfig(change: (data: any, type: string) => void): ColumnsType<any> {
+
+    let arr: ColumnsType<any> = [
+        {
+            title: '所属企业',
+            dataIndex: 'corpName',
+            key: 'corpName',
+            align: 'center',
+            width: 120,
+        },
+        {
+            title: '企微号',
+            dataIndex: 'userName',
+            key: 'userName',
+            align: 'center',
+            width: 120,
+            ellipsis: true,
+            render: (a: string, b: any) => {
+                return a + `(${b?.userId})`
+            }
+        },
+        {
+            title: '发送时间',
+            dataIndex: 'sendTime',
+            key: 'sendTime',
+            align: 'center',
+            width: 120,
+            ellipsis: true,
+        },
+        {
+            title: '预计发送时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            align: 'center',
+            width: 120,
+            ellipsis: true,
+        },
+        {
+            title: '发送状态',
+            dataIndex: 'status',
+            key: 'status',
+            align: 'center',
+            width: 100,
+            render: (a: number) => {
+                return <span>{{ '0': <Badge status="error" text='未发送' />, '2': <Badge status="success" text='已发送' /> }[a]}</span>
+            }
+        },
+        {
+            title: '发送对象',
+            dataIndex: 'userCount',
+            key: 'userCount',
+            align: 'center',
+            width: 100,
+            render: (a: number) => {
+                return <span>好友{a > 0 ? a : 0}个</span>
+            }
+        },
+        {
+            title: '发送成功',
+            dataIndex: 'sendSuccessCount',
+            key: 'sendSuccessCount',
+            align: 'center',
+            width: 80,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '发送失败',
+            dataIndex: 'sendFailCount',
+            key: 'sendFailCount',
+            align: 'center',
+            width: 80,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '因客户不是好友导致发送失败',
+            dataIndex: 'unFriendCount',
+            key: 'unFriendCount',
+            align: 'center',
+            width: 80,
+            ellipsis: true,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '因客户已经收到其他群发消息导致发送失败',
+            dataIndex: 'othersSendCount',
+            key: 'othersSendCount',
+            align: 'center',
+            width: 80,
+            ellipsis: true,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '消息ID',
+            dataIndex: 'msgId',
+            key: 'msgId',
+            align: 'center',
+            width: 100,
+            ellipsis: true,
+            render: (a: string) => {
+                return <a onClick={() => copy(a)}>{a}</a>
+            }
+        },
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            align: 'center',
+            fixed: 'right',
+            width: 120,
+            render: (a: any, b: { status: any, sendCancelRemindStatus: number }) => {
+                return b?.status == '2' ? '--' : <Space wrap>
+                    {b?.sendCancelRemindStatus === 1 ? '已取消发送' : <>
+                        <Button type="link" style={{ padding: 0 }} disabled={b?.status == '2'} onClick={() => { change(b, 'txSend') }} >{'提醒发送'}</Button>
+                        <Button type="link" style={{ padding: 0 }} disabled={b?.status == '2'} onClick={() => { change(b, 'qxSend') }} >{'取消发送'}</Button>
+                    </>}
+                </Space>
+            }
+        },
+    ]
+    return arr
+}
+
+
+export function groupTaskNotesTableConfig(refresh: () => void): ColumnsType<any> {
+    const { token } = useNewToken()
+    // 处理群发内容
+    function groupBody(item) {
+        let msgData = []
+        if (item?.text?.content) {
+            let newValue = item?.text?.content
+            let newEmo = emoList.reduce((prev, cur) => {
+                return [...prev, ...cur]
+            }, [])
+            let emos = newValue.match(/\[[\u4e00-\u9fa5]+\]/g)
+            if (emos && emos?.length) {
+                emos.forEach((emo: any) => {
+                    let emoData = newEmo.find(o => `[${o.name}]` === emo)
+                    if (emoData) {
+                        newValue = newValue.replace(emo, `<img src="${emoData.url}" alt="${emoData.name}" style="width:20px;height:20px"/>`)
+                    }
+                })
+            }
+            if (newValue.includes('\n')) {
+                newValue = newValue.replace('\n', `<br/>`)
+            }
+            let names = newValue.match(/%[\u4e00-\u9fa5]+%/g)
+            if (names && names?.length) {
+                newValue = newValue.replace('%昵称%', ` <span style="display: inline-block;margin:0 1px;position: relative; border: 1px solid ${token.colorBorder}; padding: ${token.paddingXS}px; border-radius: ${token.borderRadius}px;color: ${token.colorTextBase}; background: ${token.colorSuccess};color:${token.colorTextLightSolid}" contenteditable="false">昵称<strong data-name="昵称" style="padding: 0 6px;cursor:pointer" onclick="let html =document.querySelector('[data-name=昵称').parentElement.parentElement.innerHTML;let span = ' '+document.querySelector('[data-name=昵称]').parentElement.outerHTML+' ';console.log('=',html,'=');console.log('=',span,'=');document.execCommand('selectAll');document.execCommand('delete'); document.execCommand('insertHTML', true, html.replace(span,''));">X</strong></span> `)
+            }
+            msgData.push({
+                textContent: newValue,
+                mediaType: 'text'
+            })
+        }
+        if (item?.attachmentList && item?.attachmentList?.length > 0) {
+            let newAttachmentList = item?.attachmentList.map((item: any) => {
+                switch (item.msgType) {
+                    case 'TASK_CONTENT_IMAGE':
+                        return { mediaType: 'image', imageUrl: item?.image?.picUrl }
+                    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_FILE':
+                        return { mediaType: 'file', fileUrl: item?.file?.fileUrl }
+                    case 'TASK_STATUS_VIDEO':
+                        return { mediaType: 'video', videoUrl: item?.video?.videoUrl }
+                    case 'TASK_STATUS_MINIPROGRAM':
+                        return {
+                            mediaType: 'miniprogram',
+                            miniprogramAppid: item.miniprogram?.appId,
+                            miniprogramPage: item?.miniprogram?.page,
+                            miniprogramPicurl: item?.miniprogram?.picUrl,
+                            miniprogramTitle: item?.miniprogram?.title
+                        }
+                }
+            })
+            msgData = msgData.concat(newAttachmentList)
+        }
+        return msgData
+    }
+    return [
+        {
+            title: '任务ID',
+            dataIndex: 'taskId',
+            key: 'taskId',
+            align: 'center',
+            width: 65
+        },
+        {
+            title: '客服号名称',
+            dataIndex: 'corpUserName',
+            key: 'corpUserName',
+            align: 'center',
+            width: 150,
+            ellipsis: true
+        },
+        {
+            title: '消息ID',
+            dataIndex: 'msgId',
+            key: 'msgId',
+            align: 'center',
+            width: 250,
+            ellipsis: true,
+            render: (a: string) => {
+                return <a onClick={() => copy(a)}>{a}</a>
+            }
+        },
+        {
+            title: <div>发送内容<Tooltip title="2025-01-10前的群发无法查看内容"><InfoCircleOutlined /></Tooltip></div>,
+            dataIndex: 'contentDTO',
+            key: 'contentDTO',
+            align: 'center',
+            width: 90,
+            ellipsis: true,
+            render: (a, b) => {
+                let data = []
+                if (a) {
+                    data = groupBody(a)
+                }
+
+                return data?.length > 0 ? <Tooltip color={"#fff"} title={<PreviewMsg type="MSG" minWidth={"200px"} height={'calc(100vh - 400px)'} content={{ data }} />}>
+                    <a>查看内容</a>
+                </Tooltip> : "无内容"
+            }
+        },
+        {
+            title: '发送时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            ellipsis: true,
+            align: 'center',
+            width: 130
+        },
+        {
+            title: <>执行结果 <Tooltip title={<span>此结果为任务在后台创建后是否成功提交到腾讯。腾讯下发至手机和手机的最终执行结果请查看下发企微号。</span>}><QuestionCircleOutlined style={{ color: 'red' }} /></Tooltip></>,
+            dataIndex: 'errCode',
+            key: 'errCode',
+            width: 60,
+            align: 'center',
+            ellipsis: true,
+            render: (a: number) => {
+                return { 0: '成功', 1: '失败' }[a]
+            }
+        },
+        {
+            title: '失败人数',
+            dataIndex: 'failCount',
+            key: 'failCount',
+            align: 'center',
+            ellipsis: true,
+            width: 60,
+            render: (a: any, b: any) => {
+                return <GroupErrorCountList failCount={a} corpId={b?.corpId} msgId={b?.msgId} taskId={b?.taskId} />
+            }
+        },
+        {
+            title: '错误信息',
+            dataIndex: 'errMsg',
+            key: 'errMsg',
+            ellipsis: true,
+            width: 150
+        },
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            width: 70,
+            fixed: 'right',
+            align: 'center',
+            render(_, record) {
+                if (record.errCode === 1) {
+                    return <GroupReSend onChange={refresh} id={record.taskId} corpUserId={record.corpUserId} />
+                }
+                return '--'
+            },
+        },
+    ]
+}
+
+
+
+export function groupErrorCountTableConfig(): ColumnsType<any> {
+    return [
+        {
+            title: '客户名称',
+            dataIndex: 'name',
+            key: 'name',
+            align: 'center',
+            width: 100,
+            render: (a: string, b: any) => {
+                return <Space>
+                    <Avatar shape="square" size="small" icon={<UserOutlined />} src={b?.avatar} />
+                    <Typography.Paragraph ellipsis={{ rows: 1, tooltip: true }} style={{ width: 62, marginBottom: 0 }}>{a}</Typography.Paragraph>
+                </Space>
+            }
+        },
+        {
+            title: '用户ID',
+            dataIndex: 'externalUserId',
+            key: 'externalUserId',
+            align: 'center',
+            ellipsis: true,
+            render: (a: string) => {
+                return <a onClick={() => copy(a)}>{a}</a>
+            }
+        },
+        {
+            title: '性别',
+            dataIndex: 'gender',
+            key: 'gender',
+            align: 'center',
+            width: 80,
+            render: (a: number) => {
+                return <span>{GENDER_TYPE[a]}</span>
+            }
+        },
+    ]
+}

+ 76 - 0
src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupTaskNotes.tsx

@@ -0,0 +1,76 @@
+import { useAjax } from "@/Hook/useAjax"
+import { useUpdateEffect } from "ahooks"
+import { Button, DatePicker, Space, Table, Tag } from "antd"
+import React, { useEffect, useState } from "react"
+import { SearchOutlined } from '@ant-design/icons';
+import dayjs from "dayjs"
+import { groupTaskNotesTableConfig } from "./groupTableConfig"
+import { getSendGroupTaskGroupMsgListApi, GetSendTaskGroupMsgListProps } from "@/pages/weComTask/API/businessPlan/create"
+import SearchBox from "@/pages/weComTask/components/searchBox";
+
+interface Props {
+    taskId: number
+}
+/**
+ * 群发记录
+ * @returns 
+ */
+const GroupTaskNotes: React.FC<Props> = ({ taskId }) => {
+
+    /*************************************/
+    const [queryForms, setQueryForms] = useState<GetSendTaskGroupMsgListProps>({ pageNum: 1, pageSize: 20, sendTimeStartTime: dayjs().format('YYYY-MM-DD'), sendTimeEndTime: dayjs().format('YYYY-MM-DD'), taskId })
+
+    const getSendTaskMsgList = useAjax((params) => getSendGroupTaskGroupMsgListApi(params))
+    /*************************************/
+
+
+    useEffect(() => {
+        setQueryForms({ ...queryForms, taskId })
+    }, [taskId])
+
+    useUpdateEffect(() => {
+        if (queryForms.taskId) {
+            getSendTaskMsgList.run(queryForms)
+        }
+    }, [queryForms])
+
+    return <Space direction="vertical" style={{ width: '100%' }}>
+        <SearchBox
+            bodyPadding={1}
+            buttons={<Button onClick={() => getSendTaskMsgList.refresh()} loading={getSendTaskMsgList.loading} icon={<SearchOutlined />}>搜索</Button>}
+        >
+            <>
+                <DatePicker placeholder="发送开始时间" value={queryForms?.sendTimeStartTime ? dayjs(queryForms.sendTimeStartTime) : undefined} onChange={(e, date: any) => { setQueryForms({ ...queryForms, sendTimeStartTime: date }) }} />
+                <DatePicker placeholder="发送结束时间" value={queryForms?.sendTimeEndTime ? dayjs(queryForms.sendTimeEndTime) : undefined} onChange={(e, date: any) => { setQueryForms({ ...queryForms, sendTimeEndTime: date }) }} />
+            </>
+        </SearchBox>
+        {/* 表 */}
+        <Table
+            style={{ marginBottom: 1 }}
+            dataSource={getSendTaskMsgList?.data?.data?.records}
+            loading={getSendTaskMsgList?.loading}
+            columns={groupTaskNotesTableConfig(() => getSendTaskMsgList.refresh())}
+            scroll={{ x: 400, y: 320 }}
+            rowKey={(s) => {
+                return s.id
+            }}
+            size='small'
+            rowClassName={(record) => record?.errCode === 1 ? 'errorClassName' : ''}
+            pagination={{
+                total: getSendTaskMsgList?.data?.data?.total,
+                showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+                showSizeChanger: true,
+                showLessItems: true,
+                defaultCurrent: 1,
+                defaultPageSize: 200,//默认初始的每页条数
+                current: getSendTaskMsgList?.data?.data?.current || 1,
+                pageSize: getSendTaskMsgList?.data?.data?.size || 20,
+                onChange: (page, pageSize) => {
+                    setQueryForms({ ...queryForms, pageNum: page, pageSize })
+                }
+            }}
+        />
+    </Space>
+}
+
+export default React.memo(GroupTaskNotes)

+ 143 - 0
src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/groupXfCorpTabls.tsx

@@ -0,0 +1,143 @@
+import { useAjax } from "@/Hook/useAjax"
+import useNewToken from "@/Hook/useNewToken"
+import { useUpdateEffect } from "ahooks"
+import { App, Badge, Button, DatePicker, Input, Select, Space, Table, Tag } from "antd"
+import React, { useEffect, useState } from "react"
+import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
+import { groupMsgCorpUserListConfig } from "./groupTableConfig"
+import { cancelGroupSendApi, getSendGroupTaskGroupuserListApi, GetSendTaskGroupuserListProps, remindGroupSendApi, syncSendGroupTaskExternalApi, syncSendGroupTaskUserApi } from "@/pages/weComTask/API/businessPlan/create"
+
+
+interface Props {
+    taskId: number
+}
+/**
+ * 群发下发企微号列表
+ * @param param0 
+ * @returns 
+ */
+const GroupXfCorpTabls: React.FC<Props> = ({ taskId }) => {
+
+    /******************************/
+    const { message } = App.useApp()
+    const { token } = useNewToken()
+    const [queryForms, setQueryForms] = useState<GetSendTaskGroupuserListProps>({ pageNum: 1, pageSize: 20, taskId })
+
+    const getSendTaskuserList = useAjax((params) => getSendGroupTaskGroupuserListApi(params))
+    const syncSendTaskUser = useAjax((params) => syncSendGroupTaskUserApi(params))
+    const cancelSend = useAjax((params) => cancelGroupSendApi(params))
+    const remindSend = useAjax((params) => remindGroupSendApi(params))
+    const syncSendTaskExternal = useAjax((params) => syncSendGroupTaskExternalApi(params))
+    /******************************/
+
+    useEffect(() => {
+        setQueryForms({ ...queryForms, taskId })
+    }, [taskId])
+
+    useUpdateEffect(() => {
+        if (queryForms.taskId) {
+            getSendTaskuserList.run(queryForms)
+        }
+    }, [queryForms])
+
+    const syncHandle = () => {
+        if (queryForms.taskId) {
+            syncSendTaskUser.run({ taskId: queryForms.taskId }).then(res => {
+                if (res) {
+                    syncHandleUser()
+                }
+            })
+        } else {
+            message.error('企微ID任务ID空')
+        }
+    }
+
+    // 客户同步
+    const syncHandleUser = () => {
+        if (queryForms.taskId) {
+            syncSendTaskExternal.run({ taskId: queryForms.taskId }).then(res => {
+                if (res) {
+                    message.success('同步成功')
+                    getSendTaskuserList.refresh()
+                }
+            })
+        } else {
+            message.error('企微ID任务ID空')
+        }
+    }
+
+    const changeHandle = (data: any, type: string) => {
+        const hide = message.loading('设置中...', 0)
+        if (type === 'txSend') {
+            remindSend.run({ corpId: data?.corpId, msgId: data?.msgId }).then(res => {
+                hide()
+                message.success('提醒成功')
+                getSendTaskuserList.refresh()
+            }).catch(() => hide())
+        } else if (type === 'qxSend') {
+            cancelSend.run({ corpId: data?.corpId, msgId: data?.msgId }).then(res => {
+                hide()
+                message.success('取消成功')
+                getSendTaskuserList.refresh()
+            }).catch(() => hide())
+        }
+    }
+
+    return <div>
+        <Space style={{ width: '100%' }} wrap>
+            <Input placeholder="消息ID" style={{ width: 120 }} allowClear onChange={(e) => setQueryForms({ ...queryForms, msgId: e.target.value })} />
+            <Select
+                showSearch
+                style={{ minWidth: 100 }}
+                allowClear
+                onChange={(value) => {
+                    setQueryForms({ ...queryForms, status: value })
+                }}
+                value={queryForms?.status}
+                placeholder="发送状态"
+                filterOption={(input, option) =>
+                    ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+                }
+                options={[{ label: <Badge status="error" text='未发送' />, value: 0 }, { label: <Badge status="success" text='已发送' />, value: 2 }]}
+            />
+            <DatePicker placeholder="发送开始时间" onChange={(e, date) => { setQueryForms({ ...queryForms, sendTimeStartTime: date as string }) }} />
+            <DatePicker placeholder="发送结束时间" onChange={(e, date) => { setQueryForms({ ...queryForms, sendTimeEndTime: date as string }) }} />
+            <DatePicker.RangePicker placeholder={['预计送达时间(起始)', '预计送达时间(结束)']} onChange={(e, date) => { setQueryForms({ ...queryForms, createStartDate: date[0], createEndDate: date[1] }) }} />
+            <Button onClick={() => getSendTaskuserList.refresh()} loading={getSendTaskuserList.loading} icon={<SearchOutlined />}>搜索</Button>
+            <Button onClick={() => syncHandle()} loading={syncSendTaskUser.loading || syncSendTaskExternal.loading} type="primary" icon={<SyncOutlined />}>同步</Button>
+        </Space>
+
+        {/* 表 */}
+        <Table
+            style={{ marginTop: 10 }}
+            dataSource={getSendTaskuserList?.data?.data?.records}
+            loading={getSendTaskuserList?.loading}
+            columns={groupMsgCorpUserListConfig(changeHandle)}
+            scroll={{ x: 400, y: 320 }}
+            rowKey={(s) => {
+                return s.id
+            }}
+            size='small'
+            onRow={(row) => {
+                return !row.status ? {
+                    style: { background: token.colorPrimaryBgHover }
+                } : {}
+            }}
+            pagination={{
+                total: getSendTaskuserList?.data?.data?.total,
+                showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+                showSizeChanger: true,
+                showLessItems: true,
+                defaultCurrent: 1,
+                defaultPageSize: 200,//默认初始的每页条数
+                current: getSendTaskuserList?.data?.data?.current || 1,
+                pageSize: getSendTaskuserList?.data?.data?.size || 20,
+                onChange: (page, pageSize) => {
+                    setQueryForms({ ...queryForms, pageNum: page, pageSize })
+                }
+            }}
+        />
+    </div>
+}
+
+export default React.memo(GroupXfCorpTabls)

+ 185 - 0
src/pages/weComTask/page/groupChatSend/official/taskList/components/groupTask/index.tsx

@@ -0,0 +1,185 @@
+import { Card, Popover, Table, Tabs, Tag, Typography } from 'antd';
+import React, { useContext, useState } from 'react';
+import style from '../../index.less'
+import { QuestionCircleFilled } from '@ant-design/icons';
+import PreviewTime from '@/pages/weComTask/components/previewTime';
+import FilterUserTooltip from '@/pages/weComTask/components/filterUser/filterUserTooltip';
+import LookMsg from '@/pages/weComTask/components/previewMsg/lookMsg';
+import useNewToken from '@/Hook/useNewToken';
+import GroupXfCorpTabls from './groupXfCorpTabls';
+import GroupTaskNotes from './groupTaskNotes';
+import { DispatchTaskDetails } from '../../log';
+import { STATUS_ZJ, TIME_TYPE_ZJ } from '@/pages/weComTask/page/businessPlan/create/const';
+import { getMsgData } from '@/pages/weComTask/page/businessPlan/create/components/massSending/showContent';
+const { Text } = Typography;
+
+
+
+interface Props {
+    groupSendTaskVOList: { [x: string]: any }[]
+}
+
+/**
+ * 群发任务详情
+ * @returns 
+ */
+const GroupTask: React.FC<Props> = ({ groupSendTaskVOList }) => {
+
+    const { bookPlatForm } = useContext(DispatchTaskDetails)!;
+
+    return <Table
+        dataSource={groupSendTaskVOList}
+        columns={[
+            {
+                title: '任务名称',
+                dataIndex: 'taskName',
+                key: 'taskName',
+                width: 100,
+                ellipsis: true,
+                align: 'center'
+            },
+            {
+                title: '状态',
+                dataIndex: 'status',
+                key: 'status',
+                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: 'externalUserFilter',
+                key: 'externalUserFilter',
+                width: 100,
+                align: 'center',
+                render(value) {
+                    return value ? <div className={style.nameBox}>
+                        <div>
+                            <Text ellipsis>指定</Text>
+                        </div>
+                        <Popover
+                            placement="right"
+                            styles={{ body: { maxWidth: 350, maxHeight: 350, overflow: 'hidden', overflowY: 'auto' } }}
+                            mouseEnterDelay={0.5}
+                            content={<FilterUserTooltip
+                                bookCityList={bookPlatForm?.map(item => ({ label: item.platformName, value: item.platformKey }))}
+                                configName={value?.configName}
+                                data={value}
+                            />}
+                        >
+                            <a style={{ color: '#000' }}><QuestionCircleFilled /></a>
+                        </Popover>
+                    </div> : '全部'
+                },
+            },
+            {
+                title: '投手',
+                dataIndex: 'putUserName',
+                key: 'putUserName',
+                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: groupSendTaskVOList?.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 const ExpandedRow: React.FC<{ record: any }> = ({ record }) => {
+
+    const { contentDTO, id } = record
+    const { token } = useNewToken()
+    const [activeKey, setActiveKey] = useState<string>('2')
+
+    return <div style={{ display: 'flex', gap: 10, maxHeight: 450 }}>
+        <div style={{ width: 320, maxHeight: 450, overflow: 'hidden', overflowY: 'auto' }}>
+            <PreviewTime
+                {...record}
+            />
+            <h2>群发内容</h2>
+            <div style={{ marginTop: 5, backgroundColor: '#d9d9d9', borderRadius: 6, padding: '10px 10px 16px' }}>
+                <LookMsg data={getMsgData(contentDTO?.[0], token)} />
+            </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' && <GroupXfCorpTabls taskId={id} />}
+                        </Card>
+                    },
+                    {
+                        key: '4',
+                        label: '群发记录',
+                        children: <Card styles={{ body: { minHeight: 400 } }}>
+                            {activeKey == '4' && <GroupTaskNotes taskId={id} />}
+                        </Card>
+                    }
+                ]}
+            />
+        </div>
+    </div>
+}
+
+
+export default GroupTask;

+ 24 - 6
src/pages/weComTask/page/groupChatSend/official/taskList/index.tsx

@@ -8,6 +8,9 @@ import { SearchOutlined, DeleteOutlined, PauseCircleOutlined } from '@ant-design
 import Log from './log';
 import { inject, observer } from 'mobx-react';
 import { toJS } from 'mobx';
+import { getBindMpListApi } from '@/pages/weComTask/API/corpUserAssign';
+import { getCorpAllListApi } from '@/API/global';
+import { DefaultOptionType } from 'antd/es/select';
 
 /**
  * 任务列表
@@ -16,19 +19,34 @@ import { toJS } from 'mobx';
 const TaskList: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookListProps[], bookPlatForm: TASK_CREATE.BookPlatFormProps[] } } }> = ({ weComTaskStore }) => {
 
     /***********************************************/
-    const projectName = sessionStorage.getItem('CAMPCORP')
+    const projectName = sessionStorage.getItem('CAMPCORP_OFFICIAL')
     const { message } = App.useApp();
     const { bookList, bookPlatForm } = toJS(weComTaskStore.data)
     const [queryForm, setQueryForm] = useState<BUSINES_SPLAN_API.GetProjectListProps>({ pageNum: 1, pageSize: 20, projectName, projectType: 1 })
     const [queryFormNew, setQueryFormNew] = useState<BUSINES_SPLAN_API.GetProjectListProps>({ pageNum: 1, pageSize: 20, projectName, projectType: 1 })
     const [logOpenData, setLogOpenData] = useState<{ visible: boolean, data: any }>({ visible: false, data: {} })
     const [selectedRows, setselectedRows] = useState<any[]>([])
+    const [mpList, setMpList] = useState<DefaultOptionType[]>([])
+    const [corpList, setCorpList] = useState<DefaultOptionType[]>([])
 
     const getProjectList = useAjax((params) => getProjectListApi(params))
     const delProject = useAjax((params) => delProjectApi(params))
     const cancelProject = useAjax((params) => cancelProjectApi(params))
+    const getBindMpList = useAjax(() => getBindMpListApi())
+    const getCorpAllList = useAjax((param) => getCorpAllListApi(param))
     /***********************************************/
 
+    useEffect(() => {
+        getBindMpList.run().then(res => {
+            if (res?.data)
+                setMpList(res.data.map((item: any) => ({ label: item.name, value: item.id, appId: item.appId })))
+        })
+        getCorpAllList.run({}).then(res => {
+            if (res?.data)
+                setCorpList(res.data.map((item: any) => ({ label: item.corpName, value: item.corpId })))
+        })
+    }, [])
+
     useEffect(() => {
         const projectName = sessionStorage.getItem('CAMPCORP_OFFICIAL')
         if (projectName) {
@@ -47,9 +65,9 @@ const TaskList: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookL
 
     // 复制
     const handleCopy = (data: any, isCopy: boolean) => {
-        sessionStorage.setItem('OFFICIALTASKID', JSON.stringify({ id: data.id, isCopy }))
-        sessionStorage.setItem('oldPath', '/weComTask/businessPlan/create')
-        window.location.href = '/weComTask#/weComTask/businessPlan/create'
+        sessionStorage.setItem('PGSD_OFFICIALTASKID', JSON.stringify({ id: data.id, isCopy }))
+        sessionStorage.setItem('oldPath', '/weComTask/groupChatSend/official/create')
+        window.location.href = '/weComTask#/weComTask/groupChatSend/official/create'
     }
 
     // 删除
@@ -132,7 +150,7 @@ const TaskList: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookL
         </div>
         <Table
             dataSource={getProjectList?.data?.data?.records}
-            columns={taskListColumns(bookPlatForm, bookList, handleLog, handleCopy, handleDel)}
+            columns={taskListColumns(mpList, corpList, handleLog, handleCopy, handleDel)}
             rowKey={'id'}
             bordered={true}
             size='small'
@@ -186,8 +204,8 @@ const TaskList: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookL
         {/* 日志 */}
         {logOpenData.visible && <Log
             {...logOpenData}
-            bookPlatForm={bookPlatForm}
             bookList={bookList}
+            bookPlatForm={bookPlatForm}
             onClose={() => {
                 setLogOpenData({ visible: false, data: undefined })
             }}

+ 8 - 24
src/pages/weComTask/page/groupChatSend/official/taskList/log.tsx

@@ -1,7 +1,8 @@
 import { useAjax } from '@/Hook/useAjax';
 import { getProjectLogListApi } from '@/pages/weComTask/API/businessPlan/create';
-import { Drawer, Spin, Tabs } from 'antd';
+import { Drawer, Spin } from 'antd';
 import React, { useEffect, useState } from 'react';
+import GroupTask from './components/groupTask';
 
 
 interface Props {
@@ -9,7 +10,7 @@ interface Props {
     bookPlatForm: TASK_CREATE.BookPlatFormProps[]
     bookList: TASK_CREATE.BookListProps[]
     visible?: boolean,
-    onClose?: () => void,
+    onClose?: () => void
 }
 
 export const DispatchTaskDetails = React.createContext<TASK_CREATE.DispatchTaskDetails | null>(null);
@@ -18,15 +19,14 @@ export const DispatchTaskDetails = React.createContext<TASK_CREATE.DispatchTaskD
  * @param param0 
  * @returns 
  */
-const Log: React.FC<Props> = ({ data, bookPlatForm, bookList, visible, onClose }) => {
+const Log: React.FC<Props> = ({ data, visible, onClose, bookPlatForm, bookList }) => {
 
     /******************************************************/
-    const [previewData, setPreviewData] = useState<{ welcomeMsgTemplateVO?: any, groupSendTaskVOList?: any[], externalUserTransferTasksVOList?: any[] }>({})
+    const [previewData, setPreviewData] = useState<{ groupChatSendTaskVOList?: any[] }>({})
     const getProjectLogList = useAjax((params) => getProjectLogListApi(params))
     /******************************************************/
 
     useEffect(() => {
-        console.log(data.id)
         getProjectLogList.run(data.id).then(res => {
             if (res?.data) {
                 setPreviewData(res.data)
@@ -43,27 +43,11 @@ const Log: React.FC<Props> = ({ data, bookPlatForm, bookList, visible, onClose }
     >
 
         <Spin spinning={getProjectLogList.loading}>
-            {/* <DispatchTaskDetails.Provider
+            <DispatchTaskDetails.Provider
                 value={{ bookPlatForm, bookList }}
             >
-                <Tabs
-                    items={Object.keys(previewData).filter(key => key === 'welcomeMsgTemplateVO' ? previewData[key] : previewData[key]?.length).map(key => ({
-                        key: key,
-                        label: { 'externalUserTransferTasksVOList': '客户继承', 'groupSendTaskVOList': '客户群发', 'welcomeMsgTemplateVO': '欢迎语', 'messageSendTaskVOS': '高级群发', 'momentsTaskVOList': '朋友圈' }[key],
-                        children: key === 'externalUserTransferTasksVOList' ? <>
-                            <ExternalUserTransferTask externalUserTransferTasksVOList={previewData[key]} />
-                        </> : key === 'groupSendTaskVOList' ? <>
-                            <GroupTask groupSendTaskVOList={previewData[key]} />
-                        </> : key === 'welcomeMsgTemplateVO' ? <>
-                            <WelcomeTask welcomeMsgTemplateVO={previewData[key]} />
-                        </> : key === 'messageSendTaskVOS' ? <>
-                            <HighGroupTask groupSendTaskVOList={previewData[key]} />
-                        </> : key === 'momentsTaskVOList' ? <>
-                            <MomentTask momentTaskVOList={previewData[key]} />
-                        </> : undefined
-                    }))}
-                />
-            </DispatchTaskDetails.Provider> */}
+                <GroupTask groupSendTaskVOList={previewData?.groupChatSendTaskVOList} />
+            </DispatchTaskDetails.Provider>
         </Spin>
     </Drawer>
 };

+ 43 - 5
src/pages/weComTask/page/groupChatSend/official/taskList/tableConfig.tsx

@@ -3,11 +3,15 @@ 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 { getGroupChatSendData } from "../create/const";
+import PreviewStrategy from "../create/components/Strategy/previewStrategy";
+import { DefaultOptionType } from "antd/es/select";
+import PreviewContent from "../create/components/content/previewContent";
 
 const { Text, Paragraph } = Typography;
 const taskListColumns = (
-    bookPlatForm: any[],
-    bookList: any[],
+    mpList: DefaultOptionType[],
+    corpList: DefaultOptionType[],
     handleLog: (data: any) => void,
     handleCopy: (data: any, isCopy: boolean) => void,
     handleDel: (data: any, type: 'del' | 'cancel') => void,
@@ -57,6 +61,42 @@ const taskListColumns = (
                 </Paragraph>
             }
         },
+        {
+            title: '群发配置',
+            dataIndex: 'groupChatSendTaskAddDTO',
+            key: 'groupChatSendTaskAddDTO',
+            width: 170,
+            ellipsis: true,
+            render: (value) => {
+                if (value && Object.keys(value)?.length > 0) {
+                    const data = getGroupChatSendData(value)
+                    return <div className={style.nameBox}>
+                        <div>
+                            <Text ellipsis>{value?.groupSendName || '<空>'}</Text>
+                        </div>
+                        <Popover
+                            placement="left"
+                            content={<div>
+                                <PreviewStrategy strategyDTO={data} mpList={mpList} corpList={corpList} />
+                            </div>}
+                            styles={{ body: { width: 380, overflow: 'hidden', overflowY: 'auto', maxHeight: 400 } }}
+                        >
+                            <a><QuestionCircleFilled /></a>
+                        </Popover>
+                        <Popover
+                            placement="left"
+                            content={<div>
+                                <PreviewContent strategyList={data?.strategyList} />
+                            </div>}
+                            styles={{ body: { width: 500, overflow: 'hidden', overflowY: 'auto', maxHeight: 400 } }}
+                        >
+                            <a><QuestionCircleFilled /></a>
+                        </Popover>
+                    </div>
+                }
+                return <Text type="danger">当前没有群发配置</Text>
+            }
+        },
         {
             title: '创建时间',
             dataIndex: 'createTime',
@@ -70,11 +110,9 @@ const taskListColumns = (
             dataIndex: 'taskCount',
             key: 'taskCount',
             width: 260,
-            render(value, record) {
+            render(_, record) {
                 return <Flex gap={6}>
-                    <Text type={record.welcomeMsgCount > 0 ? 'success' : 'danger'}>欢迎语:{record.welcomeMsgCount}</Text>
                     <Text type={record.groupSendTaskCount > 0 ? 'success' : 'danger'}>群发:{record.groupSendTaskCount}</Text>
-                    <Text type={record.externalUserTransferTaskCount > 0 ? 'success' : 'danger'}>客户继承:{record.externalUserTransferTaskCount}</Text>
                 </Flex>
             },
         },

+ 68 - 0
src/pages/weComTask/page/groupChatSend/robot/create/components/Strategy/index.tsx

@@ -0,0 +1,68 @@
+import React, { useContext, useState } from "react";
+import style from '../../../../../businessPlan/create/index.less'
+import { App, Button, Empty, Popconfirm } from "antd";
+import useNewToken from "@/Hook/useNewToken";
+import { DispatchRobotChatCreate } from "../..";
+import SettingsStrategy from "./settingsStrategy";
+import PreviewStrategy from "./previewStrategy";
+
+const Strategy: React.FC = () => {
+
+    /*********************************/
+    const { message } = App.useApp()
+    const { token } = useNewToken()
+    const { setSettings, settings, onPreviewReset, mpList, corpList } = useContext(DispatchRobotChatCreate)!;
+    const [newVisible, setNewVisible] = useState<boolean>(false);
+    /*********************************/
+
+    return <>
+        <div className={`${style.settingsBody_content_row}`} style={{ width: '35%' }}>
+            <div className={`${style.settingsBody_content_col}`} style={{ width: '100%' }}>
+                <div className={style.title}>
+                    <span>策略</span>
+                    {settings?.strategyDTO && Object.keys(settings?.strategyDTO).length > 0 && <Popconfirm
+                        title="确定清空?"
+                        onConfirm={() => {
+                            setSettings(undefined)
+                            onPreviewReset();
+                        }}
+                    >
+                        <a style={{ color: 'red' }}>清空</a>
+                    </Popconfirm>}
+                </div>
+                <div className={style.detail}>
+                    <div className={style.detail_title}>群发策略配置</div>
+                    <div className={style.detail_body}>
+                        {settings?.strategyDTO && Object.keys(settings?.strategyDTO).length > 0 ? <>
+                            <PreviewStrategy strategyDTO={settings?.strategyDTO} mpList={mpList} corpList={corpList} />
+                        </> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
+                    </div>
+                </div>
+                <div className={style.detail_footer}>
+                    <Button type="link" style={{ padding: 0, fontSize: 12, color: token.colorPrimary }} size="small" onClick={() => setNewVisible(true)}>编辑</Button>
+                </div>
+            </div>
+        </div>
+
+        {/* 配置基础信息 */}
+        {newVisible && <SettingsStrategy
+            visible={newVisible}
+            value={settings?.strategyDTO}
+            mpList={mpList}
+            corpList={corpList}
+            onClose={() => {
+                setNewVisible(false);
+            }}
+            onChange={(values) => {
+                setSettings({
+                    ...settings,
+                    strategyDTO: values
+                });
+                onPreviewReset();
+                setNewVisible(false);
+            }}
+        />}
+    </>
+}
+
+export default React.memo(Strategy);

+ 211 - 0
src/pages/weComTask/page/groupChatSend/robot/create/components/Strategy/previewStrategy.tsx

@@ -0,0 +1,211 @@
+import React, { useEffect } from 'react';
+import dayjs from 'dayjs';
+import { Card, Form, Input, Radio, Select } from 'antd';
+import SendTimeSet from '@/pages/weComTask/components/sendTimeSet';
+import '../../../../../businessPlan/create/global.less'
+import FilterUser from '@/pages/weComTask/components/filterUser';
+import { DefaultOptionType } from 'antd/es/select';
+import SelectGroupChat from '@/pages/weComTask/API/SelectGroupChat';
+import SelectCorpUser from '@/pages/weComTask/page/corpUserManage/selectCorpUser';
+
+interface Props {
+    strategyDTO: { [x: string]: any }
+    mpList: DefaultOptionType[]
+    corpList: DefaultOptionType[]
+}
+
+const PreviewStrategy: React.FC<Props> = ({ strategyDTO, mpList, corpList }) => {
+
+    /**************************************/
+    const [form] = Form.useForm();
+    const strategyList = Form.useWatch('strategyList', form)
+    /**************************************/
+
+    useEffect(() => {
+        if (strategyDTO && Object.keys(strategyDTO).length) {
+            const data = {
+                ...strategyDTO, strategyList: strategyDTO?.strategyList?.map(item => {
+                    const { sendTime, startTime, endTime, sendDay, timeRepeatType } = item
+                    if (timeRepeatType === 'TIME_TYPE_SINGLE_PLACE') {
+                        return {
+                            ...item,
+                            timeRepeatType,
+                            sendDay: sendDay ? dayjs(sendDay + ' ' + sendTime) : undefined
+                        }
+                    }
+                    return {
+                        ...item,
+                        timeRepeatType,
+                        sendTime: sendTime ? dayjs('2025-04-25 ' + sendTime) : undefined,
+                        startTime: startTime ? dayjs(startTime) : undefined,
+                        endTime: endTime ? dayjs(endTime) : undefined,
+                        sendDay: sendDay ? dayjs(sendDay) : undefined
+                    }
+                })
+            }
+            form.setFieldsValue(data)
+        }
+    }, [strategyDTO])
+
+    return <Form
+        form={form}
+        name="newGroupChatStrategyShow"
+        labelAlign='left'
+        labelCol={{ span: 5 }}
+        colon={false}
+        preserve={true}
+    >
+        <Card title={<strong>基础信息配置</strong>} style={{ background: '#fff', marginBottom: 10 }} id='basicInfo'>
+            <div className='block_tm'>
+                <Form.Item label={<strong>任务名称</strong>} name="groupSendName" rules={[{ required: true, message: '请输入任务名称!' }]}>
+                    <Input placeholder="请输入任务名称" />
+                </Form.Item>
+            </div>
+        </Card>
+        <Form.List name="strategyList">
+            {(fields) => (
+                <>
+                    {fields.map(({ key, name, ...restField }, index) => {
+                        const timeRepeatType = strategyList?.[index]?.timeRepeatType
+                        const sendData = strategyList?.[index]?.sendData
+
+                        return <Card
+                            key={key}
+                            title={<strong>策略{index + 1} 配置</strong>}
+                            style={{ background: '#fff', marginBottom: 10 }}
+                            id={`strategy_${index + 1}`}
+                        >
+                            <div className='block_tm'>
+                                <Form.Item
+                                    {...restField}
+                                    name={[name, 'strategyName']}
+                                    label={<strong>策略名称</strong>}
+                                >
+                                    <Input placeholder='请输入策略名称' />
+                                </Form.Item>
+                                <SendTimeSet isShow active='all' form={form} restField={restField} name={name} timeRepeatType={timeRepeatType} />
+                                <Form.List name={[name, 'sendData']}>
+                                    {(fields) => (
+                                        <>
+                                            {fields.map(({ key, name, ...restField }, i) => {
+                                                const chatCorpId = sendData?.[i]?.chatCorpId
+                                                const robotCorpId = sendData?.[i]?.robotCorpId
+                                                return <Card
+                                                    key={i}
+                                                    title={<strong>策略{index + 1} 发送群对象{i + 1} 配置</strong>}
+                                                    style={{ background: '#fff', marginBottom: 10 }}
+                                                >
+                                                    <Form.Item {...restField} label={<strong>发送群对象名称</strong>} name={[name, 'sendGroupName']}>
+                                                        <Input placeholder="请输入发送群对象名称" />
+                                                    </Form.Item>
+                                                    <Form.Item
+                                                        {...restField}
+                                                        label={<strong>群聊关联公众号</strong>}
+                                                        name={[name, 'mpAccountId']}
+                                                    >
+                                                        <Select
+                                                            showSearch
+                                                            allowClear
+                                                            placeholder="选择公众号"
+                                                            filterOption={(input, option) =>
+                                                                (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                            }
+                                                            options={mpList}
+                                                        />
+                                                    </Form.Item>
+                                                    <Form.Item
+                                                        {...restField}
+                                                        label={<strong>群聊主体</strong>}
+                                                        name={[name, 'corpIds']}
+                                                    >
+                                                        <Select
+                                                            showSearch
+                                                            placeholder="选择群聊主体"
+                                                            filterOption={(input, option) =>
+                                                                (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                            }
+                                                            mode='multiple'
+                                                            options={corpList}
+                                                        />
+                                                    </Form.Item>
+                                                    <Form.Item
+                                                        {...restField}
+                                                        label={<strong>机器人主体</strong>}
+                                                        name={[name, 'robotCorpId']}
+                                                    >
+                                                        <Select
+                                                            showSearch
+                                                            allowClear
+                                                            placeholder="选择机器人主体"
+                                                            filterOption={(input, option) =>
+                                                                (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                            }
+                                                            options={corpList}
+                                                        />
+                                                    </Form.Item>
+                                                    {robotCorpId && <Form.Item
+                                                        {...restField}
+                                                        label={<strong>机器人号</strong>}
+                                                        name={[name, 'robotCorpUser']}
+                                                        rules={[{ required: true, message: '请选择素材群主体!' }]}
+                                                    >
+                                                        <SelectCorpUser placeholder="请选择机器人号" corpId={robotCorpId} type="radio" />
+                                                    </Form.Item>}
+                                                    <Form.Item
+                                                        {...restField}
+                                                        label={<strong>素材群主体</strong>}
+                                                        name={[name, 'chatCorpId']}
+                                                        rules={[{ required: true, message: '请选择素材群主体!' }]}
+                                                    >
+                                                        <Select
+                                                            showSearch
+                                                            placeholder="选择素材群主体"
+                                                            filterOption={(input, option) =>
+                                                                (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                            }
+                                                            options={corpList}
+                                                        />
+                                                    </Form.Item>
+                                                    {chatCorpId && <Form.Item
+                                                        {...restField}
+                                                        label={<strong>素材群</strong>}
+                                                        name={[name, 'corpGroupChat']}
+                                                        rules={[{ required: true, message: '请选择素材群!' }]}
+                                                    >
+                                                        <SelectGroupChat corpId={chatCorpId} />
+                                                    </Form.Item>}
+                                                    <Form.Item label={<strong>发送群对象配置</strong>}>
+                                                        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
+                                                            <Form.Item
+                                                                {...restField}
+                                                                name={[name, 'externalUserType']}
+                                                                noStyle
+                                                            >
+                                                                <Radio.Group options={[{ label: '全部', value: 'all' }, { label: '指定', value: 'specify' }]} />
+                                                            </Form.Item>
+                                                            {sendData?.[i]?.externalUserType === 'specify' && <div style={{ marginTop: 8, width: '100%' }}>
+                                                                <Form.Item
+                                                                    {...restField}
+                                                                    name={[name, 'externalUserFilter']}
+                                                                    noStyle
+                                                                >
+                                                                    <FilterUser configType={'GROUP_GROUP'} isSHow />
+                                                                </Form.Item>
+                                                            </div>}
+                                                        </div>
+                                                    </Form.Item>
+                                                </Card>
+                                            })}
+                                        </>
+                                    )}
+                                </Form.List>
+                            </div>
+                        </Card>
+                    })}
+                </>
+            )}
+        </Form.List>
+    </Form>
+};
+
+export default React.memo(PreviewStrategy);

+ 239 - 0
src/pages/weComTask/page/groupChatSend/robot/create/components/Strategy/settingsSendObject.tsx

@@ -0,0 +1,239 @@
+import { App, Button, Empty, Form, Input, Modal, Radio, Select, Space, Table } from 'antd';
+import React, { useState } from 'react';
+import { PlusOutlined } from '@ant-design/icons';
+import FilterUser from '@/pages/weComTask/components/filterUser';
+import { ColumnsType } from 'antd/es/table';
+
+/**
+ * 新增修改发送对象
+ * @param param0 
+ * @returns 
+ */
+const AddSendObject: React.FC<ROBOT_CHAT_CREATE.AddSendObjectProps> = ({ mpList, corpList, visible, onChange, onClose, initialValues }) => {
+
+    /**************************************/
+    const { message } = App.useApp();
+    const [form] = Form.useForm();
+    const externalUserType = Form.useWatch('externalUserType', form);
+    /**************************************/
+
+    const handleOk = () => {
+        form.validateFields().then((values) => {
+            if (initialValues?.id) {
+                values.id = initialValues.id
+            } else {
+                values.id = Date.now();
+            }
+            onChange?.(values)
+        }).catch(() => {
+            form.submit()
+        });
+    }
+
+    return <Modal
+        title={<strong>{(initialValues && Object.keys(initialValues).length > 0) ? '修改' : '新建'}群配置</strong>}
+        open={visible}
+        onCancel={onClose}
+        width={600}
+        onOk={handleOk}
+        styles={{ body: { maxHeight: 550, overflowY: 'auto' } }}
+    >
+        <Form
+            form={form}
+            name="AddSendObject"
+            labelAlign='left'
+            labelCol={{ span: 7 }}
+            colon={false}
+            labelWrap
+            scrollToFirstError={{
+                behavior: 'smooth',
+                block: 'center'
+            }}
+            onFinishFailed={({ errorFields }) => {
+                message.error(errorFields?.[0]?.errors?.[0])
+            }}
+            onFinish={handleOk}
+            initialValues={(initialValues && Object.keys(initialValues).length > 0) ? initialValues : { externalUserType: 'all' }}
+            preserve={true}
+        >
+            <Form.Item label={<strong>发送群对象名称</strong>} name={'sendGroupName'} rules={[{ required: true, message: '请输入发送群对象名称!' }]}>
+                <Input placeholder="请输入发送群对象名称" style={{ width: 358 }} allowClear />
+            </Form.Item>
+            <Form.Item
+                label={<strong>群聊关联公众号</strong>}
+                name={'weChatAppid'}
+            >
+                <Select
+                    showSearch
+                    allowClear
+                    placeholder="选择公众号"
+                    filterOption={(input, option) =>
+                        (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                    }
+                    style={{ width: 358 }}
+                    options={mpList}
+                />
+            </Form.Item>
+            <Form.Item
+                label={<strong>群聊主体</strong>}
+                name={'corpId'}
+                rules={[{ required: true, message: '请选择群聊主体!' }]}
+            >
+                <Select
+                    showSearch
+                    allowClear
+                    placeholder="选择群聊主体"
+                    filterOption={(input, option) =>
+                        (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                    }
+                    style={{ width: 358 }}
+                    options={corpList}
+                />
+            </Form.Item>
+            <Form.Item
+                label={<strong>发送群对象配置</strong>}
+                required
+            >
+                <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
+                    <Form.Item
+                        name={'externalUserType'}
+                        rules={[{ required: true, message: '请选择转移对象!' }]}
+                        noStyle
+                    >
+                        <Radio.Group options={[{ label: '全部', value: 'all' }, { label: '指定', value: 'specify' }]} />
+                    </Form.Item>
+                    {externalUserType === 'specify' && <div style={{ marginTop: 8, width: '100%' }}>
+                        <Form.Item
+                            name={'externalUserFilter'}
+                            rules={[{ required: true, message: '请选择群人群包!' }]}
+                            noStyle
+                        >
+                            <FilterUser configType={'GROUP_GROUP'} />
+                        </Form.Item>
+                    </div>}
+                </div>
+            </Form.Item>
+        </Form>
+    </Modal>
+}
+
+const ShowSendObjectTable: React.FC<ROBOT_CHAT_CREATE.ShowSendObjectTable> = ({ mpList, corpList, data, handleDelete, handleEdit, isPreview }) => {
+
+    const columns: ColumnsType<any> = [
+        {
+            title: '发送群对象名称',
+            dataIndex: 'sendGroupName',
+            key: 'sendGroupName',
+            width: 120,
+            ellipsis: true,
+            fixed: 'left'
+        },
+        {
+            title: '群聊关联公众号',
+            dataIndex: 'weChatAppid',
+            key: 'weChatAppid',
+            width: 100,
+            align: 'center',
+            ellipsis: true,
+            render(value) {
+                return mpList?.find(item => item.value === value)?.label || '--'
+            },
+        },
+        {
+            title: '群聊主体',
+            dataIndex: 'corpId',
+            key: 'corpId',
+            width: 100,
+            align: 'center',
+            ellipsis: true,
+            render(value) {
+                return corpList?.find(item => item.value === value)?.label || '--'
+            },
+        },
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            width: 100,
+            fixed: 'right',
+            align: 'center',
+            render(_, record) {
+                return <Space>
+                    <a onClick={() => {
+                        handleEdit?.(record)
+                    }}>修改</a>
+                    <a style={{ color: 'red' }} onClick={() => {
+                        handleDelete?.(record.id)
+                    }}>删除</a>
+                </Space>
+            },
+        }
+    ]
+    if (isPreview) {
+        columns.pop()
+    }
+
+    return <Table
+        size='small'
+        bordered
+        rowKey={'id'}
+        dataSource={data}
+        scroll={{ y: 1000 }}
+        columns={columns}
+    />
+}
+
+
+const SettingsSendObject: React.FC<ROBOT_CHAT_CREATE.SettingsSendObjectProps> = ({ mpList, corpList, value, onChange }) => {
+
+    /******************************************/
+    const [visible, setVisible] = useState<boolean>(false)
+    const [initialValues, setInitialValues] = useState<any>()
+    /******************************************/
+
+    return <>
+        {value?.length > 0 ? <ShowSendObjectTable
+            mpList={mpList}
+            corpList={corpList}
+            data={value}
+            handleDelete={(id) => {
+                onChange?.(value.filter(item => item.id !== id))
+            }}
+            handleEdit={(record) => {
+                setInitialValues(record)
+                setVisible(true)
+            }}
+        /> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description='暂无发送群对象配置' />}
+        <Button type="dashed" style={{ width: '100%' }} block icon={<PlusOutlined />} onClick={() => setVisible(true)}>新建群配置</Button>
+        {visible && <AddSendObject
+            mpList={mpList}
+            corpList={corpList}
+            visible={visible}
+            initialValues={initialValues}
+            onChange={(values) => {
+                console.log(values)
+                const newValue = [] //JSON.parse(JSON.stringify(value || '[]'))
+                // if (initialValues?.id) {
+                //     newValue = newValue.map(item => {
+                //         if (item.id === initialValues.id)
+                //             return values
+                //         return item
+                //     })
+                // } else {
+                //     newValue.push(values)
+                // }
+                newValue.push({a: 1})
+                console.log(newValue)
+                // onChange?.(newValue)
+                // setVisible(false)
+                // setInitialValues(undefined)
+            }}
+            onClose={() => {
+                setVisible(false)
+                setInitialValues(undefined)
+            }}
+        />}
+    </>;
+};
+
+export default React.memo(SettingsSendObject);

+ 384 - 0
src/pages/weComTask/page/groupChatSend/robot/create/components/Strategy/settingsStrategy.tsx

@@ -0,0 +1,384 @@
+import { App, Button, Card, Form, Input, Modal, Radio, Select } from "antd";
+import React, { useEffect, useRef, useState } from "react";
+import '../../../../../businessPlan/create/global.less'
+import SendTimeSet from "@/pages/weComTask/components/sendTimeSet";
+import { MinusCircleOutlined, PlusOutlined, DeleteOutlined } from "@ant-design/icons";
+import FilterUser from "@/pages/weComTask/components/filterUser";
+import { DefaultOptionType } from "antd/es/select";
+import NewSteps from "@/pages/weComTask/components/newSteps";
+import dayjs from 'dayjs';
+import SelectGroupChat from "@/pages/weComTask/API/SelectGroupChat";
+import SelectCorpUser from "@/pages/weComTask/page/corpUserManage/selectCorpUser";
+
+interface Props extends GROUP_CHAT_CREATE.FoundationProps<any> {
+    mpList: DefaultOptionType[]
+    corpList: DefaultOptionType[]
+}
+/**
+ * 官方 群聊群发策略配置
+ * @param param0 
+ * @returns 
+ */
+const SettingsStrategy: React.FC<Props> = ({ visible, onClose, value, onChange, mpList, corpList }) => {
+
+    /************************************/
+    const { message } = App.useApp()
+    const [form] = Form.useForm();
+    const ref1 = useRef<HTMLDivElement>(null)
+    const strategyList = Form.useWatch('strategyList', form)
+
+    const [stepsList, setStepsList] = useState<any>([
+        { title: '群聊创建配置', description: '任务名称', id: 'basicInfo' },
+        {
+            title: '策略配置',
+            children: [
+                {
+                    title: `策略${1}`, id: 'strategy_1',
+                    description: `名称、执行时间...`,
+                    children: [{
+                        title: `发送群对象${1}`, id: 'strategy_1_sendData_1',
+                        description: `名称、公众号、主体...`,
+                    }]
+                },
+                { title: '完成' }
+            ]
+        },
+        { title: '完成' }
+    ])
+    /************************************/
+
+    useEffect(() => {
+        if (value?.strategyList?.length) {
+            const data = {
+                ...value, strategyList: value?.strategyList?.map(item => {
+                    const { sendTime, startTime, endTime, sendDay, timeRepeatType } = item
+                    if (timeRepeatType === 'TIME_TYPE_SINGLE_PLACE') {
+                        return {
+                            ...item,
+                            timeRepeatType,
+                            sendDay: sendDay ? dayjs(sendDay + ' ' + sendTime) : undefined
+                        }
+                    }
+                    return {
+                        ...item,
+                        timeRepeatType,
+                        sendTime: sendTime ? dayjs('2025-04-25 ' + sendTime) : undefined,
+                        startTime: startTime ? dayjs(startTime) : undefined,
+                        endTime: endTime ? dayjs(endTime) : undefined,
+                        sendDay: sendDay ? dayjs(sendDay) : undefined
+                    }
+                })
+            }
+            filedUpdateChange(data)
+            form.setFieldsValue(data)
+        }
+    }, [value])
+
+    const handleOk = () => {
+        form.validateFields().then((values) => {
+            const data = {
+                ...values,
+                strategyList: values?.strategyList?.map(item => {
+                    const { startTime, endTime, sendDay, sendTime, timeRepeatType, repeatArray, ...rest } = item
+                    const data = { ...rest, timeRepeatType }
+                    if (timeRepeatType === 'TIME_TYPE_SINGLE_PLACE') {
+                        // 定时发送
+                        data.sendDay = dayjs(sendDay).format('YYYY-MM-DD')
+                        data.sendTime = dayjs(sendDay).format('HH:mm:ss')
+                    } else if (timeRepeatType === 'TIME_TYPE_REPEAT_DAY') {
+                        // 每日循环
+                        data.startTime = dayjs(startTime).format('YYYY-MM-DD')
+                        if (endTime) {
+                            data.endTime = dayjs(endTime).format('YYYY-MM-DD')
+                        }
+                        data.sendTime = dayjs(sendTime).format('HH:mm:ss')
+                    } else if (timeRepeatType === 'TIME_TYPE_REPEAT_WEEK' || timeRepeatType === 'TIME_TYPE_REPEAT_MONTH') {
+                        // 每周循环、每月循环
+                        data.startTime = dayjs(startTime).format('YYYY-MM-DD')
+                        data.sendTime = dayjs(sendTime).format('HH:mm:ss')
+                        if (endTime) {
+                            data.endTime = dayjs(endTime).format('YYYY-MM-DD')
+                        }
+                        data.repeatArray = repeatArray
+                    }
+                    return data
+                })
+            }
+            onChange(data)
+        }).catch(() => {
+            form.submit()
+        });
+    };
+
+
+    const filedUpdateChange = ({ groupSendName, strategyList }: any) => {
+        const strategyChildren = strategyList?.map((item, index) => {
+            const { strategyName, timeRepeatType, sendDay, startTime, sendTime, repeatArray, sendData } = item
+            const sendTimeChecked =
+                timeRepeatType === "TIME_TYPE_SINGLE_TIMELY" ||
+                (timeRepeatType === "TIME_TYPE_SINGLE_PLACE" && sendDay) ||
+                (timeRepeatType === "TIME_TYPE_REPEAT_DAY" && startTime && sendTime) ||
+                ((timeRepeatType === "TIME_TYPE_REPEAT_WEEK" || timeRepeatType === "TIME_TYPE_REPEAT_MONTH") && startTime && sendTime && repeatArray)
+
+            const children = sendData?.map((sd, sdIndex) => {
+                const { sendGroupName, mpAccountId, corpIds, robotCorpId, robotCorpUser, chatCorpId, corpGroupChat, externalUserType, externalUserFilter } = sd
+                return {
+                    title: `发送群对象${sdIndex + 1}`, id: `strategy_${index + 1}_sendData_${sdIndex + 1}`,
+                    description: `名称、公众号、主体...`,
+                    checked:
+                        sendGroupName &&
+                        mpAccountId &&
+                        corpIds?.length &&
+                        (externalUserType === 'all' ? true : externalUserFilter && Object.keys(externalUserFilter)?.length > 0) &&
+                        (robotCorpId ? robotCorpUser?.length > 0 : true) &&
+                        chatCorpId && Object.keys(corpGroupChat || {}).length > 0
+                }
+            })
+            return {
+                title: `策略${index + 1}`,
+                description: `名称、执行时间`,
+                id: `strategy_${index + 1}`,
+                children,
+                checked: sendTimeChecked && strategyName && children.some(item => item.checked)
+            }
+        })
+        const stepsData = [
+            { title: '群聊创建配置', description: '任务名称', id: 'basicInfo', checked: groupSendName },
+            {
+                title: '策略配置',
+                children: [
+                    ...strategyChildren,
+                    {
+                        title: '完成', checked: strategyChildren.some(item => item.checked)
+                    }
+                ],
+                checked: strategyChildren.some(item => item.checked),
+            },
+            {
+                title: '完成', checked: strategyChildren.some(item => item.checked)
+            }
+        ]
+        setStepsList(stepsData)
+    }
+
+    return <Modal
+        title={<strong>群聊群发策略配置  &nbsp;&nbsp;&nbsp;<span style={{ color: 'red' }}>对于执行时间冲突的策略,按照策略的排序执行</span></strong>}
+        open={visible}
+        onCancel={onClose}
+        width={850}
+        onOk={handleOk}
+        className={`settingsModal`}
+    >
+        <div className={`body_steps`}>
+            <NewSteps
+                items={stepsList}
+                onChange={(e) => {
+                    if (e?.id)
+                        ref1.current?.querySelector('#' + e?.id)?.scrollIntoView({ behavior: 'smooth' })
+                }}
+            />
+        </div>
+        <div className={`body_content`} ref={ref1}>
+            <Form
+                form={form}
+                name="newRobotSendStrategy"
+                labelAlign='left'
+                labelCol={{ span: 5 }}
+                colon={false}
+                scrollToFirstError={{
+                    behavior: 'smooth',
+                    block: 'center'
+                }}
+                onFinishFailed={({ errorFields }) => {
+                    message.error(errorFields?.[0]?.errors?.[0])
+                }}
+                onFinish={handleOk}
+                initialValues={{
+                    strategyList: [{ id: Date.now(), sendData: [{ externalUserType: 'all', id: Date.now() }] }]
+                }}
+                onFieldsChange={() => {
+                    filedUpdateChange(form.getFieldsValue())
+                }}
+                preserve={true}
+            >
+                <Card title={<strong>基础信息配置</strong>} style={{ background: '#fff', marginBottom: 10 }} id='basicInfo'>
+                    <Form.Item label={<strong>任务名称</strong>} name="groupSendName" rules={[{ required: true, message: '请输入任务名称!' }]}>
+                        <Input placeholder="请输入任务名称" style={{ width: 358 }} allowClear />
+                    </Form.Item>
+                </Card>
+
+                <Form.List name="strategyList">
+                    {(fields, { add, remove }) => (
+                        <>
+                            {fields.map(({ key, name, ...restField }, index) => {
+                                const timeRepeatType = strategyList?.[index]?.timeRepeatType
+                                const sendData = strategyList?.[index]?.sendData
+
+                                return <Card
+                                    key={key}
+                                    title={<strong>策略{index + 1} 配置</strong>}
+                                    style={{ background: '#fff', marginBottom: 10 }}
+                                    extra={strategyList?.length > 1 && <div style={{ color: 'red', cursor: 'pointer' }} onClick={() => remove(name)}>
+                                        <MinusCircleOutlined />
+                                    </div>}
+                                    id={`strategy_${index + 1}`}
+                                >
+                                    <Form.Item
+                                        {...restField}
+                                        name={[name, 'strategyName']}
+                                        label={<strong>策略名称</strong>}
+                                        rules={[{ required: true, message: '请输入策略名称!' }]}
+                                    >
+                                        <Input placeholder='请输入策略名称' allowClear style={{ width: 358 }} />
+                                    </Form.Item>
+                                    <SendTimeSet active='all' form={form} restField={restField} name={name} timeRepeatType={timeRepeatType} />
+                                    <Form.List name={[name, 'sendData']}>
+                                        {(fields, { add, remove }) => (
+                                            <>
+                                                {fields.map(({ key, name, ...restField }, i) => {
+                                                    const chatCorpId = sendData?.[i]?.chatCorpId
+                                                    const robotCorpId = sendData?.[i]?.robotCorpId
+                                                    return <Card
+                                                        key={i}
+                                                        title={<strong>策略{index + 1} 发送群对象{i + 1} 配置</strong>}
+                                                        style={{ background: '#fff', marginBottom: 10 }}
+                                                        extra={sendData?.length > 1 ? <Button icon={<DeleteOutlined />} type='link' style={{ color: 'red' }} onClick={() => remove(name)}></Button> : null}
+                                                        id={`strategy_${index + 1}_sendData_${i + 1}`}
+                                                    >
+                                                        <Form.Item {...restField} label={<strong>发送群对象名称</strong>} name={[name, 'sendGroupName']} rules={[{ required: true, message: '请输入发送群对象名称!' }]}>
+                                                            <Input placeholder="请输入发送群对象名称" style={{ width: 358 }} allowClear />
+                                                        </Form.Item>
+                                                        <Form.Item
+                                                            {...restField}
+                                                            label={<strong>群聊关联公众号</strong>}
+                                                            name={[name, 'mpAccountId']}
+                                                        >
+                                                            <Select
+                                                                showSearch
+                                                                allowClear
+                                                                placeholder="选择公众号"
+                                                                filterOption={(input, option) =>
+                                                                    (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                                }
+                                                                style={{ width: 358 }}
+                                                                options={mpList}
+                                                            />
+                                                        </Form.Item>
+                                                        <Form.Item
+                                                            {...restField}
+                                                            label={<strong>群聊主体</strong>}
+                                                            name={[name, 'corpIds']}
+                                                            rules={[{ required: true, message: '请选择群聊主体!' }]}
+                                                        >
+                                                            <Select
+                                                                showSearch
+                                                                allowClear
+                                                                placeholder="选择群聊主体"
+                                                                filterOption={(input, option) =>
+                                                                    (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                                }
+                                                                mode="multiple"
+                                                                style={{ width: 358 }}
+                                                                options={corpList}
+                                                            />
+                                                        </Form.Item>
+                                                        <Form.Item
+                                                            {...restField}
+                                                            label={<strong>机器人主体</strong>}
+                                                            name={[name, 'robotCorpId']}
+                                                        >
+                                                            <Select
+                                                                showSearch
+                                                                allowClear
+                                                                placeholder="选择机器人主体"
+                                                                filterOption={(input, option) =>
+                                                                    (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                                }
+                                                                style={{ width: 358 }}
+                                                                options={corpList}
+                                                            />
+                                                        </Form.Item>
+                                                        {robotCorpId && <Form.Item
+                                                            {...restField}
+                                                            label={<strong>机器人号</strong>}
+                                                            name={[name, 'robotCorpUser']}
+                                                            rules={[{ required: true, message: '请选择素材群主体!' }]}
+                                                        >
+                                                            <SelectCorpUser placeholder="请选择机器人号" width={358} corpId={robotCorpId} type="radio" />
+                                                        </Form.Item>}
+                                                        <Form.Item
+                                                            {...restField}
+                                                            label={<strong>素材群主体</strong>}
+                                                            name={[name, 'chatCorpId']}
+                                                            rules={[{ required: true, message: '请选择素材群主体!' }]}
+                                                        >
+                                                            <Select
+                                                                showSearch
+                                                                allowClear
+                                                                placeholder="选择素材群主体"
+                                                                filterOption={(input, option) =>
+                                                                    (option?.label as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                                                }
+                                                                style={{ width: 358 }}
+                                                                options={corpList}
+                                                            />
+                                                        </Form.Item>
+                                                        {chatCorpId && <Form.Item
+                                                            {...restField}
+                                                            label={<strong>素材群</strong>}
+                                                            name={[name, 'corpGroupChat']}
+                                                            rules={[{ required: true, message: '请选择素材群!' }]}
+                                                        >
+                                                            <SelectGroupChat corpId={chatCorpId} />
+                                                        </Form.Item>}
+                                                        <Form.Item
+                                                            label={<strong>发送群对象配置</strong>}
+                                                            required
+                                                        >
+                                                            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
+                                                                <Form.Item
+                                                                    {...restField}
+                                                                    name={[name, 'externalUserType']}
+                                                                    rules={[{ required: true, message: '请选择转移对象!' }]}
+                                                                    noStyle
+                                                                >
+                                                                    <Radio.Group options={[{ label: '全部', value: 'all' }, { label: '指定', value: 'specify' }]} />
+                                                                </Form.Item>
+                                                                {sendData?.[i]?.externalUserType === 'specify' && <div style={{ marginTop: 8, width: '100%' }}>
+                                                                    <Form.Item
+                                                                        {...restField}
+                                                                        name={[name, 'externalUserFilter']}
+                                                                        rules={[{ required: true, message: '请选择人群包!' }]}
+                                                                        noStyle
+                                                                    >
+                                                                        <FilterUser configType={'GROUP_GROUP'} />
+                                                                    </Form.Item>
+                                                                </div>}
+                                                            </div>
+                                                        </Form.Item>
+                                                    </Card>
+                                                })}
+                                                <Form.Item>
+                                                    <Button type="dashed" onClick={() => add({ externalUserType: 'all', id: Date.now() })} block icon={<PlusOutlined />}>
+                                                        新增发送群对象
+                                                    </Button>
+                                                </Form.Item>
+                                            </>
+                                        )}
+                                    </Form.List>
+                                </Card>
+                            })}
+                            <Form.Item>
+                                <Button type="primary" onClick={() => add({ id: Date.now(), sendData: [{ externalUserType: 'all', id: Date.now() }] })} block icon={<PlusOutlined />}>
+                                    新增策略
+                                </Button>
+                            </Form.Item>
+                        </>
+                    )}
+                </Form.List>
+            </Form>
+        </div>
+    </Modal>
+}
+
+export default React.memo(SettingsStrategy);

+ 68 - 0
src/pages/weComTask/page/groupChatSend/robot/create/components/content/index.tsx

@@ -0,0 +1,68 @@
+import React, { useContext, useState } from 'react';
+import style from '../../../../../businessPlan/create/index.less'
+import useNewToken from '@/Hook/useNewToken';
+import { DispatchRobotChatCreate } from '../..';
+import { Button, Empty, message, Popconfirm } from 'antd';
+import SettingsContent from './settingsContent';
+import PreviewContent from './previewContent';
+
+const Content: React.FC = () => {
+
+    /*******************************************/
+    const { token } = useNewToken()
+    const { setSettings, settings, onPreviewReset, mpList, corpList } = useContext(DispatchRobotChatCreate)!;
+    const [newVisible, setNewVisible] = useState<boolean>(false);
+    /*******************************************/
+
+    return <>
+        <div className={`${style.settingsBody_content_row}`} style={{ width: '65%' }}>
+            <div className={`${style.settingsBody_content_col}`} style={{ width: '100%' }}>
+                <div className={style.title}>
+                    <span>内容</span>
+                    {/* {settings?.strategyDTO && Object.keys(settings?.strategyDTO).length > 0 && <Popconfirm
+                        title="确定清空?"
+                        onConfirm={() => {
+                            setSettings(undefined)
+                            onPreviewReset();
+                        }}
+                    >
+                        <a style={{ color: 'red' }}>清空</a>
+                    </Popconfirm>} */}
+                </div>
+                <div className={style.detail}>
+                    <div className={style.detail_title}>群发内容配置</div>
+                    <div className={style.detail_body}>
+                        {settings?.strategyDTO && Object.keys(settings?.strategyDTO).length > 0 ? <>
+                            <PreviewContent strategyList={settings?.strategyDTO?.strategyList} />
+                        </> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
+                    </div>
+                </div>
+                <div className={style.detail_footer}>
+                    {settings?.strategyDTO && Object.keys(settings?.strategyDTO).length > 0
+                        ? <Button type="link" style={{ padding: 0, fontSize: 12, color: token.colorPrimary }} size="small" onClick={() => setNewVisible(true)}>编辑</Button>
+                        : <Button type="link" style={{ padding: 0, fontSize: 12, color: token.colorPrimary }} size="small" onClick={() => message.error('请先设置策略')}>编辑</Button>
+                    }
+                </div>
+            </div>
+        </div>
+
+        {/* 群聊群发内容配置 */}
+        {newVisible && <SettingsContent
+            visible={newVisible}
+            value={settings?.strategyDTO}
+            onClose={() => setNewVisible(false)}
+            onChange={(values) => {
+                setSettings({
+                    ...settings,
+                    strategyDTO: {
+                        ...settings.strategyDTO,
+                        ...values
+                    }
+                })
+                setNewVisible(false)
+            }}
+        />}
+    </>;
+};
+
+export default React.memo(Content);

+ 81 - 0
src/pages/weComTask/page/groupChatSend/robot/create/components/content/previewContent.tsx

@@ -0,0 +1,81 @@
+import useNewToken from '@/Hook/useNewToken';
+import LookMsg from '@/pages/weComTask/components/previewMsg/lookMsg';
+import { getMsgData } from '@/pages/weComTask/page/businessPlan/create/components/massSending/showContent';
+import { welcomeContentData } from '@/pages/weComTask/page/businessPlan/create/const';
+import { Empty, Tabs, Typography } from 'antd';
+import React, { useState } from 'react';
+const { Text } = Typography;
+
+interface PreviewContentProps {
+    strategyList: { [x: string]: any }[];
+}
+/**
+ * 预览
+ * @returns 
+ */
+const PreviewContent: React.FC<PreviewContentProps> = ({ strategyList }) => {
+
+    /****************************************/
+    const [activeKey, setActiveKey] = useState<string>('1')
+    /****************************************/
+
+    return <Tabs
+        activeKey={activeKey}
+        tabPosition='left'
+        style={{
+            height: '100%',
+            width: '100%'
+        }}
+        onChange={(key) => { setActiveKey(key) }}
+        items={strategyList.map((item, index) => {
+            return {
+                label: <div style={{ maxWidth: 100 }}><Text>{item.strategyName}</Text></div>,
+                key: `${index + 1}`,
+                children: <div style={{ width: '100%', height: 282, overflow: 'hidden', overflowY: 'auto' }}>
+                    <PreviewSendData sendData={item.sendData} />
+                </div>
+            }
+        })}
+    />;
+};
+
+const PreviewSendData: React.FC<{ sendData: { [x: string]: any }[]; }> = ({ sendData }) => {
+
+    /****************************************/
+    const [activeKey, setActiveKey] = useState<string>('1')
+    const { token } = useNewToken()
+    /****************************************/
+
+    return <Tabs
+        activeKey={activeKey}
+        tabPosition='left'
+        style={{
+            height: '100%',
+            width: '100%'
+        }}
+        onChange={(key) => { setActiveKey(key) }}
+        items={sendData.map((item, index) => {
+            return {
+                label: <div style={{ maxWidth: 150 }}><Text>{item.sendGroupName}</Text></div>,
+                key: `${index + 1}`,
+                children: <div style={{ width: '100%', height: 282, overflow: 'hidden', overflowY: 'auto' }}>
+                    {item?.contentDTO?.length > 0 ? <>
+                        <div style={{ backgroundColor: token.colorPrimary, padding: 4, color: '#FFF', fontSize: 12 }}>
+                            <div>
+                                发送模式:{welcomeContentData?.find(i => i.value === item?.sendMode)?.label}
+                            </div>
+                        </div>
+                        {item?.contentDTO?.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>
+                                <LookMsg data={getMsgData(item, token)} />
+                            </div>
+                        })}
+                    </> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
+                </div>
+            }
+        })}
+    />
+}
+
+export default PreviewContent;

+ 238 - 0
src/pages/weComTask/page/groupChatSend/robot/create/components/content/settingsContent.tsx

@@ -0,0 +1,238 @@
+import { App, Button, Card, Form, Modal, Select } from 'antd';
+import React, { useEffect, useRef, useState } from 'react';
+import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
+import MaterialNoTextMould from '@/pages/weComTask/page/businessPlan/create/components/massSending/materialNoTextMould';
+import { welcomeContentData } from '@/pages/weComTask/page/businessPlan/create/const';
+import NewSteps from '@/pages/weComTask/components/newSteps';
+
+/**
+ * 官方 群聊群发内容配置
+ * @param param0 
+ * @returns 
+ */
+const SettingsContent: React.FC<GROUP_CHAT_CREATE.FoundationProps<any>> = ({ visible, onClose, value, onChange }) => {
+
+    /***********************************************/
+    const { message } = App.useApp()
+    const [form] = Form.useForm();
+    const strategyList = Form.useWatch('strategyList', form)
+    const ref1 = useRef<HTMLDivElement>(null)
+
+    const [stepsList, setStepsList] = useState<any>([
+        {
+            title: `策略${1}`,
+            id: 'strategy_1',
+            children: [{
+                title: `发送群对象${1}`,
+                id: 'strategy_1_sendData_1'
+            }]
+        },
+        { title: '完成' }
+    ])
+    /***********************************************/
+
+    useEffect(() => {
+        if (value) {
+            console.log(value)
+            const valueS = {
+                ...value,
+                strategyList: value?.strategyList?.map(str => {
+                    return {
+                        ...str,
+                        sendData: str?.sendData?.map(item => {
+                            return {
+                                ...item,
+                                contentDTO: item?.contentDTO || [{}]
+                            }
+                        })
+                    }
+                })
+            }
+            filedUpdateChange(valueS)
+            form.setFieldsValue(valueS)
+        }
+    }, [value])
+
+    const handleOk = () => {
+        form.validateFields().then((values) => {
+            onChange(values)
+        }).catch(() => {
+            form.submit()
+        });
+    };
+
+    const filedUpdateChange = ({ strategyList }: any) => {
+        const strategyChildren = strategyList?.map((item, index) => {
+            const { sendData } = item
+
+            const children = sendData?.map((sd, sdIndex) => {
+
+                const content = sd?.contentDTO?.map((i, i_index) => {
+                    return { 
+                        title: `内容${i_index + 1}`, 
+                        checked: (i?.attachmentList?.length || i?.text?.content), 
+                        id: `strategy_${index + 1}_sendData_${sdIndex + 1}_content_${i_index + 1}`
+                    }
+                })
+
+                return {
+                    title: `发送群对象${sdIndex + 1}`, id: `strategy_${index + 1}_sendData_${sdIndex + 1}`, checked: content.every(item => item.checked) && (sd?.sendMode || sd?.sendMode === 0),
+                    children: [
+                        { title: `内容组发送模式`, id: `strategy_${index + 1}_sendData_${sdIndex + 1}_sendMode`, checked: sd?.sendMode || sd?.sendMode === 0 },
+                        ...content
+                    ]
+                }
+            })
+            return {
+                title: `策略${index + 1}`,
+                id: `strategy_${index + 1}`,
+                children,
+                checked: children.every(item => item.checked)
+            }
+        })
+        const stepsData = [
+            ...strategyChildren,
+            {
+                title: '完成', checked: strategyChildren.every(item => item.checked)
+            }
+        ]
+        setStepsList(stepsData)
+    }
+
+    return <Modal
+        title={<strong>群聊群发内容配置</strong>}
+        open={visible}
+        onCancel={onClose}
+        width={850}
+        onOk={handleOk}
+        className={`settingsModal`}
+    >
+        <div className={`body_steps`}>
+            <NewSteps
+                items={stepsList}
+                onChange={(e) => {
+                    if (e?.id)
+                        ref1.current?.querySelector('#' + e?.id)?.scrollIntoView({ behavior: 'smooth' })
+                }}
+            />
+        </div>
+        <div className={`body_content`} ref={ref1}>
+            <Form
+                form={form}
+                name="newRobotSendCnotent"
+                labelAlign='left'
+                labelCol={{ span: 5 }}
+                colon={false}
+                scrollToFirstError={{
+                    behavior: 'smooth',
+                    block: 'center'
+                }}
+                onFinishFailed={({ errorFields }) => {
+                    message.error(errorFields?.[0]?.errors?.[0])
+                }}
+                onFinish={handleOk}
+                onFieldsChange={() => {
+                    filedUpdateChange(form.getFieldsValue())
+                }}
+                preserve={true}
+            >
+                <Form.List name="strategyList">
+                    {(fields) => (
+                        <>
+                            {fields.map(({ key, name, ...restField }, index) => {
+                                const strategyItem = strategyList[index]
+                                return <Card
+                                    key={key}
+                                    title={<strong>策略 {strategyItem.strategyName} 配置</strong>}
+                                    style={{ background: '#fff', marginBottom: 10 }}
+                                    id={`strategy_${index + 1}`}
+                                >
+                                    <Form.List name={[name, 'sendData']} {...restField}>
+                                        {(fields) => (
+                                            <>
+                                                {fields.map(({ key, name, ...restField }, i) => {
+                                                    const sendDataItem = strategyItem?.sendData?.[i];
+
+                                                    return <Card
+                                                        key={i}
+                                                        title={<strong>发送群对象 {sendDataItem.sendGroupName} 配置</strong>}
+                                                        style={{ background: '#fff', marginBottom: 10 }}
+                                                        id={`strategy_${index + 1}_sendData_${i + 1}`}
+                                                    >
+                                                        <div id={`strategy_${index + 1}_sendData_${i + 1}_sendMode`}>
+                                                            <Form.Item
+                                                                {...restField}
+                                                                label={<strong>内容组发送模式</strong>}
+                                                                name={[name, 'sendMode']}
+                                                                rules={[{ required: true, message: '请选择内容组发送模式!' }]}
+                                                            >
+                                                                <Select
+                                                                    showSearch
+                                                                    style={{ width: 358 }}
+                                                                    allowClear
+                                                                    placeholder="请选择内容组发送模式"
+                                                                    filterOption={(input, option) =>
+                                                                        ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+                                                                    }
+                                                                    options={welcomeContentData}
+                                                                />
+                                                            </Form.Item>
+                                                        </div>
+                                                        <div id={'clientId' + '_' + index + '_' + i + 'contentDTO'}>
+                                                            <Form.List {...restField} name={[name, 'contentDTO']} >
+                                                                {(fields, { add, remove }) => {
+                                                                    return <>
+                                                                        {fields.map(({ key, name, ...restField }, c_index) => {
+                                                                            return <div key={c_index} id={`strategy_${index + 1}_sendData_${i + 1}_content_${c_index + 1}`}>
+                                                                                <h3 style={{ display: 'flex', justifyContent: 'space-between', margin: 0 }}>内容{c_index + 1}
+                                                                                    {fields?.length > 1 && <Button
+                                                                                        type="dashed"
+                                                                                        danger
+                                                                                        onClick={() => remove(c_index)}
+                                                                                        icon={<MinusOutlined />}
+                                                                                        size='small'
+                                                                                    >
+                                                                                        移除内容
+                                                                                    </Button>}
+                                                                                </h3>
+                                                                                <Form.Item
+                                                                                    {...restField}
+                                                                                    name={name}
+                                                                                    rules={[{
+                                                                                        validator: (_, value) => {
+                                                                                            if ((value && !(value?.text?.content || value?.attachmentList?.length)) || !value) {
+                                                                                                return Promise.reject('请填写内容或上传附件');
+                                                                                            }
+                                                                                            return Promise.resolve();
+                                                                                        }
+                                                                                    }]}
+                                                                                >
+                                                                                    <MaterialNoTextMould />
+                                                                                </Form.Item>
+                                                                            </div>
+                                                                        })}
+                                                                        <Form.Item noStyle>
+                                                                            <Button type="dashed" onClick={() => add()} style={{ width: '100%' }} icon={<PlusOutlined />}>
+                                                                                新增内容
+                                                                            </Button>
+                                                                        </Form.Item>
+                                                                    </>
+                                                                }}
+                                                            </Form.List>
+                                                        </div>
+                                                    </Card>
+                                                })}
+                                            </>
+                                        )}
+                                    </Form.List>
+                                </Card>
+                            })}
+                        </>
+                    )}
+                </Form.List>
+            </Form>
+        </div>
+    </Modal>;
+};
+
+export default SettingsContent;

+ 39 - 0
src/pages/weComTask/page/groupChatSend/robot/create/const.ts

@@ -0,0 +1,39 @@
+/**
+ * 返回群发原始数据
+ * @param data 
+ */
+export const getRobotSendData = (data: { [x: string]: any }) => {
+    const { groupSendName, strategyList } = data
+    return {
+        groupSendName,
+        strategyList: strategyList.map(item => {
+            const { taskDetail, ...its } = item
+            return {
+                ...its,
+                sendData: taskDetail.map(td => {
+                    const { externalUserFilterName, externalUserFilter, robotCorpUser, ...itsd } = td
+                    const detail: { [x: string]: any } = {
+                        ...itsd,
+                        sendGroupName: externalUserFilterName,
+                        chatCorpId: itsd?.corpGroupChat?.corpId
+                    }
+                    if (externalUserFilter) {
+                        detail.externalUserType = 'specify'
+                        const { configName, ...configContent } = externalUserFilter
+                        detail.externalUserFilter = {
+                            configName,
+                            configContent
+                        }
+                    } else {
+                        detail.externalUserType = 'all'
+                    }
+                    if (robotCorpUser) {
+                        detail.robotCorpId = robotCorpUser.corpId
+                        detail.robotCorpUser = [robotCorpUser]
+                    }
+                    return detail
+                })
+            }
+        })
+    }
+}

+ 505 - 10
src/pages/weComTask/page/groupChatSend/robot/create/index.tsx

@@ -1,16 +1,511 @@
-import React from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
+import style from '../../../businessPlan/create/index.less'
+import { App, Button, Card, Empty, Form, Input, Popconfirm, Select, Space, Spin, Table } from 'antd';
+import { toJS } from 'mobx';
+import { inject, observer } from 'mobx-react';
+import { welcomeMsgJobTypeApi } from '@/pages/weComTask/API/weMaterial/weMaterial';
+import { useAjax } from '@/Hook/useAjax';
+import Strategy from './components/Strategy';
+import { getBindMpListApi } from '@/pages/weComTask/API/corpUserAssign';
+import { DefaultOptionType } from 'antd/es/select';
+import { getCorpAllListApi } from '@/API/global';
+import { SaveOutlined, RedoOutlined, SearchOutlined, PlusOutlined } from '@ant-design/icons';
+import Content from './components/content';
+import { removeEmptyValues } from '@/utils/utils';
+import { PreviewColumns } from './tableConfig';
+import SubmitModal from '../../../businessPlan/create/submitModal';
+import { addTaskApi, getCreateDetailsApi, updateTaskApi } from '@/pages/weComTask/API/businessPlan/create';
+import { useNavigate } from 'react-router-dom';
+import { getRobotSendData } from './const';
 
+
+export const DispatchRobotChatCreate = React.createContext<ROBOT_CHAT_CREATE.DispatchRobotChatCreate | null>(null);
 /**
- * 机器人群发
+ * 官方群发任务创建
  * @returns 
  */
-const RobotCreate: React.FC = () => {
-    return (
-        <div>
-            <h2>创建群聊机器人</h2>
-            {/* 这里可以添加表单或其他内容 */}
-        </div>
-    );
+const OfficialCreate: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookListProps[], bookPlatForm: TASK_CREATE.BookPlatFormProps[] } } }> = ({ weComTaskStore }) => {
+
+    /*******************************************/
+    const navigate = useNavigate();
+    const { message, modal } = App.useApp()
+    const { bookList, bookPlatForm } = toJS(weComTaskStore.data)
+    const [settings, setSettings] = useState<ROBOT_CHAT_CREATE.SettingsProps>();
+    const [msgJobTypeList, setMsgJobTypeList] = useState<{ value: string, label: string }[]>([])
+    const [projectId, setProjectId] = useState<number>()
+    const [previewData, setPreviewData] = useState<any[]>([])
+    const [previewDataOld, setPreviewDataOld] = useState<any[]>([])
+    const [mpList, setMpList] = useState<DefaultOptionType[]>([])
+    const [corpList, setCorpList] = useState<DefaultOptionType[]>([])
+    const [previewContent, setPreviewContent] = useState<{ [x: string]: { [x: string]: string } }>({})
+    const [subVisible, setSubVisible] = useState<boolean>(false) // 选择设置名称弹窗控制
+
+    const welcomeMsgJobType = useAjax(() => welcomeMsgJobTypeApi())//获取业务类型
+    const getBindMpList = useAjax(() => getBindMpListApi())
+    const getCorpAllList = useAjax((param) => getCorpAllListApi(param))
+    const addTask = useAjax((params) => addTaskApi(params))
+    const updateTask = useAjax((params) => updateTaskApi(params))
+    const getCreateDetails = useAjax((params) => getCreateDetailsApi(params))
+    /*******************************************/
+
+    useEffect(() => {
+        const project = sessionStorage.getItem('ROBOT_OFFICIALTASKID')
+        if (project) {
+            const { id, isCopy } = JSON.parse(project)
+            console.log('projectId-->', id, isCopy)
+            if (!isCopy) {
+                setProjectId(id)
+            }
+            getCreateDetails.run(id).then(res => {
+                sessionStorage.removeItem('ROBOT_OFFICIALTASKID')
+                if (res?.data) {
+                    const { bizType, platform, templateProductId, channel, robotGroupChatSendTaskAddDTO } = res.data
+                    let newSettings: ROBOT_CHAT_CREATE.SettingsProps = {
+                        bizType,
+                        platform: Number(platform) as any,
+                        templateProductId,
+                        channel
+                    }
+                    if (robotGroupChatSendTaskAddDTO && Object.keys(robotGroupChatSendTaskAddDTO).length > 0) {
+                        const data = getRobotSendData(robotGroupChatSendTaskAddDTO)
+                        newSettings = { ...newSettings, strategyDTO: data }
+                    }
+                    setSettings(newSettings)
+                }
+            })
+        } else {
+            const task = localStorage.getItem('TASK_ROBOT_CHAT_SEND_CREATE')
+            if (task) {
+                setSettings(JSON.parse(task).settings)
+            }
+        }
+    }, [])
+
+    useEffect(() => {
+        getBindMpList.run().then(res => {
+            if (res?.data)
+                setMpList(res.data.map((item: any) => ({ label: item.name, value: item.id, appId: item.appId })))
+        })
+
+        getCorpAllList.run({}).then(res => {
+            if (res?.data)
+                setCorpList(res.data.map((item: any) => ({ label: item.corpName, value: item.corpId })))
+        })
+    }, [])
+
+    useEffect(() => {
+        welcomeMsgJobType.run().then(res => {
+            if (res?.data) {
+                setMsgJobTypeList(Object.keys(res.data).map(key => ({ value: key, label: res.data[key] })))
+            }
+        })
+    }, [])
+
+    console.log('settings--->', settings)
+
+    // 重置表格
+    const onPreviewReset = () => {
+        setPreviewData([])
+        setPreviewDataOld([])
+    }
+
+    const severBd = () => {
+        localStorage.setItem('TASK_ROBOT_CHAT_SEND_CREATE', JSON.stringify({ settings }))
+        message.success('存储成功')
+    }
+
+    // 预览
+    const preview = () => {
+        const { bizType, platform, templateProductId, channel } = settings
+        if (!(settings?.strategyDTO && Object.keys(settings?.strategyDTO).length > 0)) {
+            message.error('请先配置内容')
+            return
+        }
+        if (!bizType) {
+            message.error('请选择业务类型')
+            return
+        }
+        if (!platform) {
+            message.error('请选择书城')
+            return
+        }
+        const list: any[] = []
+        let id = 1
+        if (!settings?.strategyDTO?.strategyList?.every((str, s_index) => {
+            const { sendData, ...sdto } = str
+            return sendData?.every((sd, f_index) => {
+                const { contentDTO, mpAccountId, corpIds, robotCorpId, robotCorpUser, chatCorpId, corpGroupChat, ...cdto } = sd
+                if (contentDTO?.length) {
+                    return contentDTO?.every((cd, c_index) => {
+                        if (cd?.attachmentList?.length || cd?.text?.content) {
+                            const mediaItem = JSON.parse(JSON.stringify(cd?.attachmentList || []))
+                            if (cd?.text?.content) {
+                                mediaItem.push({
+                                    msgType: 'TASK_CONTENT_TEXT',
+                                    textContent: cd?.text?.content
+                                })
+                            }
+                            const linkData = []
+                            const miniProgramData = []
+                            const contentReactNode = mediaItem.map(item => {
+                                switch (item.msgType) {
+                                    case 'TASK_CONTENT_LINK':
+                                        linkData.push(item)
+                                        return `<span style="color: red">链接</span>`
+                                    case 'TASK_STATUS_MINIPROGRAM':
+                                        miniProgramData.push(item)
+                                        return `<span style="color: red">小程序</span>`
+                                    case 'TASK_STATUS_FILE':
+                                        return `<span>文件</span>`
+                                    case 'TASK_STATUS_VIDEO':
+                                        return `<span>视频</span>`
+                                    case 'TASK_CONTENT_IMAGE':
+                                        return `<span>图片</span>`
+                                    case 'TASK_CONTENT_TEXT':
+                                        return `<span>文本</span>`
+                                    default:
+                                        return `<span style="color: red">请联系管理员</span>`
+                                }
+                            })
+                            const weChat = mpList.find(item => item.value === mpAccountId)
+                            list.push({
+                                id,
+                                bizType,
+                                platform,
+                                templateProductId,
+                                channel,
+                                taskName: settings?.strategyDTO?.taskName,
+                                strategyData: sdto,
+                                mpAccountId,
+                                weChatName: weChat?.label,
+                                mpAccountAppid: weChat?.appId,
+                                corpIds,
+                                corpNames: corpIds.map(corpId => corpList.find(item => item.value === corpId)?.label),
+                                robotCorpId,
+                                robotCorpName: corpList.find(item => item.value === robotCorpId)?.label,
+                                robotCorpUser,
+                                chatCorpId,
+                                chatCorpName: corpList.find(item => item.value === chatCorpId)?.label,
+                                corpGroupChat,
+                                sendData: cdto,
+                                contentReactNode,
+                                content: cd,
+                                strategyIndex: s_index,
+                                sendIndex: f_index,
+                                contentIndex: c_index,
+                                linkData,
+                                miniProgramData
+                            })
+                            id++;
+                            return true
+                        } else {
+                            message.error(`策略:${str?.strategyName},发送对象:${sd?.sendGroupName}请填写发送内容`)
+                            return false
+                        }
+                    })
+                } else {
+                    message.error(`策略:${str?.strategyName},发送对象:${sd?.sendGroupName}请填写发送内容`)
+                    return false
+                }
+
+            })
+        })) {
+            return
+        }
+        console.log('=========================>', list)
+        setPreviewData(list)
+        setPreviewDataOld(list)
+        setPreviewContent({})
+    }
+
+    const tableSearch = useCallback((values) => {
+        console.log(values)
+        const obj = removeEmptyValues(values)
+        if (Object.keys(obj).length) {
+            const newPreviewData = previewDataOld.filter(item => {
+                return (obj?.weChatAppids?.length > 0 ? obj.weChatAppids.includes(item?.mpAccountId) : true)
+                    && (obj?.corpIds?.length > 0 ? obj.corpIds.some(corpId => item?.corpIds.includes(corpId)) : true)
+            })
+            setPreviewData(newPreviewData)
+        } else {
+            setPreviewData(previewDataOld)
+        }
+    }, [previewDataOld, previewData])
+
+    const setTaskName = () => {
+        if (previewDataOld.every((item, index) => {
+            if ((item?.linkData?.length > 0 ? previewContent?.[index + 1]?.linkUrl : true) && (item?.miniProgramData?.length > 0 ? (previewContent?.[index + 1]?.miniprogramAppid && previewContent?.[index + 1]?.miniprogramPage) : true)) {
+                return true
+            } else {
+                message.error('请补充图文或者小程序内容')
+                return false
+            }
+        })) {
+            setSubVisible(true)
+        }
+    }
+
+    const onSubmit = (values: any) => {
+        const { bizType, platform, templateProductId, channel, strategyDTO } = settings
+        const params = {
+            ...values,
+            bizType,
+            platform,
+            templateProductId,
+            channel,
+            robotChatSendTaskAddDTO: {
+                ...strategyDTO,
+                strategyList: strategyDTO?.strategyList?.map(item => {
+                    const { sendData, ...its } = item
+                    return {
+                        ...its,
+                        taskDetail: sendData.map(sd => {
+                            const { externalUserType, externalUserFilter, contentDTO, sendGroupName, chatCorpId, corpGroupChat, robotCorpId, robotCorpUser, ...itsd } = sd
+
+                            const detail: { [x: string]: any } = {
+                                ...itsd,
+                                corpGroupChat,
+                                contentDTO,
+                                externalUserFilterName: sendGroupName,
+                            }
+                            if (robotCorpId) {
+                                detail.robotCorpUser = { corpUserId: robotCorpUser?.[0]?.corpUserId, corpId: robotCorpUser?.[0]?.corpId, corpName: robotCorpUser?.[0]?.corpName, name: robotCorpUser?.[0]?.name }
+                            }
+                            if (externalUserType === 'specify') {
+                                detail.externalUserFilter = {
+                                    configName: externalUserFilter.configName,
+                                    ...externalUserFilter.configContent
+                                }
+                            }
+                            return detail
+                        })
+                    }
+                })
+            }
+        }
+        // console.log('--->', params)
+        if (projectId) {
+            params.projectId = projectId
+            updateTask.run(params).then(res => {
+                console.log(res)
+                if (res?.data) {
+                    message.success('修改提交成功')
+                    setProjectId(undefined)
+                    sessionStorage.setItem('CAMPCORP_OFFICIAL', values?.projectName)
+                    navigate('/weComTask/groupChatSend/robot/taskList')
+                }
+            })
+        } else {
+            addTask.run(params).then(res => {
+                console.log(res)
+                if (res?.data) {
+                    modal.success({
+                        content: '任务提交成功',
+                        styles: { body: { fontWeight: 700 } },
+                        okText: '跳转任务列表',
+                        closable: true,
+                        onOk: () => {
+                            sessionStorage.setItem('CAMPCORP_OFFICIAL', values?.projectName)
+                            navigate('/weComTask/groupChatSend/robot/taskList')
+                        },
+                        onCancel: () => {
+                            setSubVisible(false)
+                        }
+                    })
+                }
+            })
+        }
+    }
+
+    return <div className={style.create}>
+        <Spin spinning={false}>
+            <Card title={<strong>{projectId ? getCreateDetails?.data?.data?.projectName + '任务编辑' : ''}配置区</strong>} className={`${style.card} ${style.config}`}>
+                <Space wrap>
+                    <Space.Compact>
+                        <Button>业务类型</Button>
+                        <Select
+                            showSearch
+                            style={{ width: 120 }}
+                            allowClear
+                            placeholder="请选择类型"
+                            filterOption={(input, option) =>
+                                ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+                            }
+                            value={settings?.bizType}
+                            onChange={(e) => {
+                                setSettings({ ...settings, bizType: e })
+                                onPreviewReset()
+                            }}
+                            options={msgJobTypeList.filter(item => item.value === 'novel')}
+                        />
+                    </Space.Compact>
+
+                    {settings?.bizType === 'novel' ? <>
+                        <Space.Compact>
+                            <Button>书城</Button>
+                            <Select
+                                showSearch
+                                allowClear
+                                placeholder="请选择书城"
+                                style={{ width: 120 }}
+                                filterOption={(input, option) =>
+                                    ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+                                }
+                                value={settings?.platform}
+                                onChange={(e) => {
+                                    setSettings({ ...settings, platform: e })
+                                    onPreviewReset()
+                                }}
+                                options={bookPlatForm.map(item => ({ value: item.id, label: item.platformName }))}
+                            />
+                        </Space.Compact>
+                        <Space.Compact>
+                            <Button>适用产品</Button>
+                            <Select
+                                showSearch
+                                style={{ width: 150 }}
+                                allowClear
+                                placeholder="请选择模板适用产品"
+                                filterOption={(input, option) =>
+                                    ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+                                }
+                                value={settings?.templateProductId}
+                                onChange={(e) => {
+                                    setSettings({ ...settings, templateProductId: e })
+                                    onPreviewReset()
+                                }}
+                                options={bookList.map(item => ({ value: item.id, label: item.bookName }))}
+                            />
+                        </Space.Compact>
+                    </> : settings?.bizType === 'game' ? <Space.Compact>
+                        <Button>游戏渠道</Button>
+                        <Input
+                            style={{ width: 200 }}
+                            allowClear
+                            placeholder="请输入游戏渠道"
+                            value={settings.channel}
+                            onChange={(e) => {
+                                setSettings({ ...settings, channel: e.target.value })
+                                onPreviewReset()
+                            }}
+                        />
+                    </Space.Compact> : undefined}
+                </Space>
+
+                <div className={style.settingsBody}>
+                    <div className={style.settingsBody_content}>
+                        <DispatchRobotChatCreate.Provider
+                            value={{
+                                settings, setSettings,
+                                onPreviewReset,
+                                bookPlatForm, bookList,
+                                mpList,
+                                corpList
+                            }}
+                        >
+                            {/* 策略配置 */}
+                            <Strategy />
+                            {/* 群发内容 */}
+                            <Content />
+                        </DispatchRobotChatCreate.Provider>
+                    </div>
+                </div>
+                <Space className={style.bts} wrap>
+                    <Button icon={<SaveOutlined />} onClick={severBd}>存为预设</Button>
+                    <Popconfirm
+                        title="确定清空?"
+                        onConfirm={() => {
+                            setSettings(undefined)
+                            onPreviewReset()
+                            localStorage.removeItem('TASK_ROBOT_CHAT_SEND_CREATE')
+                        }}
+                    >
+                        <Button icon={<RedoOutlined />} danger>清空配置/预设</Button>
+                    </Popconfirm>
+                    <Button type='primary' onClick={preview}><SearchOutlined />预览任务/配置内容</Button>
+                </Space>
+            </Card>
+        </Spin>
+
+        <Card
+            className={style.card}
+            style={{ marginTop: 10, marginBottom: 10 }}
+            extra={previewDataOld?.length > 0 ? <Button type='primary' icon={<PlusOutlined />} onClick={() => {
+                setTaskName()
+            }}>提交</Button> : undefined}
+        >
+            {previewDataOld?.length > 0 ? <div style={{ minHeight: 300 }}>
+                <Form
+                    layout={'inline'}
+                    onFinish={tableSearch}
+                    colon={false}
+                >
+                    <Form.Item label={<strong>关联公众号</strong>} style={{ marginBottom: 10 }} name="weChatAppids">
+                        <Select
+                            showSearch
+                            style={{ minWidth: 160 }}
+                            maxTagCount={1}
+                            placeholder="公众号"
+                            filterOption={(input, option) =>
+                                ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+                            }
+                            allowClear
+                            mode='multiple'
+                            options={mpList}
+                        />
+                    </Form.Item>
+                    <Form.Item label={<strong>群聊主体</strong>} style={{ marginBottom: 10 }} name="corpIds">
+                        <Select
+                            showSearch
+                            style={{ minWidth: 160 }}
+                            maxTagCount={1}
+                            placeholder="请选择主体"
+                            filterOption={(input, option) =>
+                                ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+                            }
+                            allowClear
+                            options={corpList}
+                            mode='multiple'
+                        />
+                    </Form.Item>
+
+                    <Form.Item style={{ marginBottom: 10 }}>
+                        <Space>
+                            <Button htmlType="reset">重置</Button>
+                            <Button type="primary" htmlType='submit'>搜索</Button>
+                        </Space>
+                    </Form.Item>
+                </Form>
+                <Table
+                    dataSource={previewData}
+                    columns={PreviewColumns(bookPlatForm, bookList, bookPlatForm.find(item => item.id === Number(settings?.platform)).platformKey, setPreviewContent, previewContent)}
+                    rowKey={'id'}
+                    bordered={true}
+                    scroll={{ y: 550 }}
+                    pagination={{
+                        showTotal(total, range) {
+                            return `共 ${total} 条记录 第 ${range[0]}-${range[1]} 条`
+                        },
+                    }}
+                />
+            </div> : <div style={{ minHeight: 400, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
+                <Empty description="请先完成模块配置后,再预览" />
+            </div>}
+        </Card>
+
+        {/* 提交配置 */}
+        {subVisible && <SubmitModal
+            visible={subVisible}
+            loading={addTask.loading || updateTask.loading}
+            projectName={projectId ? getCreateDetails?.data?.data?.projectName : undefined}
+            onChange={(values) => {
+                onSubmit(values)
+            }}
+            onClose={() => {
+                setSubVisible(false)
+            }}
+        />}
+    </div>;
 };
 
-export default RobotCreate;
+export default inject('store')(observer((props: any) => OfficialCreate(props.store)))

+ 227 - 0
src/pages/weComTask/page/groupChatSend/robot/create/tableConfig.tsx

@@ -0,0 +1,227 @@
+import { Input, Typography } from "antd";
+import { AnyObject } from "antd/es/_util/type"
+import { ColumnsType } from "antd/es/table"
+import { TIME_TYPE, welcomeContentData } from "../../../businessPlan/create/const";
+import FilterUser from "@/pages/weComTask/components/filterUser";
+import ShowContentTable from "../../../businessPlan/create/components/massSending/showContentTable";
+import SelectBookLinkButton from "../../../bookLink/SelectBookLinkButton";
+const { Paragraph, Title } = Typography;
+
+
+export const PreviewColumns = (bookPlatForm: TASK_CREATE.BookPlatFormProps[], bookList: TASK_CREATE.BookListProps[], platform: string, setPreviewContent: React.Dispatch<React.SetStateAction<{ [x: string]: { [x: string]: string; }; }>>, previewContent: { [x: string]: { [x: string]: string } }): ColumnsType<AnyObject> => {
+
+    return [
+        {
+            title: '基础信息',
+            dataIndex: 'taskName',
+            key: 'taskName',
+            width: 130,
+            render(value, record) {
+                return <>
+                    <Title level={5} style={{ margin: 0 }}>任务名称:{value}</Title>
+                    {record?.bizType && <Paragraph style={{ margin: 0 }}>业务类型:{record?.bizType === 'novel' ? '小说' : record?.bizType === 'game' ? '游戏' : '<空>'}</Paragraph>}
+                    {record?.platform && <Paragraph style={{ margin: 0 }}>书城:{record?.platform ? bookPlatForm?.find(item => item.id === record?.platform)?.platformName : '<空>'}</Paragraph>}
+                    {record?.templateProductId && <Paragraph style={{ margin: 0 }}>适用产品:{record?.templateProductId ? bookList?.find(item => item.id === record?.templateProductId)?.bookName : '<空>'}</Paragraph>}
+                </>
+            }
+        },
+        {
+            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>}
+                </>
+            }
+        },
+        {
+            title: '群聊关注公众号',
+            dataIndex: 'weChatName',
+            key: 'weChatName',
+            width: 120,
+            ellipsis: true,
+            render(value) {
+                return value || '--'
+            },
+        },
+        {
+            title: '群聊企微主体',
+            dataIndex: 'corpNames',
+            key: 'corpNames',
+            width: 160,
+            ellipsis: true,
+            render(value) {
+                return value ? value.join('、') : '--'
+            },
+        },
+        {
+            title: '机器人',
+            dataIndex: 'robotCorpName',
+            key: 'robotCorpName',
+            width: 160,
+            ellipsis: true,
+            render(value, record) {
+                return value ? <div style={{ width: '100%' }}>
+                    <Title level={5} style={{ margin: 0 }}>主体:{value}</Title>
+                    <Paragraph style={{ margin: 0 }}>机器人号:{record?.robotCorpUser?.map(item => item.name)?.join('、') || '<空>'}</Paragraph>
+                </div> : '--'
+            },
+        },
+        {
+            title: '素材群',
+            dataIndex: 'chatCorpName',
+            key: 'chatCorpName',
+            width: 160,
+            ellipsis: true,
+            render(value, record) {
+                return value ? <div style={{ width: '100%' }}>
+                    <Title ellipsis level={5} style={{ margin: 0 }}>主体:{value}</Title>
+                    <Paragraph style={{ margin: 0 }} ellipsis>素材群:{record?.corpGroupChat?.chatName || record?.corpGroupChat?.chatId}</Paragraph>
+                </div> : '--'
+            },
+        },
+        {
+            title: '发送对象',
+            dataIndex: 'sendData',
+            key: 'sendData',
+            width: 200,
+            render(value, record) {
+                return <>
+                    <Title level={5} style={{ margin: 0 }}>发送对象{record.sendIndex + 1}</Title>
+                    <Paragraph style={{ margin: 0 }} ellipsis>名称:{value?.sendGroupName}</Paragraph>
+                    <Paragraph style={{ margin: 0 }}>内容发送模式:{welcomeContentData?.find(i => i.value === value?.sendMode)?.label}</Paragraph>
+                    <Paragraph style={{ margin: 0 }}>类型:{value?.externalUserType === 'all' ? '全部' : '指定'}</Paragraph>
+                    {value?.externalUserType === 'specify' && <FilterUser isSHow value={value?.externalUserFilter} configType='USER_GROUP' />}
+                </>
+            },
+            onCell: (record) => {
+                return { rowSpan: record.sendDataRowSpan }
+            }
+        },
+        {
+            title: '发送内容',
+            dataIndex: 'content',
+            key: 'content',
+            width: 200,
+            render(value, record) {
+                return <>
+                    <ShowContentTable
+                        data={value}
+                        name={<><span>内容{record.contentIndex + 1}:</span><span dangerouslySetInnerHTML={{ __html: record.contentReactNode?.map((item) => item).join('、') }}></span></>}
+                    />
+                </>
+            },
+        },
+        {
+            title: '图文链接',
+            dataIndex: 'linkUrl',
+            key: 'linkUrl',
+            width: 250,
+            render(_, record) {
+                return record?.linkData?.length > 0 ? <div style={{ display: 'flex', gap: 4, alignItems: 'flex-end' }}>
+                    <Input.TextArea
+                        placeholder="请输入链接"
+                        allowClear
+                        value={previewContent?.[record.id]?.linkUrl}
+                        onChange={e => {
+                            const newPreviewContent = JSON.parse(JSON.stringify(previewContent))
+                            const c = previewContent?.[record.id] || {}
+                            newPreviewContent[record.id] = { ...c, linkUrl: e.target.value }
+                            setPreviewContent(newPreviewContent)
+                        }}
+                    />
+                    <SelectBookLinkButton
+                        bookPlatForm={bookPlatForm}
+                        platform={platform}
+                        mpAccountId={record.mpAccountId}
+                        linkData={record.linkData}
+                        onChange={(linkStr, miniprogramAppid, miniprogramPage) => {
+                            const msg: { [x: string]: string } = { linkUrl: linkStr }
+                            if (miniprogramAppid && miniprogramPage && record?.miniProgramData?.length > 0 && !previewContent?.[record.id]?.miniprogramAppid && !previewContent?.[record.id]?.miniprogramPage) {
+                                msg.miniprogramAppid = miniprogramAppid
+                                msg.miniprogramPage = miniprogramPage
+                            }
+                            const newPreviewContent = JSON.parse(JSON.stringify(previewContent))
+                            const c = previewContent?.[record.id] || {}
+                            newPreviewContent[record.id] = { ...c, ...msg }
+                            setPreviewContent(newPreviewContent)
+                        }}
+                    />
+                </div> : '--'
+            },
+        },
+        {
+            title: '小程序APPID',
+            dataIndex: 'miniprogramAppid',
+            key: 'miniprogramAppid',
+            width: 200,
+            render(_, record) {
+                return record?.miniProgramData?.length > 0 ? <Input.TextArea
+                    placeholder="请输入小程序APPID"
+                    allowClear
+                    value={previewContent?.[record.id]?.miniprogramAppid}
+                    onChange={e => {
+                        const newPreviewContent = JSON.parse(JSON.stringify(previewContent))
+                        const c = previewContent?.[record.id] || {}
+                        newPreviewContent[record.id] = { ...c, miniprogramAppid: e.target.value }
+                        setPreviewContent(newPreviewContent)
+                    }}
+                /> : '--'
+            },
+        },
+        {
+            title: '小程序路径',
+            dataIndex: 'miniprogramPage',
+            key: 'miniprogramPage',
+            width: 250,
+            render(_, record) {
+                return record?.miniProgramData?.length > 0 ? <div style={{ display: 'flex', gap: 4, alignItems: 'flex-end' }}>
+                    <Input.TextArea
+                        placeholder="请输入小程序路径"
+                        allowClear
+                        value={previewContent?.[record.id]?.miniprogramPage}
+                        onChange={e => {
+                            const newPreviewContent = JSON.parse(JSON.stringify(previewContent))
+                            const c = previewContent?.[record.id] || {}
+                            newPreviewContent[record.id] = { ...c, miniprogramPage: e.target.value }
+                            setPreviewContent(newPreviewContent)
+                        }}
+                    />
+                    <SelectBookLinkButton
+                        bookPlatForm={bookPlatForm}
+                        platform={platform}
+                        mpAccountId={record.mpAccountId}
+                        linkData={[{
+                            msgType: "TASK_CONTENT_LINK",
+                            link: {
+                                title: record.miniProgramData?.[0]?.miniprogram?.title,
+                                picUrl: record.miniProgramData?.[0]?.miniprogram?.picUrl,
+                                desc: ''
+                            }
+                        }]}
+                        onChange={(_, miniprogramAppid, miniprogramPage) => {
+                            const msg: { [x: string]: string } = {}
+                            if (miniprogramAppid && miniprogramPage) {
+                                msg.miniprogramAppid = miniprogramAppid
+                                msg.miniprogramPage = miniprogramPage
+                                const newPreviewContent = JSON.parse(JSON.stringify(previewContent))
+                                const c = previewContent?.[record.id] || {}
+                                newPreviewContent[record.id] = { ...c, ...msg }
+                                setPreviewContent(newPreviewContent)
+                            }
+                        }}
+                    />
+                </div> : '--'
+            }
+        }
+    ]
+
+}

+ 51 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupErrorCountList.tsx

@@ -0,0 +1,51 @@
+import { useAjax } from "@/Hook/useAjax"
+import { Popover, Table } from "antd"
+import React from "react"
+import { groupErrorCountTableConfig } from "./groupTableConfig"
+import { getGroupFailUserListApi } from "@/pages/weComTask/API/businessPlan/create"
+
+
+
+
+interface Props {
+    failCount: any,
+    corpId: string,
+    msgId: string,
+    taskId: string
+}
+
+/**
+ * 获取群发记录中失败客户列表
+ * @returns 
+ */
+const GroupErrorCountList: React.FC<Props> = ({ failCount, corpId, msgId, taskId }) => {
+
+    /***********************************/
+    const getFailUserList = useAjax((params) => getGroupFailUserListApi(params))//获取朋友圈任务列表
+    /***********************************/
+
+    const getList = () => {
+        getFailUserList.run({ corpId, msgId, taskId })
+    }
+
+    return <>
+        {failCount ? <Popover
+            trigger={['click']}
+            styles={{ body: { width: 500 } }}
+            content={<Table
+                dataSource={getFailUserList?.data?.data?.map((item: any, index: number) => ({ ...item, id: item?.externalUserId + index }))}
+                rowKey={'id'}
+                scroll={{ y: 200 }}
+                loading={getFailUserList.loading}
+                columns={groupErrorCountTableConfig()}
+            />}
+            onOpenChange={(open) => {
+                if (open) getList()
+            }}
+        >
+            <a>{failCount}</a>
+        </Popover> : '--'}
+    </>
+}
+
+export default React.memo(GroupErrorCountList)

+ 35 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupReSend.tsx

@@ -0,0 +1,35 @@
+import { useAjax } from "@/Hook/useAjax"
+import { reSendGroupTaskApi } from "@/pages/weComTask/API/businessPlan/create"
+import { App, Button } from "antd"
+import React from "react"
+
+
+interface Props {
+    id: number
+    corpUserId?: string
+    onChange?: () => void
+}
+/**
+ * 重新发送
+ * @returns 
+ */
+const GroupReSend: React.FC<Props> = ({ onChange, ...props }) => {
+
+    /*************************/
+    const { message } = App.useApp()
+    const reSendTask = useAjax((params) => reSendGroupTaskApi(params))
+    /*************************/
+
+    const sendHandle = () => {
+        reSendTask.run(props).then(res => {
+            if (res.data) {
+                message.success('发送成功')
+                onChange?.()
+            }
+        })
+    }
+
+    return <Button type="link" loading={reSendTask.loading} onClick={sendHandle} style={{ padding: 0 }}>重新发送</Button>
+}
+
+export default React.memo(GroupReSend)

+ 336 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupTableConfig.tsx

@@ -0,0 +1,336 @@
+import useNewToken from "@/Hook/useNewToken"
+import PreviewMsg from "@/pages/weComTask/components/previewMsg"
+import { emoList } from "@/pages/weComTask/components/textEditor/Expression"
+import { copy } from "@/utils/utils"
+import { Avatar, Badge, Button, Space, Tooltip, Typography } from "antd"
+import { ColumnsType } from "antd/es/table"
+import { InfoCircleOutlined, QuestionCircleOutlined, UserOutlined } from "@ant-design/icons"
+import GroupErrorCountList from "./groupErrorCountList"
+import GroupReSend from "./groupReSend"
+import { GENDER_TYPE } from "@/pages/weComTask/page/businessPlan/create/const"
+
+export function groupMsgCorpUserListConfig(change: (data: any, type: string) => void): ColumnsType<any> {
+
+    let arr: ColumnsType<any> = [
+        {
+            title: '所属企业',
+            dataIndex: 'corpName',
+            key: 'corpName',
+            align: 'center',
+            width: 120,
+        },
+        {
+            title: '企微号',
+            dataIndex: 'userName',
+            key: 'userName',
+            align: 'center',
+            width: 120,
+            ellipsis: true,
+            render: (a: string, b: any) => {
+                return a + `(${b?.userId})`
+            }
+        },
+        {
+            title: '发送时间',
+            dataIndex: 'sendTime',
+            key: 'sendTime',
+            align: 'center',
+            width: 120,
+            ellipsis: true,
+        },
+        {
+            title: '预计发送时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            align: 'center',
+            width: 120,
+            ellipsis: true,
+        },
+        {
+            title: '发送状态',
+            dataIndex: 'status',
+            key: 'status',
+            align: 'center',
+            width: 100,
+            render: (a: number) => {
+                return <span>{{ '0': <Badge status="error" text='未发送' />, '2': <Badge status="success" text='已发送' /> }[a]}</span>
+            }
+        },
+        {
+            title: '发送对象',
+            dataIndex: 'userCount',
+            key: 'userCount',
+            align: 'center',
+            width: 100,
+            render: (a: number) => {
+                return <span>好友{a > 0 ? a : 0}个</span>
+            }
+        },
+        {
+            title: '发送成功',
+            dataIndex: 'sendSuccessCount',
+            key: 'sendSuccessCount',
+            align: 'center',
+            width: 80,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '发送失败',
+            dataIndex: 'sendFailCount',
+            key: 'sendFailCount',
+            align: 'center',
+            width: 80,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '因客户不是好友导致发送失败',
+            dataIndex: 'unFriendCount',
+            key: 'unFriendCount',
+            align: 'center',
+            width: 80,
+            ellipsis: true,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '因客户已经收到其他群发消息导致发送失败',
+            dataIndex: 'othersSendCount',
+            key: 'othersSendCount',
+            align: 'center',
+            width: 80,
+            ellipsis: true,
+            render: (a) => {
+                return a > 0 ? a : 0
+            }
+        },
+        {
+            title: '消息ID',
+            dataIndex: 'msgId',
+            key: 'msgId',
+            align: 'center',
+            width: 100,
+            ellipsis: true,
+            render: (a: string) => {
+                return <a onClick={() => copy(a)}>{a}</a>
+            }
+        },
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            align: 'center',
+            fixed: 'right',
+            width: 120,
+            render: (a: any, b: { status: any, sendCancelRemindStatus: number }) => {
+                return b?.status == '2' ? '--' : <Space wrap>
+                    {b?.sendCancelRemindStatus === 1 ? '已取消发送' : <>
+                        <Button type="link" style={{ padding: 0 }} disabled={b?.status == '2'} onClick={() => { change(b, 'txSend') }} >{'提醒发送'}</Button>
+                        <Button type="link" style={{ padding: 0 }} disabled={b?.status == '2'} onClick={() => { change(b, 'qxSend') }} >{'取消发送'}</Button>
+                    </>}
+                </Space>
+            }
+        },
+    ]
+    return arr
+}
+
+
+export function groupTaskNotesTableConfig(refresh: () => void): ColumnsType<any> {
+    const { token } = useNewToken()
+    // 处理群发内容
+    function groupBody(item) {
+        let msgData = []
+        if (item?.text?.content) {
+            let newValue = item?.text?.content
+            let newEmo = emoList.reduce((prev, cur) => {
+                return [...prev, ...cur]
+            }, [])
+            let emos = newValue.match(/\[[\u4e00-\u9fa5]+\]/g)
+            if (emos && emos?.length) {
+                emos.forEach((emo: any) => {
+                    let emoData = newEmo.find(o => `[${o.name}]` === emo)
+                    if (emoData) {
+                        newValue = newValue.replace(emo, `<img src="${emoData.url}" alt="${emoData.name}" style="width:20px;height:20px"/>`)
+                    }
+                })
+            }
+            if (newValue.includes('\n')) {
+                newValue = newValue.replace('\n', `<br/>`)
+            }
+            let names = newValue.match(/%[\u4e00-\u9fa5]+%/g)
+            if (names && names?.length) {
+                newValue = newValue.replace('%昵称%', ` <span style="display: inline-block;margin:0 1px;position: relative; border: 1px solid ${token.colorBorder}; padding: ${token.paddingXS}px; border-radius: ${token.borderRadius}px;color: ${token.colorTextBase}; background: ${token.colorSuccess};color:${token.colorTextLightSolid}" contenteditable="false">昵称<strong data-name="昵称" style="padding: 0 6px;cursor:pointer" onclick="let html =document.querySelector('[data-name=昵称').parentElement.parentElement.innerHTML;let span = ' '+document.querySelector('[data-name=昵称]').parentElement.outerHTML+' ';console.log('=',html,'=');console.log('=',span,'=');document.execCommand('selectAll');document.execCommand('delete'); document.execCommand('insertHTML', true, html.replace(span,''));">X</strong></span> `)
+            }
+            msgData.push({
+                textContent: newValue,
+                mediaType: 'text'
+            })
+        }
+        if (item?.attachmentList && item?.attachmentList?.length > 0) {
+            let newAttachmentList = item?.attachmentList.map((item: any) => {
+                switch (item.msgType) {
+                    case 'TASK_CONTENT_IMAGE':
+                        return { mediaType: 'image', imageUrl: item?.image?.picUrl }
+                    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_FILE':
+                        return { mediaType: 'file', fileUrl: item?.file?.fileUrl }
+                    case 'TASK_STATUS_VIDEO':
+                        return { mediaType: 'video', videoUrl: item?.video?.videoUrl }
+                    case 'TASK_STATUS_MINIPROGRAM':
+                        return {
+                            mediaType: 'miniprogram',
+                            miniprogramAppid: item.miniprogram?.appId,
+                            miniprogramPage: item?.miniprogram?.page,
+                            miniprogramPicurl: item?.miniprogram?.picUrl,
+                            miniprogramTitle: item?.miniprogram?.title
+                        }
+                }
+            })
+            msgData = msgData.concat(newAttachmentList)
+        }
+        return msgData
+    }
+    return [
+        {
+            title: '任务ID',
+            dataIndex: 'taskId',
+            key: 'taskId',
+            align: 'center',
+            width: 65
+        },
+        {
+            title: '客服号名称',
+            dataIndex: 'corpUserName',
+            key: 'corpUserName',
+            align: 'center',
+            width: 150,
+            ellipsis: true
+        },
+        {
+            title: '消息ID',
+            dataIndex: 'msgId',
+            key: 'msgId',
+            align: 'center',
+            width: 250,
+            ellipsis: true,
+            render: (a: string) => {
+                return <a onClick={() => copy(a)}>{a}</a>
+            }
+        },
+        {
+            title: <div>发送内容<Tooltip title="2025-01-10前的群发无法查看内容"><InfoCircleOutlined /></Tooltip></div>,
+            dataIndex: 'contentDTO',
+            key: 'contentDTO',
+            align: 'center',
+            width: 90,
+            ellipsis: true,
+            render: (a, b) => {
+                let data = []
+                if (a) {
+                    data = groupBody(a)
+                }
+
+                return data?.length > 0 ? <Tooltip color={"#fff"} title={<PreviewMsg type="MSG" minWidth={"200px"} height={'calc(100vh - 400px)'} content={{ data }} />}>
+                    <a>查看内容</a>
+                </Tooltip> : "无内容"
+            }
+        },
+        {
+            title: '发送时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            ellipsis: true,
+            align: 'center',
+            width: 130
+        },
+        {
+            title: <>执行结果 <Tooltip title={<span>此结果为任务在后台创建后是否成功提交到腾讯。腾讯下发至手机和手机的最终执行结果请查看下发企微号。</span>}><QuestionCircleOutlined style={{ color: 'red' }} /></Tooltip></>,
+            dataIndex: 'errCode',
+            key: 'errCode',
+            width: 60,
+            align: 'center',
+            ellipsis: true,
+            render: (a: number) => {
+                return { 0: '成功', 1: '失败' }[a]
+            }
+        },
+        {
+            title: '失败人数',
+            dataIndex: 'failCount',
+            key: 'failCount',
+            align: 'center',
+            ellipsis: true,
+            width: 60,
+            render: (a: any, b: any) => {
+                return <GroupErrorCountList failCount={a} corpId={b?.corpId} msgId={b?.msgId} taskId={b?.taskId} />
+            }
+        },
+        {
+            title: '错误信息',
+            dataIndex: 'errMsg',
+            key: 'errMsg',
+            ellipsis: true,
+            width: 150
+        },
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            width: 70,
+            fixed: 'right',
+            align: 'center',
+            render(_, record) {
+                if (record.errCode === 1) {
+                    return <GroupReSend onChange={refresh} id={record.taskId} corpUserId={record.corpUserId} />
+                }
+                return '--'
+            },
+        },
+    ]
+}
+
+
+
+export function groupErrorCountTableConfig(): ColumnsType<any> {
+    return [
+        {
+            title: '客户名称',
+            dataIndex: 'name',
+            key: 'name',
+            align: 'center',
+            width: 100,
+            render: (a: string, b: any) => {
+                return <Space>
+                    <Avatar shape="square" size="small" icon={<UserOutlined />} src={b?.avatar} />
+                    <Typography.Paragraph ellipsis={{ rows: 1, tooltip: true }} style={{ width: 62, marginBottom: 0 }}>{a}</Typography.Paragraph>
+                </Space>
+            }
+        },
+        {
+            title: '用户ID',
+            dataIndex: 'externalUserId',
+            key: 'externalUserId',
+            align: 'center',
+            ellipsis: true,
+            render: (a: string) => {
+                return <a onClick={() => copy(a)}>{a}</a>
+            }
+        },
+        {
+            title: '性别',
+            dataIndex: 'gender',
+            key: 'gender',
+            align: 'center',
+            width: 80,
+            render: (a: number) => {
+                return <span>{GENDER_TYPE[a]}</span>
+            }
+        },
+    ]
+}

+ 76 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupTaskNotes.tsx

@@ -0,0 +1,76 @@
+import { useAjax } from "@/Hook/useAjax"
+import { useUpdateEffect } from "ahooks"
+import { Button, DatePicker, Space, Table, Tag } from "antd"
+import React, { useEffect, useState } from "react"
+import { SearchOutlined } from '@ant-design/icons';
+import dayjs from "dayjs"
+import { groupTaskNotesTableConfig } from "./groupTableConfig"
+import { getSendGroupTaskGroupMsgListApi, GetSendTaskGroupMsgListProps } from "@/pages/weComTask/API/businessPlan/create"
+import SearchBox from "@/pages/weComTask/components/searchBox";
+
+interface Props {
+    taskId: number
+}
+/**
+ * 群发记录
+ * @returns 
+ */
+const GroupTaskNotes: React.FC<Props> = ({ taskId }) => {
+
+    /*************************************/
+    const [queryForms, setQueryForms] = useState<GetSendTaskGroupMsgListProps>({ pageNum: 1, pageSize: 20, sendTimeStartTime: dayjs().format('YYYY-MM-DD'), sendTimeEndTime: dayjs().format('YYYY-MM-DD'), taskId })
+
+    const getSendTaskMsgList = useAjax((params) => getSendGroupTaskGroupMsgListApi(params))
+    /*************************************/
+
+
+    useEffect(() => {
+        setQueryForms({ ...queryForms, taskId })
+    }, [taskId])
+
+    useUpdateEffect(() => {
+        if (queryForms.taskId) {
+            getSendTaskMsgList.run(queryForms)
+        }
+    }, [queryForms])
+
+    return <Space direction="vertical" style={{ width: '100%' }}>
+        <SearchBox
+            bodyPadding={1}
+            buttons={<Button onClick={() => getSendTaskMsgList.refresh()} loading={getSendTaskMsgList.loading} icon={<SearchOutlined />}>搜索</Button>}
+        >
+            <>
+                <DatePicker placeholder="发送开始时间" value={queryForms?.sendTimeStartTime ? dayjs(queryForms.sendTimeStartTime) : undefined} onChange={(e, date: any) => { setQueryForms({ ...queryForms, sendTimeStartTime: date }) }} />
+                <DatePicker placeholder="发送结束时间" value={queryForms?.sendTimeEndTime ? dayjs(queryForms.sendTimeEndTime) : undefined} onChange={(e, date: any) => { setQueryForms({ ...queryForms, sendTimeEndTime: date }) }} />
+            </>
+        </SearchBox>
+        {/* 表 */}
+        <Table
+            style={{ marginBottom: 1 }}
+            dataSource={getSendTaskMsgList?.data?.data?.records}
+            loading={getSendTaskMsgList?.loading}
+            columns={groupTaskNotesTableConfig(() => getSendTaskMsgList.refresh())}
+            scroll={{ x: 400, y: 320 }}
+            rowKey={(s) => {
+                return s.id
+            }}
+            size='small'
+            rowClassName={(record) => record?.errCode === 1 ? 'errorClassName' : ''}
+            pagination={{
+                total: getSendTaskMsgList?.data?.data?.total,
+                showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+                showSizeChanger: true,
+                showLessItems: true,
+                defaultCurrent: 1,
+                defaultPageSize: 200,//默认初始的每页条数
+                current: getSendTaskMsgList?.data?.data?.current || 1,
+                pageSize: getSendTaskMsgList?.data?.data?.size || 20,
+                onChange: (page, pageSize) => {
+                    setQueryForms({ ...queryForms, pageNum: page, pageSize })
+                }
+            }}
+        />
+    </Space>
+}
+
+export default React.memo(GroupTaskNotes)

+ 143 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/groupXfCorpTabls.tsx

@@ -0,0 +1,143 @@
+import { useAjax } from "@/Hook/useAjax"
+import useNewToken from "@/Hook/useNewToken"
+import { useUpdateEffect } from "ahooks"
+import { App, Badge, Button, DatePicker, Input, Select, Space, Table, Tag } from "antd"
+import React, { useEffect, useState } from "react"
+import { SearchOutlined, SyncOutlined } from '@ant-design/icons';
+import { groupMsgCorpUserListConfig } from "./groupTableConfig"
+import { cancelGroupSendApi, getSendGroupTaskGroupuserListApi, GetSendTaskGroupuserListProps, remindGroupSendApi, syncSendGroupTaskExternalApi, syncSendGroupTaskUserApi } from "@/pages/weComTask/API/businessPlan/create"
+
+
+interface Props {
+    taskId: number
+}
+/**
+ * 群发下发企微号列表
+ * @param param0 
+ * @returns 
+ */
+const GroupXfCorpTabls: React.FC<Props> = ({ taskId }) => {
+
+    /******************************/
+    const { message } = App.useApp()
+    const { token } = useNewToken()
+    const [queryForms, setQueryForms] = useState<GetSendTaskGroupuserListProps>({ pageNum: 1, pageSize: 20, taskId })
+
+    const getSendTaskuserList = useAjax((params) => getSendGroupTaskGroupuserListApi(params))
+    const syncSendTaskUser = useAjax((params) => syncSendGroupTaskUserApi(params))
+    const cancelSend = useAjax((params) => cancelGroupSendApi(params))
+    const remindSend = useAjax((params) => remindGroupSendApi(params))
+    const syncSendTaskExternal = useAjax((params) => syncSendGroupTaskExternalApi(params))
+    /******************************/
+
+    useEffect(() => {
+        setQueryForms({ ...queryForms, taskId })
+    }, [taskId])
+
+    useUpdateEffect(() => {
+        if (queryForms.taskId) {
+            getSendTaskuserList.run(queryForms)
+        }
+    }, [queryForms])
+
+    const syncHandle = () => {
+        if (queryForms.taskId) {
+            syncSendTaskUser.run({ taskId: queryForms.taskId }).then(res => {
+                if (res) {
+                    syncHandleUser()
+                }
+            })
+        } else {
+            message.error('企微ID任务ID空')
+        }
+    }
+
+    // 客户同步
+    const syncHandleUser = () => {
+        if (queryForms.taskId) {
+            syncSendTaskExternal.run({ taskId: queryForms.taskId }).then(res => {
+                if (res) {
+                    message.success('同步成功')
+                    getSendTaskuserList.refresh()
+                }
+            })
+        } else {
+            message.error('企微ID任务ID空')
+        }
+    }
+
+    const changeHandle = (data: any, type: string) => {
+        const hide = message.loading('设置中...', 0)
+        if (type === 'txSend') {
+            remindSend.run({ corpId: data?.corpId, msgId: data?.msgId }).then(res => {
+                hide()
+                message.success('提醒成功')
+                getSendTaskuserList.refresh()
+            }).catch(() => hide())
+        } else if (type === 'qxSend') {
+            cancelSend.run({ corpId: data?.corpId, msgId: data?.msgId }).then(res => {
+                hide()
+                message.success('取消成功')
+                getSendTaskuserList.refresh()
+            }).catch(() => hide())
+        }
+    }
+
+    return <div>
+        <Space style={{ width: '100%' }} wrap>
+            <Input placeholder="消息ID" style={{ width: 120 }} allowClear onChange={(e) => setQueryForms({ ...queryForms, msgId: e.target.value })} />
+            <Select
+                showSearch
+                style={{ minWidth: 100 }}
+                allowClear
+                onChange={(value) => {
+                    setQueryForms({ ...queryForms, status: value })
+                }}
+                value={queryForms?.status}
+                placeholder="发送状态"
+                filterOption={(input, option) =>
+                    ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+                }
+                options={[{ label: <Badge status="error" text='未发送' />, value: 0 }, { label: <Badge status="success" text='已发送' />, value: 2 }]}
+            />
+            <DatePicker placeholder="发送开始时间" onChange={(e, date) => { setQueryForms({ ...queryForms, sendTimeStartTime: date as string }) }} />
+            <DatePicker placeholder="发送结束时间" onChange={(e, date) => { setQueryForms({ ...queryForms, sendTimeEndTime: date as string }) }} />
+            <DatePicker.RangePicker placeholder={['预计送达时间(起始)', '预计送达时间(结束)']} onChange={(e, date) => { setQueryForms({ ...queryForms, createStartDate: date[0], createEndDate: date[1] }) }} />
+            <Button onClick={() => getSendTaskuserList.refresh()} loading={getSendTaskuserList.loading} icon={<SearchOutlined />}>搜索</Button>
+            <Button onClick={() => syncHandle()} loading={syncSendTaskUser.loading || syncSendTaskExternal.loading} type="primary" icon={<SyncOutlined />}>同步</Button>
+        </Space>
+
+        {/* 表 */}
+        <Table
+            style={{ marginTop: 10 }}
+            dataSource={getSendTaskuserList?.data?.data?.records}
+            loading={getSendTaskuserList?.loading}
+            columns={groupMsgCorpUserListConfig(changeHandle)}
+            scroll={{ x: 400, y: 320 }}
+            rowKey={(s) => {
+                return s.id
+            }}
+            size='small'
+            onRow={(row) => {
+                return !row.status ? {
+                    style: { background: token.colorPrimaryBgHover }
+                } : {}
+            }}
+            pagination={{
+                total: getSendTaskuserList?.data?.data?.total,
+                showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+                showSizeChanger: true,
+                showLessItems: true,
+                defaultCurrent: 1,
+                defaultPageSize: 200,//默认初始的每页条数
+                current: getSendTaskuserList?.data?.data?.current || 1,
+                pageSize: getSendTaskuserList?.data?.data?.size || 20,
+                onChange: (page, pageSize) => {
+                    setQueryForms({ ...queryForms, pageNum: page, pageSize })
+                }
+            }}
+        />
+    </div>
+}
+
+export default React.memo(GroupXfCorpTabls)

+ 212 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/index.tsx

@@ -0,0 +1,212 @@
+import { Card, Popover, Table, Tabs, Tag, Typography } from 'antd';
+import React, { useContext, useState } from 'react';
+import style from '../../index.less'
+import { QuestionCircleFilled } from '@ant-design/icons';
+import PreviewTime from '@/pages/weComTask/components/previewTime';
+import FilterUserTooltip from '@/pages/weComTask/components/filterUser/filterUserTooltip';
+import LookMsg from '@/pages/weComTask/components/previewMsg/lookMsg';
+import useNewToken from '@/Hook/useNewToken';
+import GroupXfCorpTabls from './groupXfCorpTabls';
+import GroupTaskNotes from './groupTaskNotes';
+import { DispatchTaskDetails } from '../../log';
+import { STATUS_ZJ, TIME_TYPE_ZJ } from '@/pages/weComTask/page/businessPlan/create/const';
+import { getMsgData } from '@/pages/weComTask/page/businessPlan/create/components/massSending/showContent';
+import ShareTabls from './shareTabls';
+const { Text } = Typography;
+
+
+
+interface Props {
+    robotGroupChatSendTaskVOList: { [x: string]: any }[]
+}
+
+/**
+ * 群发任务详情
+ * @returns 
+ */
+const GroupTask: React.FC<Props> = ({ robotGroupChatSendTaskVOList }) => {
+
+    const { bookPlatForm } = useContext(DispatchTaskDetails)!;
+
+    return <Table
+        dataSource={robotGroupChatSendTaskVOList}
+        columns={[
+            {
+                title: '任务名称',
+                dataIndex: 'taskName',
+                key: 'taskName',
+                width: 100,
+                ellipsis: true,
+                align: 'center'
+            },
+            {
+                title: '状态',
+                dataIndex: 'status',
+                key: 'status',
+                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: 'externalUserFilter',
+                key: 'externalUserFilter',
+                width: 100,
+                align: 'center',
+                render(value) {
+                    return value ? <div className={style.nameBox}>
+                        <div>
+                            <Text ellipsis>指定</Text>
+                        </div>
+                        <Popover
+                            placement="right"
+                            styles={{ body: { maxWidth: 350, maxHeight: 350, overflow: 'hidden', overflowY: 'auto' } }}
+                            mouseEnterDelay={0.5}
+                            content={<FilterUserTooltip
+                                bookCityList={bookPlatForm?.map(item => ({ label: item.platformName, value: item.platformKey }))}
+                                configName={value?.configName}
+                                data={value}
+                            />}
+                        >
+                            <a style={{ color: '#000' }}><QuestionCircleFilled /></a>
+                        </Popover>
+                    </div> : '全部'
+                },
+            },
+            {
+                title: '素材群主体',
+                dataIndex: 'chatCorpName',
+                key: 'chatCorpName',
+                width: 100,
+                ellipsis: true,
+                align: 'center'
+            },
+            {
+                title: '素材群',
+                dataIndex: 'groupChatVO',
+                key: 'groupChatVO',
+                width: 100,
+                ellipsis: true,
+                align: 'center',
+                render: (value, record, index) => {
+                    return value?.chatName || '--'
+                },
+            },
+            {
+                title: '投手',
+                dataIndex: 'putUserName',
+                key: 'putUserName',
+                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: robotGroupChatSendTaskVOList?.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 const ExpandedRow: React.FC<{ record: any }> = ({ record }) => {
+
+    const { contentDTO, id } = record
+    const { token } = useNewToken()
+    const [activeKey, setActiveKey] = useState<string>('2')
+
+    return <div style={{ display: 'flex', gap: 10, maxHeight: 450 }}>
+        <div style={{ width: 320, maxHeight: 450, overflow: 'hidden', overflowY: 'auto' }}>
+            <PreviewTime
+                {...record}
+            />
+            <h2>群发内容</h2>
+            <div style={{ marginTop: 5, backgroundColor: '#d9d9d9', borderRadius: 6, padding: '10px 10px 16px' }}>
+                <LookMsg data={getMsgData(contentDTO?.[0], token)} />
+            </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' && <GroupXfCorpTabls taskId={id} />}
+                        </Card>
+                    },
+                    {
+                        key: '4',
+                        label: '群发记录',
+                        children: <Card styles={{ body: { minHeight: 400 } }}>
+                            {activeKey == '4' && <GroupTaskNotes taskId={id} />}
+                        </Card>
+                    },
+                    {
+                        key: '5',
+                        label: '转发记录',
+                        children: <Card styles={{ body: { minHeight: 400 } }}>
+                            {activeKey == '5' && <ShareTabls taskId={id} />}
+                        </Card>
+                    }
+                ]}
+            />
+        </div>
+    </div>
+}
+
+
+export default GroupTask;

+ 228 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/components/groupTask/shareTabls.tsx

@@ -0,0 +1,228 @@
+import { useAjax } from "@/Hook/useAjax"
+import useNewToken from "@/Hook/useNewToken"
+import { Badge, Button, DatePicker, Input, Popconfirm, Select, Space, Table, Tag } from "antd"
+import React, { useEffect, useState } from "react"
+import { SearchOutlined } from '@ant-design/icons';
+import SearchBox from "@/pages/weComTask/components/searchBox"
+import { getSendGroupTaskRobotListApi, putMessageUpdateReloadApi } from "@/pages/weComTask/API/businessPlan/create"
+import { useUpdateEffect } from "ahooks";
+
+
+interface Props {
+    taskId: number,
+}
+
+function msToHMS(ms) {
+    const totalSeconds = Math.floor(ms / 1000);
+    const hours = Math.floor(totalSeconds / 3600);
+    const minutes = Math.floor((totalSeconds % 3600) / 60);
+    const seconds = totalSeconds % 60;
+
+    return [
+        hours.toString().padStart(2, '0'),
+        minutes.toString().padStart(2, '0'),
+        seconds.toString().padStart(2, '0')
+    ].join(':');
+}
+
+/**
+ * 下发企微号列表
+ * @param param0 
+ * @returns 
+ */
+const ShareTabls: React.FC<Props> = ({ taskId }) => {
+
+    /******************************/
+    const { token } = useNewToken()
+    const [queryForms, setQueryForms] = useState<any>({ pageNum: 1, pageSize: 20, taskId })
+
+    const getMessageSendTaskuserList = useAjax((params) => getSendGroupTaskRobotListApi(params))
+    const putMessageUpdateApi = useAjax((id) => putMessageUpdateReloadApi(id))
+    /******************************/
+
+    useEffect(() => {
+        setQueryForms({ ...queryForms, taskId })
+    }, [taskId])
+
+    useUpdateEffect(() => {
+        if (queryForms.taskId) {
+            getMessageSendTaskuserList.run(queryForms).catch(err => { })
+        }
+    }, [queryForms])
+    const reload = (id) => {
+        console.log(id);
+        putMessageUpdateApi.run(id).then(res => {
+            getMessageSendTaskuserList.refresh()
+        })
+    }
+    return <Space direction="vertical" style={{ width: '100%' }}>
+        <SearchBox
+            bodyPadding={1}
+            buttons={<>
+                <Button onClick={() => getMessageSendTaskuserList.refresh()} loading={getMessageSendTaskuserList.loading} icon={<SearchOutlined />}>搜索</Button>
+            </>}
+        >
+            <>
+                <Input placeholder="消息ID" style={{ width: 120 }} allowClear onChange={(e) => setQueryForms({ ...queryForms, msgId: e.target.value })} />
+                <Select
+                    showSearch
+                    style={{ minWidth: 100 }}
+                    allowClear
+                    onChange={(value) => {
+                        setQueryForms({ ...queryForms, status: value })
+                    }}
+                    value={queryForms?.status}
+                    placeholder="发送状态"
+                    filterOption={(input, option) =>
+                        ((option?.label ?? '') as string).toLowerCase().includes(input.toLowerCase())
+                    }
+                    options={[{ label: <Badge status="error" text='未发送' />, value: 0 }, { label: <Badge status="success" text='已发送' />, value: 2 }]}
+                />
+                <DatePicker placeholder="发送开始时间" onChange={(e, date) => { setQueryForms({ ...queryForms, sendTimeStartTime: date }) }} />
+                <DatePicker placeholder="发送结束时间" onChange={(e, date) => { setQueryForms({ ...queryForms, sendTimeEndTime: date }) }} />
+                <DatePicker.RangePicker placeholder={['预计送达时间(起始)', '预计送达时间(结束)']} onChange={(e, date) => { setQueryForms({ ...queryForms, createStartDate: date[0], createEndDate: date[1] }) }} />
+            </>
+        </SearchBox>
+        {/* 表 */}
+        <Table
+            style={{ marginBottom: 1 }}
+            dataSource={getMessageSendTaskuserList?.data?.data?.records}
+            loading={getMessageSendTaskuserList?.loading}
+            columns={[{
+                title: '所属企业',
+                dataIndex: 'corpName',
+                key: 'corpName',
+                align: 'center',
+                width: 120,
+            },
+            {
+                title: '企微号',
+                dataIndex: 'userName',
+                key: 'userName',
+                align: 'center',
+                width: 120,
+                ellipsis: true,
+                render: (a: string, b: any) => {
+                    return b?.userId
+                }
+            },
+            {
+                title: '发送开始时间',
+                dataIndex: 'createTime',
+                key: 'createTime',
+                align: 'center',
+                width: 140,
+                ellipsis: true,
+            },
+            {
+                title: '发送结束时间',
+                dataIndex: 'sendTime',
+                key: 'sendTime',
+                align: 'center',
+                width: 140,
+                ellipsis: true,
+            },
+            {
+                title: '任务耗时',
+                dataIndex: 'timeClac',
+                key: 'timeClac',
+                align: 'center',
+                width: 100,
+                ellipsis: true,
+                render: (_: any, b: any) => {
+                    let hm, pj
+                    if (b?.sendTime && b?.createTime) {
+                        hm = new Date(b?.sendTime).getTime() - new Date(b?.createTime).getTime()
+                        pj = hm / b?.userCount / 1000
+                    }
+
+                    return hm ? <>{msToHMS(hm)}<br />{pj?.toFixed(1)}秒/群</> : "--"
+                }
+            },
+            {
+                title: '发送状态',
+                dataIndex: 'status',
+                key: 'status',
+                align: 'center',
+                width: 100,
+                render: (a: number) => {
+                    return <span>{{ '1': <Badge status="error" text='未发送' />, '2': <Badge status="success" text='已发送' /> }[a]}</span>
+                }
+            },
+            {
+                title: '发送数',
+                dataIndex: 'userCount',
+                key: 'userCount',
+                align: 'center',
+                width: 100,
+                render: (a: number, b: any) => {
+                    return b?.status == "1" ? 0 : a
+                }
+            },
+            {
+                title: '失败用户/群',
+                dataIndex: 'errChatNameList',
+                key: 'errChatNameList',
+                align: 'center',
+                width: 200,
+                ellipsis: true,
+                render: (a: any) => {
+                    return a
+                }
+            },
+            {
+                title: '失败原因',
+                dataIndex: "errorMsg",
+                align: 'center',
+                width: 200,
+                ellipsis: true,
+                render: (a: string, b: any) => {
+                    return b?.status == "1" ? a : ""
+                }
+            },
+            {
+                title: '操作',
+                dataIndex: 'action',
+                key: 'action',
+                align: 'center',
+                width: 50,
+                render: (a: any, b: any) => {
+                    return <Space>
+                        {
+                            b?.status == "1" ? <Popconfirm title='确定重新发送?' onConfirm={() => {
+                                reload(b?.id)
+                            }}>
+                                <Button type="link" danger style={{ padding: 0 }} >{'重新发送'}</Button>
+                            </Popconfirm> : <></>
+                        }
+                    </Space>
+                }
+            }]}
+            scroll={{ x: 600, y: 350 }}
+            rowKey={(s) => {
+                return s.id
+            }}
+            size='small'
+            onRow={(row) => {
+                return !row.status ? {
+                    style: { background: token.colorPrimaryBgHover }
+                } : {}
+            }}
+            pagination={{
+                total: getMessageSendTaskuserList?.data?.data?.total,
+                showTotal: (total) => <Tag color="cyan">总共{total}数据</Tag>,
+                showSizeChanger: true,
+                showLessItems: true,
+                defaultCurrent: 1,
+                defaultPageSize: 200,//默认初始的每页条数
+                current: getMessageSendTaskuserList?.data?.data?.current || 1,
+                pageSize: getMessageSendTaskuserList?.data?.data?.size || 20,
+                onChange: (page, pageSize) => {
+                    setQueryForms({ ...queryForms, pageNum: page, pageSize })
+                }
+            }}
+        />
+    </Space>
+}
+
+export default React.memo(ShareTabls)

+ 15 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/index.less

@@ -0,0 +1,15 @@
+.nameBox {
+    display: flex;
+    align-items: center;
+
+    >div {
+        flex: 1 0;
+        overflow: hidden;
+    }
+
+    >a {
+        margin: 0 3px;
+        color: black;
+        cursor: help;
+    }
+}

+ 216 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/index.tsx

@@ -0,0 +1,216 @@
+import { useAjax } from '@/Hook/useAjax';
+import { cancelProjectApi, delProjectApi, getProjectListApi } from '@/pages/weComTask/API/businessPlan/create';
+import { App, Button, Card, DatePicker, Input, Popconfirm, Space, Table } from 'antd';
+import React, { useEffect, useState } from 'react';
+import taskListColumns from './tableConfig';
+import dayjs from 'dayjs';
+import { SearchOutlined, DeleteOutlined, PauseCircleOutlined } from '@ant-design/icons';
+import Log from './log';
+import { inject, observer } from 'mobx-react';
+import { toJS } from 'mobx';
+import { getBindMpListApi } from '@/pages/weComTask/API/corpUserAssign';
+import { getCorpAllListApi } from '@/API/global';
+import { DefaultOptionType } from 'antd/es/select';
+
+/**
+ * 任务列表
+ * @returns 
+ */
+const TaskList: React.FC<{ weComTaskStore: { data: { bookList: TASK_CREATE.BookListProps[], bookPlatForm: TASK_CREATE.BookPlatFormProps[] } } }> = ({ weComTaskStore }) => {
+
+    /***********************************************/
+    const projectName = sessionStorage.getItem('CAMPCORP_OFFICIAL')
+    const { message } = App.useApp();
+    const { bookList, bookPlatForm } = toJS(weComTaskStore.data)
+    const [queryForm, setQueryForm] = useState<BUSINES_SPLAN_API.GetProjectListProps>({ pageNum: 1, pageSize: 20, projectName, projectType: 2 })
+    const [queryFormNew, setQueryFormNew] = useState<BUSINES_SPLAN_API.GetProjectListProps>({ pageNum: 1, pageSize: 20, projectName, projectType: 2 })
+    const [logOpenData, setLogOpenData] = useState<{ visible: boolean, data: any }>({ visible: false, data: {} })
+    const [selectedRows, setselectedRows] = useState<any[]>([])
+    const [mpList, setMpList] = useState<DefaultOptionType[]>([])
+    const [corpList, setCorpList] = useState<DefaultOptionType[]>([])
+
+    const getProjectList = useAjax((params) => getProjectListApi(params))
+    const delProject = useAjax((params) => delProjectApi(params))
+    const cancelProject = useAjax((params) => cancelProjectApi(params))
+    const getBindMpList = useAjax(() => getBindMpListApi())
+    const getCorpAllList = useAjax((param) => getCorpAllListApi(param))
+    /***********************************************/
+
+    useEffect(() => {
+        getBindMpList.run().then(res => {
+            if (res?.data)
+                setMpList(res.data.map((item: any) => ({ label: item.name, value: item.id, appId: item.appId })))
+        })
+        getCorpAllList.run({}).then(res => {
+            if (res?.data)
+                setCorpList(res.data.map((item: any) => ({ label: item.corpName, value: item.corpId })))
+        })
+    }, [])
+
+    useEffect(() => {
+        const projectName = sessionStorage.getItem('CAMPCORP_OFFICIAL')
+        if (projectName) {
+            sessionStorage.removeItem('CAMPCORP_OFFICIAL')
+        }
+    }, [])
+
+    useEffect(() => {
+        getProjectList.run(queryFormNew)
+    }, [queryFormNew])
+
+    // 日志
+    const handleLog = (data: any) => {
+        setLogOpenData({ visible: true, data })
+    }
+
+    // 复制
+    const handleCopy = (data: any, isCopy: boolean) => {
+        sessionStorage.setItem('ROBOT_OFFICIALTASKID', JSON.stringify({ id: data.id, isCopy }))
+        sessionStorage.setItem('oldPath', '/weComTask/groupChatSend/robot/create')
+        window.location.href = '/weComTask#/weComTask/groupChatSend/robot/create'
+    }
+
+    // 删除
+    const handleDel = (data: { projectIds: number[] }, type: 'del' | 'cancel') => {
+        const hide = message.loading(type === 'del' ? '正在删除...' : '正在取消...', 0)
+        switch (type) {
+            case 'del':
+                delProject.run(data).then(res => {
+                    hide()
+                    setselectedRows([])
+                    if (res?.data) {
+                        message.success('删除成功')
+                        getProjectList.refresh()
+                    } else {
+                        message.error('删除失败')
+                    }
+                }).catch(() => hide())
+                break
+            case 'cancel':
+                cancelProject.run(data).then(res => {
+                    hide()
+                    setselectedRows([])
+                    if (res?.data) {
+                        message.success('取消成功')
+                        getProjectList.refresh()
+                    } else {
+                        message.error('取消失败')
+                    }
+                }).catch(() => hide())
+                break
+        }
+
+    }
+
+    return <Card>
+        <div style={{ display: 'flex', gap: 10, marginBottom: 10 }}>
+            <Space.Compact>
+                <Button>任务名称</Button>
+                <Input placeholder='请输入任务名称' allowClear value={queryForm?.projectName} onChange={(e) => setQueryForm({ ...queryForm, projectName: e.target.value })} />
+            </Space.Compact>
+            <Space.Compact>
+                <Button>创建时间</Button>
+                <DatePicker.RangePicker
+                    placeholder={["创建时间开始", "创建时间结束"]}
+                    value={queryForm?.createTimeMin ? [dayjs(queryForm?.createTimeMin), dayjs(queryForm?.createTimeMax)] : undefined}
+                    onChange={(_, options) => {
+                        const newQueryForm = { ...queryForm }
+                        if (options?.[0]) {
+                            newQueryForm.createTimeMin = options?.[0] + ' 00:00:00'
+                            newQueryForm.createTimeMax = options?.[1] + ' 23:59:59'
+                        } else {
+                            delete newQueryForm?.createTimeMin
+                            delete newQueryForm?.createTimeMax
+                        }
+                        setQueryForm(newQueryForm)
+                    }}
+                />
+            </Space.Compact>
+            <Button
+                type='primary'
+                onClick={() => {
+                    setQueryFormNew({ ...queryForm, pageNum: 1 })
+                }}
+                icon={<SearchOutlined />}
+            >搜索</Button>
+            <Popconfirm
+                title="确定删除?"
+                onConfirm={() => { handleDel({ projectIds: selectedRows.map(i => i.id) }, 'del') }}
+                disabled={selectedRows.length === 0}
+            >
+                <Button type='primary' danger icon={<DeleteOutlined />} loading={delProject.loading} disabled={selectedRows.length === 0}>删除</Button>
+            </Popconfirm>
+            <Popconfirm
+                title="确定取消?"
+                onConfirm={() => { handleDel({ projectIds: selectedRows.map(i => i.id) }, 'cancel') }}
+                disabled={selectedRows.length === 0}
+            >
+                <Button type='primary' style={{ backgroundColor: 'orange', borderColor: 'orange' }} loading={cancelProject.loading} icon={<PauseCircleOutlined />} disabled={selectedRows.length === 0}>取消任务</Button>
+            </Popconfirm>
+        </div>
+        <Table
+            dataSource={getProjectList?.data?.data?.records}
+            columns={taskListColumns(mpList, corpList, handleLog, handleCopy, handleDel)}
+            rowKey={'id'}
+            bordered={true}
+            size='small'
+            scroll={{ y: 600, x: 1200 }}
+            pagination={{
+                current: getProjectList?.data?.data?.current,
+                pageSize: getProjectList?.data?.data?.size,
+                total: getProjectList?.data?.data?.total,
+                showSizeChanger: true,
+                onChange: (page, pageSize) => {
+                    setQueryFormNew({ ...queryFormNew, pageNum: page, pageSize })
+                    setQueryForm({ ...queryForm, pageNum: page, pageSize })
+                }
+            }}
+            rowSelection={{
+                selectedRowKeys: selectedRows?.map((item: any) => item?.id),
+                onSelect: (record: { id: string }, selected: boolean) => {
+                    let newData = JSON.parse(JSON.stringify(selectedRows))
+                    if (selected) {
+                        newData.push({ ...record })
+                    } else {
+                        newData = newData.filter((item: { id: string }) => item.id !== record.id)
+                    }
+                    setselectedRows(newData)
+                },
+                onSelectAll: (selected: boolean, _: { id: string }[], changeRows: { id: string }[]) => {
+                    let newData = JSON.parse(JSON.stringify(selectedRows || '[]'))
+                    if (selected) {
+                        changeRows.forEach((item: { id: string }) => {
+                            let index = newData.findIndex((ite: { id: string }) => ite.id === item.id)
+                            if (index === -1) {
+                                newData.push(item)
+                            }
+                        })
+                    } else {
+                        let newSelectAccData = newData.filter((item: { id: string }) => {
+                            let index = changeRows.findIndex((ite: { id: string }) => ite.id === item.id)
+                            if (index !== -1) {
+                                return false
+                            } else {
+                                return true
+                            }
+                        })
+                        newData = newSelectAccData
+                    }
+                    setselectedRows(newData)
+                }
+            }}
+        />
+
+        {/* 日志 */}
+        {logOpenData.visible && <Log
+            {...logOpenData}
+            bookList={bookList}
+            bookPlatForm={bookPlatForm}
+            onClose={() => {
+                setLogOpenData({ visible: false, data: undefined })
+            }}
+        />}
+    </Card>
+};
+
+export default inject('store')(observer((props: any) => TaskList(props.store)));

+ 55 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/log.tsx

@@ -0,0 +1,55 @@
+import { useAjax } from '@/Hook/useAjax';
+import { getProjectLogListApi } from '@/pages/weComTask/API/businessPlan/create';
+import { Drawer, Spin } from 'antd';
+import React, { useEffect, useState } from 'react';
+import GroupTask from './components/groupTask';
+
+
+interface Props {
+    data: any,
+    bookPlatForm: TASK_CREATE.BookPlatFormProps[]
+    bookList: TASK_CREATE.BookListProps[]
+    visible?: boolean,
+    onClose?: () => void
+}
+
+export const DispatchTaskDetails = React.createContext<TASK_CREATE.DispatchTaskDetails | null>(null);
+/**
+ * 任务日志
+ * @param param0 
+ * @returns 
+ */
+const Log: React.FC<Props> = ({ data, visible, onClose, bookPlatForm, bookList }) => {
+
+    /******************************************************/
+    const [previewData, setPreviewData] = useState<{ robotGroupChatSendTaskVOList?: any[] }>({})
+    const getProjectLogList = useAjax((params) => getProjectLogListApi(params))
+    /******************************************************/
+
+    useEffect(() => {
+        getProjectLogList.run(data.id).then(res => {
+            if (res?.data) {
+                setPreviewData(res.data)
+            }
+        })
+    }, [])
+
+    return <Drawer
+        title={<strong>{data.projectName} 任务详情</strong>}
+        onClose={onClose}
+        open={visible}
+        width={1400}
+        styles={{ body: { paddingTop: 5 } }}
+    >
+
+        <Spin spinning={getProjectLogList.loading}>
+            <DispatchTaskDetails.Provider
+                value={{ bookPlatForm, bookList }}
+            >
+                <GroupTask robotGroupChatSendTaskVOList={previewData?.robotGroupChatSendTaskVOList} />
+            </DispatchTaskDetails.Provider>
+        </Spin>
+    </Drawer>
+};
+
+export default Log;

+ 123 - 0
src/pages/weComTask/page/groupChatSend/robot/taskList/tableConfig.tsx

@@ -0,0 +1,123 @@
+import { Flex, Popconfirm, Popover, Space, Typography } from "antd";
+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 { getRobotSendData } from "../create/const";
+import PreviewStrategy from "../create/components/Strategy/previewStrategy";
+import { DefaultOptionType } from "antd/es/select";
+import PreviewContent from "../create/components/content/previewContent";
+
+const { Text, Paragraph } = Typography;
+const taskListColumns = (
+    mpList: DefaultOptionType[],
+    corpList: DefaultOptionType[],
+    handleLog: (data: any) => void,
+    handleCopy: (data: any, isCopy: boolean) => void,
+    handleDel: (data: any, type: 'del' | 'cancel') => void,
+): ColumnsType<AnyObject> => {
+
+    return [
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            width: 160,
+            render(_, record) {
+                return <Space>
+                    <Popconfirm
+                        title="确定取消?"
+                        onConfirm={() => { handleDel({ projectIds: [record.id] }, 'cancel') }}
+                    >
+                        <a style={{ color: 'orange' }}>取消任务</a>
+                    </Popconfirm>
+                    <a onClick={() => handleCopy(record, true)}>复制</a>
+                    <a onClick={() => handleCopy(record, false)}>编辑</a>
+                    <a onClick={() => handleLog(record)}>详情</a>
+                    <Popconfirm
+                        title="确定删除?"
+                        onConfirm={() => { handleDel({ projectIds: [record.id] }, 'del') }}
+                    >
+                        <a style={{ color: 'red' }}>删除</a>
+                    </Popconfirm>
+                </Space>
+            },
+        },
+        {
+            title: '任务名称',
+            dataIndex: 'projectName',
+            key: 'projectName',
+            width: 120,
+            ellipsis: true
+        },
+        {
+            title: '基础信息',
+            dataIndex: 'bizType',
+            key: 'bizType',
+            width: 180,
+            render: (_, record) => {
+                return <Paragraph style={{ margin: 0 }} ellipsis={{ tooltip: true }}>
+                    业务类型:{record?.bizType === 'novel' ? '小说' : '<空>'}-书城:{record?.platformName || '<空>'}-适用产品:{record?.templateProductName || '<空>'}
+                </Paragraph>
+            }
+        },
+        {
+            title: '机器人群发配置',
+            dataIndex: 'robotGroupChatSendTaskAddDTO',
+            key: 'robotGroupChatSendTaskAddDTO',
+            width: 170,
+            ellipsis: true,
+            render: (value) => {
+                if (value && Object.keys(value)?.length > 0) {
+                    const data = getRobotSendData(value)
+                    return <div className={style.nameBox}>
+                        <div>
+                            <Text ellipsis>{value?.groupSendName || '<空>'}</Text>
+                        </div>
+                        <Popover
+                            placement="left"
+                            content={<div>
+                                <PreviewStrategy strategyDTO={data} mpList={mpList} corpList={corpList} />
+                            </div>}
+                            styles={{ body: { width: 500, overflow: 'hidden', overflowY: 'auto', maxHeight: 400 } }}
+                        >
+                            <a><QuestionCircleFilled /></a>
+                        </Popover>
+                        <Popover
+                            placement="left"
+                            content={<div>
+                                <PreviewContent strategyList={data?.strategyList} />
+                            </div>}
+                            styles={{ body: { width: 500, overflow: 'hidden', overflowY: 'auto', maxHeight: 400 } }}
+                        >
+                            <a><QuestionCircleFilled /></a>
+                        </Popover>
+                    </div>
+                }
+                return <Text type="danger">当前没有群发配置</Text>
+            }
+        },
+        {
+            title: '创建时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            align: 'center',
+            width: 125,
+            ellipsis: true
+        },
+        {
+            title: '任务数量',
+            dataIndex: 'taskCount',
+            key: 'taskCount',
+            width: 260,
+            render(_, record) {
+                return <Flex gap={6}>
+                    <Text type={record.groupSendTaskCount > 0 ? 'success' : 'danger'}>机器人群发:{record.groupSendTaskCount}</Text>
+                </Flex>
+            },
+        },
+    ]
+}
+
+
+export default taskListColumns

+ 41 - 0
src/pages/weComTask/page/groupChatSend/robot/typings.d.ts

@@ -0,0 +1,41 @@
+declare namespace ROBOT_CHAT_CREATE {
+    interface DispatchRobotChatCreate {
+        settings: SettingsProps
+        setSettings: React.Dispatch<React.SetStateAction<SettingsProps>>
+        onPreviewReset: () => void
+        bookPlatForm: TASK_CREATE.BookPlatFormProps[]
+        bookList: TASK_CREATE.BookListProps[]
+        mpList: DefaultOptionType[]
+        corpList: DefaultOptionType[]
+    }
+    // 官方群发创建参数配置
+    interface SettingsProps {
+        bizType?: string; // 业务类型
+        platform?: string; // 书城
+        channel?: string;   // 渠道
+        templateProductId?: string; // 适用产品
+        strategyDTO?: { [x: string]: any };
+    }
+    interface SettingsSendObjectProps {
+        mpList: DefaultOptionType[]
+        corpList: DefaultOptionType[]
+        value?: { [x: string]: any }[];
+        onChange?: (value: { [x: string]: any }[]) => void;
+    }
+    interface AddSendObjectProps {
+        mpList: DefaultOptionType[]
+        corpList: DefaultOptionType[]
+        initialValues?: any;
+        onChange?: (value: { [x: string]: any }) => void;
+        visible?: boolean;
+        onClose?: () => void;
+    }
+    interface ShowSendObjectTable {
+        mpList: DefaultOptionType[]
+        corpList: DefaultOptionType[]
+        data: { [x: string]: any }[]
+        isPreview?: boolean
+        handleEdit?: (data: { [x: string]: any }) => void
+        handleDelete?: (id: number) => void
+    }
+}

+ 14 - 2
src/pages/weComTask/page/groupLeaderManage/bindDetails.tsx

@@ -1,8 +1,8 @@
 import React, { useEffect, useState } from 'react';
-import { getBindDetailListApi, unbindCorpUserChatApi } from '../../API/groupLeaderManage';
+import { getBindDetailListApi, syncCorpChatRobotApi, unbindCorpUserChatApi } from '../../API/groupLeaderManage';
 import { useAjax } from '@/Hook/useAjax';
 import { App, Button, Flex, Modal, Popconfirm, Table, Typography } from 'antd';
-import { ForkOutlined, PullRequestOutlined } from '@ant-design/icons';
+import { ForkOutlined, PullRequestOutlined, RetweetOutlined } from '@ant-design/icons';
 import BindUser from './bindUser';
 const { Paragraph } = Typography;
 
@@ -50,6 +50,7 @@ const BindDetailsModal: React.FC<BindDetailsModalProps> = React.memo(({ visible,
 
     const getBindDetailList = useAjax((params) => getBindDetailListApi(params))
     const unbindCorpUserChat = useAjax((params) => unbindCorpUserChatApi(params))
+    const syncCorpChatRobot = useAjax((params) => syncCorpChatRobotApi(params))
     /*********************************/
 
     useEffect(() => {
@@ -76,6 +77,16 @@ const BindDetailsModal: React.FC<BindDetailsModalProps> = React.memo(({ visible,
         })
     }
 
+    // 同步
+    const syncRobot = () => {
+        syncCorpChatRobot.run({ corpId: data.corpId, corpUserId: data.corpUserId, robotIdList: editSelectedRow.map(item => item?.transferExternalUser?.externalUserId) }).then((res) => {
+            if (res?.data) {
+                message.success('同步成功')
+                setEditSelectedRow([])
+            }
+        })
+    }
+
     return <Modal
         title={<strong>《{data.corpName}》群主《{data.name}》{bindType === 1 ? '客服号' : '机器人号'}详情</strong>}
         open={visible}
@@ -86,6 +97,7 @@ const BindDetailsModal: React.FC<BindDetailsModalProps> = React.memo(({ visible,
         <Flex style={{ marginBottom: 10 }} gap={10}>
             <Button loading={getBindDetailList.loading} onClick={() => getBindDetailList.refresh()}>刷新</Button>
             <Button type="primary" icon={<ForkOutlined />} onClick={() => setBindVisible(true)}>绑定{bindType === 1 ? '客服号' : '机器人号'}</Button>
+            {bindType === 2 && <Button type="primary" disabled={editSelectedRow?.length === 0} icon={<RetweetOutlined />} onClick={() => syncRobot()}>同步机器人号</Button>}
             <Popconfirm
                 title="确定解绑?"
                 onConfirm={handleUnbind}

+ 56 - 3
src/pages/weComTask/page/groupLeaderManage/index.tsx

@@ -1,12 +1,12 @@
-import { Button, Card, Input, Pagination, Select, Table } from 'antd';
+import { App, Button, Card, Input, Pagination, Popconfirm, Select, Table } from 'antd';
 import React, { useEffect, useRef, useState } from 'react';
 import style from '../bookLink/index.less'
 import { useSize } from 'ahooks';
 import { useAjax } from '@/Hook/useAjax';
-import { getCorpUserChatListApi, getCorpUserChatListProps } from '../../API/groupLeaderManage';
+import { delCorpUserChatApi, getCorpUserChatListApi, getCorpUserChatListProps } from '../../API/groupLeaderManage';
 import { GroupLeaderTableConfig } from './tableConfig';
 import SearchBox from '../../components/searchBox';
-import { SearchOutlined, PlusOutlined } from '@ant-design/icons';
+import { SearchOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
 import { getAdAccountAllOfMember, getCorpAllListApi } from '@/API/global';
 import './global.less'
 import AddGL from './addGL';
@@ -18,13 +18,16 @@ import AddGL from './addGL';
 const GroupLeaderManage: React.FC = () => {
 
     /*******************************************/
+    const { message } = App.useApp();
     const ref = useRef<HTMLDivElement>(null)
     const size = useSize(ref)
     const [queryParams, setQueryParams] = useState<getCorpUserChatListProps>({ pageNum: 1, pageSize: 20 })
     const [queryParamsNew, setQueryParamsNew] = useState<getCorpUserChatListProps>({ pageNum: 1, pageSize: 20 })
     const [visible, setVisible] = useState<boolean>(false)
+    const [selectedRows, setselectedRows] = useState<any[]>([])
 
     const getCorpUserChatList = useAjax((params) => getCorpUserChatListApi(params))
+    const delCorpUserChat = useAjax((params) => delCorpUserChatApi(params))
     const getCorpAllList = useAjax((params) => getCorpAllListApi(params))
     const allOfMember = useAjax(() => getAdAccountAllOfMember())
     /*******************************************/
@@ -38,6 +41,14 @@ const GroupLeaderManage: React.FC = () => {
         getCorpUserChatList.run(queryParamsNew)
     }, [queryParamsNew])
 
+    const handleDel = () => {
+        delCorpUserChat.run(selectedRows.map(item => item.id)).then(res => {
+            setselectedRows([])
+            getCorpUserChatList.refresh()
+            message.success('删除成功')
+        })
+    }
+
     return <Card
         styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: 'calc(100vh - 74px)', overflow: 'hidden' } }}
     >
@@ -49,6 +60,14 @@ const GroupLeaderManage: React.FC = () => {
                         setQueryParamsNew({ ...queryParams, pageNum: 1 })
                     }} loading={getCorpUserChatList.loading} icon={<SearchOutlined />}>搜索</Button>
                     <Button type="primary" icon={<PlusOutlined />} onClick={() => setVisible(true)}>新增群主号</Button>
+                    <Popconfirm
+                        title="确认删除?"
+                        onConfirm={handleDel}
+                        disabled={selectedRows.length === 0}
+                    >
+                        <Button type="primary" danger icon={<DeleteOutlined />} disabled={selectedRows.length === 0}>删除</Button>
+                    </Popconfirm>
+
                 </>}
             >
                 <>
@@ -118,6 +137,40 @@ const GroupLeaderManage: React.FC = () => {
                 size='small'
                 loading={getCorpUserChatList?.loading}
                 scroll={{ y: size?.height && ref.current ? size?.height - ref.current.querySelector('.ant-table-thead').clientHeight : 300 }}
+                rowSelection={{
+                    selectedRowKeys: selectedRows?.map((item: any) => item?.id),
+                    onSelect: (record: { id: string }, selected: boolean) => {
+                        let newData = JSON.parse(JSON.stringify(selectedRows))
+                        if (selected) {
+                            newData.push({ ...record })
+                        } else {
+                            newData = newData.filter((item: { id: string }) => item.id !== record.id)
+                        }
+                        setselectedRows(newData)
+                    },
+                    onSelectAll: (selected: boolean, _: { id: string }[], changeRows: { id: string }[]) => {
+                        let newData = JSON.parse(JSON.stringify(selectedRows || '[]'))
+                        if (selected) {
+                            changeRows.forEach((item: { id: string }) => {
+                                const index = newData.findIndex((ite: { id: string }) => ite.id === item.id)
+                                if (index === -1) {
+                                    newData.push(item)
+                                }
+                            })
+                        } else {
+                            const newSelectAccData = newData.filter((item: { id: string }) => {
+                                const index = changeRows.findIndex((ite: { id: string }) => ite.id === item.id)
+                                if (index !== -1) {
+                                    return false
+                                } else {
+                                    return true
+                                }
+                            })
+                            newData = newSelectAccData
+                        }
+                        setselectedRows(newData)
+                    }
+                }}
             />
         </div>
         <div className={style.bookLinkPagination}>