wjx 3 týždňov pred
rodič
commit
5753d979a6

+ 16 - 3
src/pages/weComTask/API/global.ts

@@ -274,7 +274,7 @@ export async function getGameListNewApi(data: { sourceSystem: string }) {
 
 
 // 根据书城获取书籍
-export async function getBookListApi(data: { pageNum: number, pageSize: number, platformKey: string, bookName?: string  }) {
+export async function getBookListApi(data: { pageNum: number, pageSize: number, platformKey: string, bookName?: string }) {
     return request({
         url: `/corp/externalUser/platform/bookList`,
         method: 'POST',
@@ -283,7 +283,7 @@ export async function getBookListApi(data: { pageNum: number, pageSize: number,
 }
 
 // 根据书城书籍获取章节
-export async function getBookChapterListListApi(data: { pageNum: number, pageSize: number, platformKey: string, bookName: string, chapterName?: string  }) {
+export async function getBookChapterListListApi(data: { pageNum: number, pageSize: number, platformKey: string, bookName: string, chapterName?: string }) {
     return request({
         url: `/corp/externalUser/platform/chapterList`,
         method: 'POST',
@@ -292,10 +292,23 @@ export async function getBookChapterListListApi(data: { pageNum: number, pageSiz
 }
 
 // 小程序链接
-export async function getGenerateUrllinkApi(data: {path: string, query: string}) {
+export async function getGenerateUrllinkApi(data: { path: string, query: string }) {
     return request({
         url: api + '/bookAppWechatMiniapp/api/miniappBase/generateUrllink/wxed3542b04192b2ee',
         method: 'POST',
         data
     });
+}
+
+/**
+ * 获取所有企微客服号
+ * @param params 
+ * @returns 
+ */
+export async function getCorpUserListApi(params: { userId?: number }) {
+    return request({
+        url: api + '/corpOperation/landing/get/corp/user',
+        method: 'GET',
+        params
+    });
 }

+ 3 - 0
src/pages/weComTask/API/miniProgramPages/index.ts

@@ -59,6 +59,9 @@ export interface GetLandingPageListProps {
     endTime?: string,
     remark?: string,
     projectGroupIdList?: number[]
+    bookName?: string
+    corpId?: string,
+    corpUserIdList?: string[]
 }
 
 /**

+ 49 - 0
src/pages/weComTask/components/materialMould/uploadLoad.tsx

@@ -0,0 +1,49 @@
+import { useAjax } from "@/Hook/useAjax";
+import { Upload } from "antd";
+import { RcFile } from "antd/es/upload";
+import React from "react"
+import { saveMediaApi } from "../../API/weMaterial/weMaterial";
+import { useOss } from "@/Hook/useOss";
+import dayjs from 'dayjs'
+
+interface Props {
+    uploadButton?: JSX.Element
+    type?: 'image' | 'video',
+    onChange?: (data: string) => void
+}
+
+const UploadLoad: React.FC<Props> = ({ uploadButton, onChange, type = 'image' }) => {
+
+    /********************************/
+    const saveMedia = useAjax((params) => saveMediaApi(params))
+    const ossUpload = useOss(true)
+    /********************************/
+
+    const upload = (file: RcFile) => {
+        const name = dayjs().valueOf().toString()
+        ossUpload.run(file, name).then(res => {
+            if (res?.data) {
+                const [fileName, nameSuffix] = file.name.split('.')
+                saveMedia.run({ fileName: name || fileName, mediaType: type || 'image', suffix: nameSuffix, url: res?.data })
+                onChange?.(res.data)
+            }
+        })
+    }
+
+    return <>
+        <Upload
+            name="avatar"
+            accept={type === 'image' ? 'image/png,image/jpg,image/jpeg,image/gif' : 'video/mp4'}
+            action="#"
+            showUploadList={false}
+            customRequest={() => { }}
+            beforeUpload={(file: RcFile): any => {
+                upload(file)
+            }}
+        >
+            {uploadButton}
+        </Upload>
+    </>
+}
+
+export default React.memo(UploadLoad)

+ 13 - 2
src/pages/weComTask/page/miniProgramPages/center.tsx

@@ -3,6 +3,7 @@ import React, { useContext } from "react"
 import style from './index.less'
 import { DispatchMiniPageCreate } from "./drawerMini";
 import CenterTop from './centerTop'
+import CenterCompt from "./centerCompt";
 
 const Center: React.FC = () => {
 
@@ -11,13 +12,23 @@ const Center: React.FC = () => {
     const { bgColor } = pageSpecs
     /**************************************/
 
-    return <Col flex="auto" className={style.center}>
+    const installActiveNull = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
+        e.stopPropagation()
+        const { pageElementsSpecList } = pageSpecs
+        const newPageElementsSpecList = pageElementsSpecList?.map(item => ({ ...item, comptActive: false }))
+        setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList })
+    }
+
+    return <Col flex="auto" className={style.center} onClick={installActiveNull}>
         <div className={style.page} style={{ backgroundColor: bgColor || '#FFFFFF' }}>
             <div><CenterTop /></div>
+            <div className={`comptPlaceholder lastChild`} id="comptCon">
+                <CenterCompt />
+            </div>
             <div className={style.sidebar}>
                 <div>
                     <ColorPicker
-                        onChange={(value, css) => {
+                        onChange={(_, css) => {
                             setPageSpecs({ ...pageSpecs, bgColor: css })
                         }}
                         value={bgColor}

+ 322 - 0
src/pages/weComTask/page/miniProgramPages/centerCompt.tsx

@@ -0,0 +1,322 @@
+import React, { useContext } from "react"
+import { arrayMove, SortableContainer, SortableElement, SortableHandle } from "react-sortable-hoc";
+import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined, BorderRightOutlined } from "@ant-design/icons";
+import { Button, Tooltip } from "antd";
+import { ReactComponent as EditSvg } from '../../../../public/svg/edit.svg'
+import { ReactComponent as ImgSvg } from '../../../../public/svg/img.svg'
+import { ReactComponent as TextSvg } from '../../../../public/svg/text.svg'
+import { ReactComponent as WxAutoSvg } from '../../../../public/svg/wxAutoSvg.svg'
+import { DispatchMiniPageCreate } from "./drawerMini";
+import { useDrop } from "ahooks";
+import { floatButtonContent, imgContent, qrCodeContent, txtContent } from "./const";
+import UploadLoad from "../../components/materialMould/uploadLoad";
+
+const DragHandle = SortableHandle(() => <Button style={{ cursor: 'grab', fontSize: 14 }} className="handle" onClick={(e) => { e.stopPropagation() }} icon={<BorderRightOutlined />}></Button>);
+
+const ComptEdit = (props: {
+    data: any,
+    pureImageUrl?: string,
+    handleBtn: (type: string, index: number) => void,
+    del: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void,
+    onChange?: (v: any) => void
+}) => {
+    const { data, handleBtn, del, pureImageUrl, onChange } = props
+    return <>
+        <section className="comptEditTrBtns">
+            <div className="comptEditTrBtnsInner">
+                {data.index > 1 && <a onClick={(e) => { e.stopPropagation(); handleBtn('upper', data.index) }}><ArrowUpOutlined /></a>}
+                {data.length !== data.index + 1 && <a onClick={(e) => { e.stopPropagation(); handleBtn('lower', data.index) }}><ArrowDownOutlined /></a>}
+                <Tooltip placement="topRight" color="#FFF" title={<div className="assBts">
+                    <div onClick={(e) => { e.stopPropagation(); handleBtn('IMAGE', data.index) }}><ImgSvg /></div>
+                    <div onClick={(e) => { e.stopPropagation(); handleBtn('TEXT', data.index) }}><TextSvg /></div>
+                </div>}>
+                    <a><PlusOutlined /></a>
+                </Tooltip>
+            </div>
+        </section>
+        <section className={'comptEditBtns'}>
+            <div className={'comptEditBtnsInner'}>
+                {pureImageUrl && <UploadLoad
+                    type='image'
+                    uploadButton={<Button icon={<EditSvg />} />}
+                    onChange={(value) => {
+                        onChange?.(value)
+                    }}
+                />}
+                <DragHandle />
+                <Button onClick={(e) => { del(e) }} style={{ fontSize: 14 }} icon={<DeleteOutlined />}></Button>
+            </div>
+        </section>
+    </>
+}
+
+interface ContainerProps {
+    children: React.ReactNode;
+    isFloatButton?: boolean
+}
+
+const SortableList = SortableContainer<ContainerProps>(({ children, isFloatButton }) => (<div className="page-0" style={isFloatButton ? { paddingBottom: 90, minHeight: 510 } : {}}>{children}</div>));
+
+interface ElementProps {
+    data: { length: number, index: number }
+    item: TASK_MINI_PAGE_CREATE.ComptItem
+    click: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
+    del: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
+    handleBtn: (type: string, index: number) => void
+    pageBackColor?: string,
+    onChange?: (v: any) => void
+}
+
+/** 内容文本 */
+const SortableItemText = SortableElement<ElementProps>(({ item, click, del, pageBackColor, handleBtn, data }) => {
+    let { fontSize, color, textAlign, text, fontWeight, paddingTop, paddingBottom } = item as TASK_MINI_PAGE_CREATE.Text
+    return <div className={`compt componentType1 ${item.comptActive && 'comptActive'}`} onClick={(e) => { click(e) }}>
+        <div className={'componentWrap'}>
+            <div className={'componentContent'} style={{ backgroundColor: pageBackColor }}>
+                <div className={'text'} style={{ fontSize: fontSize, color, textAlign, fontWeight, maxWidth: '100%', display: 'block', marginLeft: 24, marginRight: 24, marginTop: paddingTop + 'px', marginBottom: paddingBottom + 'px' }}>
+                    <div>{text ?
+                        text?.split(/[\r\n]/g)?.map((item: any, index: number) => {
+                            if (item) {
+                                return <div key={`item${index}`}>
+                                    {item?.split(' ')?.map((item1: any, ind: number) => {
+                                        if (item1) {
+                                            return <span key={`item1${ind}`}>{item1}</span>
+                                        } else {
+                                            return <span key={`item1${ind}`}>&nbsp;</span>
+                                        }
+                                    })}
+                                </div>
+                            } else {
+                                return <div key={`item${index}`}>&nbsp;</div>
+                            }
+                        })
+                        : '请输入文本内容'}</div>
+                </div>
+                <div className={'textAreaDiv'} style={{ fontSize: fontSize, margin: '10px 24px', marginLeft: 24, marginRight: 24, marginTop: paddingTop + 'px', marginBottom: paddingBottom + 'px' }}>
+                    <textarea readOnly value={text} className={'textarea'} placeholder={item.comptActive ? `请在右侧输入文本内容` : '请输入文本内容'} style={{ color, fontWeight, textAlign, backgroundColor: pageBackColor }}></textarea>
+                </div>
+            </div>
+        </div>
+        <ComptEdit data={data} handleBtn={(type, index) => { handleBtn(type, index) }} del={(e) => { del(e) }} />
+    </div>
+});
+
+/** 内容图片 */
+const SortableItemImg = SortableElement<ElementProps>(({ item, click, del, data, handleBtn, onChange }) => {
+    const { url, paddingTop, paddingBottom } = item
+
+    return <div className={`compt componentType41 ${item.comptActive && 'comptActive'}`} onClick={(e) => { click(e) }}>
+        <div className={'componentWrap'}>
+            <div className={'componentContent'}>
+                {url ? <img src={url} style={{ display: 'block', width: '100%', margin: 0, marginTop: paddingTop + 'px', marginBottom: paddingBottom + 'px' }} /> : <div className={'default'} style={{ width: 375, height: 222, margin: 0, marginTop: paddingTop + 'px', marginBottom: paddingBottom + 'px' }}>
+                    <div className={'defaultIcon'} style={{ marginTop: 44 }}>
+                        <ImgSvg />
+                    </div>
+                </div>}
+            </div>
+        </div>
+        {!url && <div className={'comptUpload'} style={{ margin: 0, marginTop: paddingTop, marginBottom: paddingBottom }}>
+            <UploadLoad
+                type='image'
+                uploadButton={<button style={{ marginTop: 114 }} className={'comptEditButton'}>上传图片</button>}
+                onChange={(value) => {
+                    onChange?.(value)
+                }}
+            />
+        </div>}
+        <ComptEdit data={data} handleBtn={(type, index) => { handleBtn(type, index) }} del={(e) => { del(e) }} pureImageUrl={url} onChange={onChange} />
+    </div>
+});
+
+
+
+/** 添加企微 */
+const SortableItemWxAuto = SortableElement<ElementProps>(({ item, click, del, data, handleBtn, onChange }) => {
+    const { imageList, paddingTop, paddingBottom, paddingLeft, paddingRight } = item
+
+    return <div className={`compt componentType41 ${item.comptActive && 'comptActive'}`} onClick={(e) => { click(e) }}>
+        <div className={'componentWrap'}>
+            <div className={'componentContent'}>
+                {imageList?.[0] ? <img src={imageList[0]} style={{ display: 'block', width: '100%', margin: 0, marginTop: paddingTop + 'px', marginBottom: paddingBottom + 'px', paddingLeft: paddingLeft + 'px', paddingRight: paddingRight + 'px', boxSizing: 'border-box' }} /> : <div className={'default'} style={{ width: 375, height: 222, margin: 0, marginTop: paddingTop + 'px', marginBottom: paddingBottom + 'px', paddingLeft: paddingLeft + 'px', paddingRight: paddingRight + 'px', boxSizing: 'border-box' }}>
+                    <div className={'defaultIcon'} style={{ marginTop: 44 }}>
+                        <WxAutoSvg />
+                    </div>
+                </div>}
+            </div>
+        </div>
+        {!imageList?.[0] && <div className={'comptUpload'} style={{ margin: 0, marginTop: paddingTop, marginBottom: paddingBottom, paddingLeft: paddingLeft + 'px', paddingRight: paddingRight + 'px', boxSizing: 'border-box' }}>
+            <UploadLoad
+                type='image'
+                uploadButton={<button style={{ marginTop: 114 }} className={'comptEditButton'}>上传企微客服二维码图片</button>}
+                onChange={(value) => {
+                    onChange?.(value)
+                }}
+            />
+        </div>}
+        <ComptEdit data={data} handleBtn={(type, index) => { handleBtn(type, index) }} del={(e) => { del(e) }} pureImageUrl={imageList?.[0]} onChange={onChange} />
+    </div>
+});
+
+
+/** 悬浮组件 */
+const SortableItemFloatbutton = SortableElement<{
+    item: TASK_MINI_PAGE_CREATE.FloatButton
+    click: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
+    del: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
+}>(({ item, click, del }) => {
+    const { qrCodeFloatList, title } = item as TASK_MINI_PAGE_CREATE.FloatButton
+    return <div className={`compt componentType134 comptFixedBottom ${item.comptActive && 'comptActive'}`} onClick={(e) => { click(e) }}>
+        <div className={'componentWrap'}>
+            <div className="componentContent">
+                {qrCodeFloatList?.length > 0 ? <img src={qrCodeFloatList[0]} style={{ width: '100%', display: 'block' }} /> : <div className="floatButtonWrapper">
+                    <div className="floatButton">
+                        <div className="floatButtonAvatarPlaceholder"></div>
+                        <div className="floatButtonTexts">
+                            <div className="floatButtonTitle" style={{ color: 'rgb(23, 23, 23)' }}>{title || '标题'}</div>
+                            <div className="floatButtonDesc" style={{ color: 'rgb(76, 76, 76)' }}>{'联系客服'}</div>
+                        </div>
+                    </div>
+                </div>}
+            </div>
+        </div>
+        <section className={'comptEditBtns'}>
+            <div className={'comptEditBtnsInner'}>
+                <Button onClick={(e) => { del(e) }} icon={<DeleteOutlined />} style={{ fontSize: 14 }}></Button>
+            </div>
+        </section>
+    </div>
+})
+
+
+const CenterCompt: React.FC = () => {
+
+    /**************************************/
+    const { pageSpecs, setPageSpecs, draggingCon, installActive, setCompt } = useContext(DispatchMiniPageCreate)!;
+    const { pageElementsSpecList, bgColor, globalElementsSpecList } = pageSpecs
+    /**************************************/
+
+    const [dropProps, { isHovering }] = useDrop({
+        onDom: (key) => {
+            onDom(key, 999);
+        },
+    });
+
+    const onDom = (key: string, index) => { // 内容
+        const newPageElementsSpecList = pageElementsSpecList?.map(item => ({ ...item, comptActive: false }))
+        const newGlobalElementsSpecList = globalElementsSpecList?.map(item => ({ ...item, comptActive: false })) || []
+        if (key === 'TEXT') {
+            if (index === 999) {
+                newPageElementsSpecList.push({ ...txtContent, comptActive: true })
+            } else {
+                newPageElementsSpecList.splice(index, 0, { ...txtContent, comptActive: true })
+            }
+        } else if (key === 'IMAGE') {
+            if (index === 999) {
+                newPageElementsSpecList.push({ ...imgContent, comptActive: true })
+            } else {
+                newPageElementsSpecList.splice(index, 0, { ...imgContent, comptActive: true })
+            }
+        } else if (key === 'QR_CODE') {
+            if (index === 999) {
+                newPageElementsSpecList.push({ ...qrCodeContent, comptActive: true, id: Date.now() })
+            } else {
+                newPageElementsSpecList.splice(index, 0, { ...qrCodeContent, comptActive: true, id: Date.now() })
+            }
+        } else if (key === 'FLOAT_BUTTON') {
+            newGlobalElementsSpecList.push({ ...floatButtonContent, comptActive: true, id: Date.now() })
+        } else {
+            return
+        }
+        setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList, globalElementsSpecList: newGlobalElementsSpecList })
+    }
+
+    /** 内容删除 */
+    const delComptSpec = (e, index: number) => {
+        e.stopPropagation(); e.preventDefault();
+        if (index === 99999) { // 删除悬浮组件
+            let newGlobalElementsSpecList = JSON.parse(JSON.stringify(globalElementsSpecList))
+            newGlobalElementsSpecList = newGlobalElementsSpecList?.filter(item => item.elementType !== 'FLOAT_BUTTON')
+            setPageSpecs({ ...pageSpecs, globalElementsSpecList: newGlobalElementsSpecList })
+        } else {
+            const newPageElementsSpecList = JSON.parse(JSON.stringify(pageElementsSpecList || []))
+            newPageElementsSpecList.splice(index, 1)
+            setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList })
+        }
+    }
+
+    /** 内容功能按钮区 */
+    const handleBtn = (type: string, index: number) => {
+        const newPageElementsSpecList = JSON.parse(JSON.stringify(pageElementsSpecList))
+        switch (type) {
+            case 'lower': // 下移动
+                setPageSpecs({ ...pageSpecs, pageElementsSpecList: arrayMove(pageElementsSpecList, index, index + 1) })
+                break;
+            case 'upper': // 上移动
+                setPageSpecs({ ...pageSpecs, pageElementsSpecList: arrayMove(pageElementsSpecList, index, index - 1) })
+                break;
+            case 'IMAGE':   // 图片
+                newPageElementsSpecList.splice(index, 0, { ...imgContent });
+                setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList })
+                break;
+            case 'TEXT': // 文本
+                newPageElementsSpecList.splice(index, 0, { ...txtContent });
+                setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList })
+                break;
+        }
+    }
+
+    const onSortEnd = ({ oldIndex, newIndex }: { oldIndex: number, newIndex: number }) => {
+        setPageSpecs({ ...pageSpecs, pageElementsSpecList: arrayMove(pageElementsSpecList, oldIndex, newIndex) })
+    }
+
+    const floatButton = globalElementsSpecList?.find(item => item.elementType === 'FLOAT_BUTTON')
+
+    if (pageElementsSpecList?.length > 0) {
+        return <SortableList axis='y' onSortEnd={onSortEnd} useDragHandle isFloatButton={!!floatButton}>
+            {pageElementsSpecList.map((item: TASK_MINI_PAGE_CREATE.ComptItem, index: number) => {
+                switch (item.elementType) {
+                    case "IMAGE":
+                        return <SortableItemImg
+                            key={`item-${item.elementType}-${index}`}
+                            index={index} data={{ length: pageElementsSpecList?.length, index }}
+                            item={item}
+                            click={(e) => { installActive(e, index) }}
+                            del={(e) => { delComptSpec(e, index) }}
+                            handleBtn={handleBtn}
+                            onChange={(url) => setCompt('url', url)}
+                        />
+                    case "TEXT":
+                        return <SortableItemText
+                            key={`item-${item.elementType}-${index}`}
+                            index={index}
+                            data={{ length: pageElementsSpecList?.length, index }}
+                            item={item}
+                            click={(e) => { installActive(e, index) }}
+                            del={(e) => { delComptSpec(e, index) }}
+                            pageBackColor={bgColor}
+                            handleBtn={handleBtn}
+                        />
+                    case "QR_CODE":
+                        return <SortableItemWxAuto
+                            key={`item-${item.elementType}-${index}`}
+                            index={index} data={{ length: pageElementsSpecList?.length, index }}
+                            item={item}
+                            click={(e) => { installActive(e, index) }}
+                            del={(e) => { delComptSpec(e, index) }}
+                            handleBtn={handleBtn}
+                            onChange={(url) => setCompt('imageList', [url])}
+                        />
+                    default:
+                        return null
+                }
+            })}
+            <div className={`comptCon ${isHovering && 'hovering'} ${draggingCon && 'draggingCon'}`} {...dropProps}>
+                {(isHovering || draggingCon) && '请拖至此处'}
+            </div>
+            {floatButton && <SortableItemFloatbutton index={99999} item={floatButton} click={(e: any) => { installActive(e, 99999) }} del={(e: any) => { delComptSpec(e, 99999) }} />}
+        </SortableList>
+    }
+    return null
+}
+
+export default React.memo(CenterCompt)

+ 35 - 21
src/pages/weComTask/page/miniProgramPages/centerTop.tsx

@@ -11,12 +11,13 @@ import { ReactComponent as EditSvg } from '../../../../public/svg/edit.svg'
 import { ReactComponent as TopVideoSvg } from '../../../../public/svg/topvideo.svg'
 import VideoNews from "../../components/newsModal/videoNews";
 import { Button } from "antd";
+import UploadLoad from "../../components/materialMould/uploadLoad";
 
 const CenterTop: React.FC = () => {
 
     /**************************************/
-    const { dragging, pageSpecs, setDragging, setPageSpecs, installActive } = useContext(DispatchMiniPageCreate)!;
-    const { pageElementsSpecList } = pageSpecs
+    const { dragging, pageSpecs, setDragging, setPageSpecs, installActive, setTopUrl } = useContext(DispatchMiniPageCreate)!;
+    const { pageElementsSpecList, globalElementsSpecList } = pageSpecs
     const topSpec = pageElementsSpecList?.[0]
     /**************************************/
 
@@ -24,6 +25,7 @@ const CenterTop: React.FC = () => {
     const [dropProps] = useDrop({
         onDom: (key: string) => { // 头部
             const newPageElementsSpecList = pageElementsSpecList?.map(item => ({ ...item, comptActive: false }))
+            const newGlobalElementsSpecList = globalElementsSpecList?.map(item => ({ ...item, comptActive: false }))
             if (key === 'TOP_IMAGE') {
                 newPageElementsSpecList[0] = { ...topImageContent, comptActive: true }
             } else if (key === 'TOP_VIDEO') {
@@ -31,18 +33,10 @@ const CenterTop: React.FC = () => {
             } else {
                 return
             }
-            setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList })
+            setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList, globalElementsSpecList: newGlobalElementsSpecList })
         }
     });
 
-
-    // 内容拖到接收区
-    const [dropConProps, { isHovering: isHoveringCon }] = useDrop({
-        onDom: (con: string, e) => { // 内容
-
-        },
-    });
-
     const delTopSpec = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
         e.stopPropagation();
         setDragging(null);
@@ -65,13 +59,23 @@ const CenterTop: React.FC = () => {
                         </div>
                     </div>
 
-                    {!topSpec?.url && <div className={'comptUpload'} style={{ margin: 0 }}>
-                        <button style={{ marginTop: 150 }} className={'comptEditButton'} onClick={() => { }}>上传图片</button>
-                    </div>}
+                    {!topSpec?.url && <div className={'comptUpload'} style={{ margin: 0 }}><UploadLoad
+                        type='image'
+                        uploadButton={<button style={{ marginTop: 150 }} className={'comptEditButton'} onClick={() => { }}>上传图片</button>}
+                        onChange={(value) => {
+                            setTopUrl(value)
+                        }}
+                    /></div>}
                     <section className={'comptEditBtns'}>
                         <div className={'comptEditBtnsInner'}>
-                            {topSpec?.url && <Button onClick={() => { }} icon={<EditSvg />}></Button>}
-                            <Button onClick={delTopSpec} icon={<DeleteOutlined />} style={{ fontSize: 14 }}/>
+                            {topSpec?.url && <UploadLoad
+                                type='image'
+                                uploadButton={<Button icon={<EditSvg />}></Button>}
+                                onChange={(value) => {
+                                    setTopUrl(value)
+                                }}
+                            />}
+                            <Button onClick={delTopSpec} icon={<DeleteOutlined />} style={{ fontSize: 14 }} />
                         </div>
                     </section>
                 </div>
@@ -88,14 +92,24 @@ const CenterTop: React.FC = () => {
                             </div>}
                         </div>
                     </div>
-                    {!topSpec?.url && <div className={'comptUpload'} style={{ margin: 0 }}>
-                        <button style={{ marginTop: 150 }} className={'comptEditButton'} onClick={() => { }}>上传视频</button>
-                    </div>}
+                    {!topSpec?.url && <div className={'comptUpload'} style={{ margin: 0 }}><UploadLoad
+                        type='video'
+                        uploadButton={<button style={{ marginTop: 150 }} className={'comptEditButton'} onClick={() => { }}>上传视频</button>}
+                        onChange={(value) => {
+                            setTopUrl(value)
+                        }}
+                    /></div>}
 
                     <section className={'comptEditBtns'}>
                         <div className={'comptEditBtnsInner'}>
-                            {topSpec?.url && <Button onClick={() => { }} icon={<EditSvg />}></Button>}
-                            <Button onClick={delTopSpec} icon={<DeleteOutlined />} style={{ fontSize: 14 }}/>
+                            {topSpec?.url && <UploadLoad
+                                type='video'
+                                uploadButton={<Button icon={<EditSvg />}></Button>}
+                                onChange={(value) => {
+                                    setTopUrl(value)
+                                }}
+                            />}
+                            <Button onClick={delTopSpec} icon={<DeleteOutlined />} style={{ fontSize: 14 }} />
                         </div>
                     </section>
                 </div>

+ 25 - 6
src/pages/weComTask/page/miniProgramPages/const.ts

@@ -19,14 +19,33 @@ export const imgContent: TASK_MINI_PAGE_CREATE.Image = {
     paddingBottom: 0,
 }
 
+// 添加企微内容
+export const qrCodeContent: TASK_MINI_PAGE_CREATE.QrCode = {
+    elementType: 'QR_CODE',
+    imageList: [],
+    paddingTop: 0,
+    paddingBottom: 0,
+    paddingLeft: 0,
+    paddingRight: 0,
+    id: 0
+}
+
+// 悬浮按钮
+export const floatButtonContent: TASK_MINI_PAGE_CREATE.FloatButton = {
+    elementType: 'FLOAT_BUTTON',
+    title: '长按添加客服看下一章',
+    qrCodeFloatList: [],
+    id: 0
+}
+
 // 内容文本
 export const txtContent: TASK_MINI_PAGE_CREATE.Text = {
     elementType: 'TEXT',
     text: '',
-    fontStyle: 0,
-    textAlignment: 0,
-    fontSize: 20,
-    fontColor: '#595959',
-    paddingTop: 22,
-    paddingBottom: 22,
+    fontWeight: 'normal',
+    textAlign: 'left',
+    fontSize: 24,
+    color: '#000000',
+    paddingTop: 10,
+    paddingBottom: 10,
 }

+ 4 - 2
src/pages/weComTask/page/miniProgramPages/dragItem.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+import style from './index.less'
 
 interface Props {
     dragProps: {
@@ -9,12 +10,13 @@ interface Props {
     }
     title: string,
     icon: JSX.Element
+    disabled?: boolean
 }
 
-const DragItem: React.FC<Props> = ({ dragProps, title, icon }) => {
+const DragItem: React.FC<Props> = ({ dragProps, title, icon, disabled }) => {
 
     const { key, ...otherProps } = dragProps;
-    return <div key={key} {...otherProps}> {icon} <span>{title}</span></div>
+    return <div key={key} {...otherProps} {...(disabled ? { draggable: false, className: style.disabled } : {})}>{icon}<span>{title}</span></div>
 }
 
 export default React.memo(DragItem)

+ 132 - 7
src/pages/weComTask/page/miniProgramPages/drawerMini.tsx

@@ -1,12 +1,17 @@
-import { Drawer, Row } from "antd"
-import React, { useState } from "react"
+import { App, Button, Drawer, Row } from "antd"
+import React, { useEffect, useState } from "react"
 import style from './index.less'
 import Left from "./left"
 import Center from "./center"
 import Right from "./right"
+import { SwapRightOutlined } from '@ant-design/icons';
+import { useAjax } from "@/Hook/useAjax"
+import { createLandingPageApi, editLandingPageApi } from "../../API/miniProgramPages"
+import Submit from "./submit"
 
 interface Props {
     groupList: { label: string, value: number }[]
+    corpList: { corpName: string, corpId: string, corpUserList: { corpUserName: string, corpUserId: string }[] }[]
     visible?: boolean
     onChange?: () => void
     onClose?: () => void
@@ -15,31 +20,133 @@ interface Props {
 
 export const DispatchMiniPageCreate = React.createContext<TASK_MINI_PAGE_CREATE.DispatchMiniPageCreate | null>(null);
 
-const DrawerMini: React.FC<Props> = ({ visible, onChange, onClose, groupList, initialValues }) => {
+const DrawerMini: React.FC<Props> = ({ visible, onChange, onClose, groupList, corpList, initialValues }) => {
 
 
     /**************************************/
+    const { message } = App.useApp();
     const [pageSpecs, setPageSpecs] = useState<TASK_MINI_PAGE_CREATE.PageSpecsProps>({
         bgColor: '#FFFFFF',
         pageElementsSpecList: [{ elementType: 'empty' }]
     })
     const [dragging, setDragging] = useState<string | null>(null);
     const [draggingCon, setDraggingCon] = useState<string | null>(null);
+
+    const [sVisible, setSVisible] = useState<boolean>(false)
     /**************************************/
+    console.log('pageSpecs-------------------->', pageSpecs)
+
+    useEffect(() => {
+        if (initialValues?.pageSpecs) {
+            setPageSpecs(initialValues.pageSpecs)
+        }
+    }, [initialValues])
 
     const installActive = (e: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => {
         e.stopPropagation();
         e.preventDefault();
-        const { pageElementsSpecList } = pageSpecs
+        const { pageElementsSpecList, globalElementsSpecList } = pageSpecs
         const newPageElementsSpecList = pageElementsSpecList?.map(item => ({ ...item, comptActive: false }))
-        newPageElementsSpecList[index].comptActive = true
+        let newGlobalElementsSpecList = globalElementsSpecList?.map(item => ({ ...item, comptActive: false })) || []
+        if (index === 99999) {
+            newGlobalElementsSpecList = newGlobalElementsSpecList.map(item => item.elementType === 'FLOAT_BUTTON' ? { ...item, comptActive: true } : item)
+        } else {
+            newPageElementsSpecList[index].comptActive = true
+        }
+        setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList, globalElementsSpecList: newGlobalElementsSpecList })
+    }
+
+    // 设置顶部素材链接
+    const setTopUrl = (url: string) => {
+        const { pageElementsSpecList } = pageSpecs
+        const newPageElementsSpecList = JSON.parse(JSON.stringify(pageElementsSpecList))
+        newPageElementsSpecList[0].url = url
         setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList })
     }
 
-    console.log('pageElementsSpecList------------->', pageSpecs)
+    // 设置内容
+    const setCompt = (key: string, value: any) => {
+        const { pageElementsSpecList } = pageSpecs
+        const newPageElementsSpecList = JSON.parse(JSON.stringify(pageElementsSpecList))
+        let selectSpec = newPageElementsSpecList?.find((item) => item.comptActive)
+        if (selectSpec) {
+            selectSpec[key] = value
+            setPageSpecs({ ...pageSpecs, pageElementsSpecList: newPageElementsSpecList })
+        }
+    }
+
+    // 设置全局组件内容
+    const setGlobal = (key: string, value: any) => {
+        const { globalElementsSpecList } = pageSpecs
+        const newGlobalElementsSpecList = JSON.parse(JSON.stringify(globalElementsSpecList))
+        let selectSpec = newGlobalElementsSpecList?.find((item) => item.comptActive)
+        if (selectSpec) {
+            selectSpec[key] = value
+            setPageSpecs({ ...pageSpecs, globalElementsSpecList: newGlobalElementsSpecList })
+        }
+    }
+
+    // 下一步
+    const handleNext = () => {
+        const { pageElementsSpecList, globalElementsSpecList } = pageSpecs
+        if (pageElementsSpecList?.some(item => {
+            if (item.elementType === 'empty') {
+                message.error('请添加顶部组件')
+                return true
+            } else if (item.elementType === 'TOP_IMAGE' || item.elementType === 'TOP_VIDEO') {
+                if (item.url) {
+                    return false
+                } else {
+                    message.error('顶部组件请上传素材')
+                    return true
+                }
+            } else if (item.elementType === 'IMAGE') {
+                if (item.url) {
+                    return false
+                } else {
+                    message.error('图片组件请上传素材')
+                    return true
+                }
+            } else if (item.elementType === 'TEXT') {
+                if (item.text) {
+                    return false
+                } else {
+                    message.error('文字组件请填写文案')
+                    return true
+                }
+            } else if (item.elementType === 'QR_CODE') {
+                if (item?.imageList?.every(i => i)) {
+                    return false
+                } else {
+                    message.error('添加商家微信组件请上传素材')
+                    return true
+                }
+            }
+        })) {
+            return
+        }
+        if (globalElementsSpecList?.some(item => {
+            if (item.elementType === 'FLOAT_BUTTON') {
+                if (item?.qrCodeFloatList?.every(i => i)) {
+                    return false
+                } else {
+                    message.error('悬浮组件请上传素材')
+                    return true
+                }
+            }
+        })) {
+            return
+        }
+        setSVisible(true)
+    }
 
     return <Drawer
-        title={<strong>{initialValues?.id ? initialValues?.isCopy ? '复制落地页' : '修改' + initialValues.name + '落地页' : '新增落地页'}</strong>}
+        title={<div className={style.drawerTitle}>
+            <div>
+                <strong>{initialValues?.isCopy ? `复制落地页 ${initialValues.name}` : initialValues?.id ? '修改' + initialValues.name + '落地页' : '新增落地页'}</strong>
+            </div>
+            <Button type='primary' className={style.next} onClick={() => { handleNext() }}>下一步 <SwapRightOutlined /></Button>
+        </div>}
         closable={{ 'aria-label': 'Close Button' }}
         onClose={onClose}
         open={visible}
@@ -53,6 +160,9 @@ const DrawerMini: React.FC<Props> = ({ visible, onChange, onClose, groupList, in
                 draggingCon, setDraggingCon,
                 pageSpecs, setPageSpecs,
                 installActive,
+                setTopUrl,
+                setCompt,
+                setGlobal
             }}
         >
             <div className={style.boxCont}>
@@ -63,6 +173,21 @@ const DrawerMini: React.FC<Props> = ({ visible, onChange, onClose, groupList, in
                 </Row>
             </div>
         </DispatchMiniPageCreate.Provider>
+        {/* 提交设置 */}
+        {sVisible && <Submit
+            pageSpecs={pageSpecs}
+            groupList={groupList}
+            corpList={corpList}
+            visible={visible}
+            initialValues={initialValues}
+            onChange={() => {
+                onChange?.()
+                setSVisible(false)
+            }}
+            onClose={() => {
+                setSVisible(false)
+            }}
+        />}
     </Drawer>
 }
 

+ 33 - 4
src/pages/weComTask/page/miniProgramPages/global.less

@@ -96,7 +96,6 @@
 .adui-form-item {
   display: flex;
   margin-bottom: 24px;
-  align-items: baseline;
 }
 
 .form-caption {
@@ -576,6 +575,7 @@
   left: 0;
   right: 0;
   z-index: 2;
+  text-align: center;
 }
 
 .comptEditButton {
@@ -819,12 +819,12 @@
   >div {
     display: block !important;
     border-radius: 0 !important;
-    
-    .ant-image{
+
+    .ant-image {
       display: block;
     }
   }
-  
+
 }
 
 .floatButtonWrapper {
@@ -995,4 +995,33 @@
 
 .custorGroup {
   flex-direction: column;
+}
+
+
+.near-item {
+  box-sizing: border-box;
+  transition: border-color 0.15s ease;
+  color: rgba(0, 0, 0, 0.2);
+  width: 100%;
+  height: 0;
+  overflow: hidden;
+}
+
+/* 隐藏(默认) */
+.near-hidden {
+  border: none;
+  height: 0;
+}
+
+/* 展示 */
+.near-visible {
+  border: 1px dashed rgba(0, 0, 0, 0.2);
+  height: 40px;
+}
+
+/* hover / 激活 */
+.near-active {
+  border: 1px dashed #1890ff;
+  color: #1890ff;
+  height: 40px;
 }

+ 77 - 26
src/pages/weComTask/page/miniProgramPages/index.tsx

@@ -12,7 +12,7 @@ import { useSize } from "ahooks";
 import { TableConfig } from "./tableConfig";
 import { randomString } from "@/utils/utils";
 import DrawerMini from "./drawerMini";
-import { getGenerateUrllinkApi } from "../../API/global";
+import { getCorpUserListApi, getGenerateUrllinkApi } from "../../API/global";
 import ShowQrCode from "./showQrCode";
 
 
@@ -33,17 +33,32 @@ const MiniProgramPages: React.FC = () => {
     const [selectedRows, setselectedRows] = useState<any[]>([])
     const [initialValues, setInitialValues] = useState<any>()
     const [qrCode, setQrCode] = useState<{ url: string, visible: boolean }>()
+    const [corpList, setCorpList] = useState<{ corpName: string, corpId: string, corpUserList: { corpUserName: string, corpUserId: string }[] }[]>([])
+    const [corpUserList, setCorpUserList] = useState<{ corpUserName: string, corpUserId: string }[]>([])
 
     const getProjectGroupsAllList = useAjax(() => getProjectGroupsAllListApi())
     const getLandingPageList = useAjax((params) => getLandingPageListApi(params))
     const delLandingPage = useAjax((params) => delLandingPageApi(params))
     const getGenerateUrllink = useAjax((params) => getGenerateUrllinkApi(params))
+    const getCorpUserList = useAjax((params) => getCorpUserListApi(params))
     /******************************************/
 
+    useEffect(() => {
+        if (queryParams?.corpId) {
+            setCorpUserList(corpList.find(item => item.corpId === queryParams.corpId)?.corpUserList || [])
+        } else {
+            setCorpUserList([])
+        }
+    }, [corpList, queryParams?.corpId])
+
     useEffect(() => {
         getProjectGroupsAllList.run().then(res => {
             setGroupList(res?.data?.map(item => ({ label: item.name, value: item.id })) || [])
         })
+
+        getCorpUserList.run().then(res => {
+            setCorpList(res?.data || [])
+        })
     }, [])
 
     useEffect(() => {
@@ -51,34 +66,34 @@ const MiniProgramPages: React.FC = () => {
     }, [queryParamsNew])
 
     const handleEdit = (d: Record<string, any>, isCopy?: boolean) => {
-        const { content, name, id, projectGroupIdList, qrCodeFloatList, remark } = d
-        let topSpec: Record<string, any> = {}
-        let floatButtonSpec: Record<string, any> = {}
-        const pageSpecsList = JSON.parse(content)
-        const elementsSpecList = (pageSpecsList.elementsSpecList as { elementType: string }[]).filter(item => {
-            if (['TOP_IMAGE', 'TOP_VIDEO'].includes(item.elementType)) {
-                topSpec = item
-                return false
-            } else if (['FLOAT_BUTTON'].includes(item.elementType)) {
-                floatButtonSpec = item
-                return false
+        const { content, name, id, projectGroupIdList, bookName, remark, corpId, corpUserList } = d
+        const { bgColor, pageName, elementsSpecList } = JSON.parse(content)
+        const pageElementsSpecList: TASK_MINI_PAGE_CREATE.PageElementsSpecListProps = []
+        const globalElementsSpecList: TASK_MINI_PAGE_CREATE.GlobalElementsSpecListProps = []
+        elementsSpecList.forEach(item => {
+            if (item.elementType === 'FLOAT_BUTTON') {
+                globalElementsSpecList.push({ ...item, comptActive: false })
+            } else {
+                pageElementsSpecList.push({ ...item, comptActive: false })
             }
-            return true
         })
+        const pageSpecs = {
+            bgColor,
+            pageElementsSpecList,
+            globalElementsSpecList
+        }
 
         const newInitialValues = {
             name,
+            pageName,
+            bookName,
+            corpId,
             id,
+            pageSpecs,
+            corpUserIdList: corpUserList.map(item => item.corpUserId),
             projectGroupIdList: projectGroupIdList.map(item => item.projectGroupId),
-            qrCodeFloatList: qrCodeFloatList?.[0]?.urlList || [''],
             remark,
-            pageSpecsList: {
-                ...pageSpecsList,
-                elementsSpecList
-            },
-            floatButtonSpec,
-            topSpec,
-            isCopy: isCopy || true
+            isCopy: isCopy || false
         }
         console.log(newInitialValues)
         if (isCopy) {
@@ -129,6 +144,32 @@ const MiniProgramPages: React.FC = () => {
                 </>}
             >
                 <>
+                    <Select
+                        placeholder='请选择主体'
+                        allowClear
+                        options={corpList.map(item => ({ label: item.corpName, value: item.corpId }))}
+                        showSearch
+                        style={{ minWidth: 130 }}
+                        value={queryParams?.corpId}
+                        filterOption={(input, option) =>
+                            (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+                        }
+                        onChange={(v) => setQueryParams({ ...queryParams, corpId: v })}
+                    />
+                    {queryParams?.corpId && <Select
+                        placeholder='请选择企微客服号'
+                        allowClear
+                        options={corpUserList.map(item => ({ label: item.corpUserName, value: item.corpUserId }))}
+                        showSearch
+                        filterOption={(input, option) =>
+                            (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+                        }
+                        mode="multiple"
+                        maxTagCount={1}
+                        style={{ minWidth: 130 }}
+                        value={queryParams?.corpUserIdList}
+                        onChange={(v) => setQueryParams({ ...queryParams, corpUserIdList: v })}
+                    />}
                     <Input
                         placeholder="落地页名称"
                         allowClear
@@ -137,6 +178,14 @@ const MiniProgramPages: React.FC = () => {
                             setQueryParams({ ...queryParams, name: e.target.value })
                         }}
                     />
+                    <Input
+                        placeholder="书名"
+                        allowClear
+                        value={queryParams?.bookName}
+                        onChange={(e) => {
+                            setQueryParams({ ...queryParams, bookName: e.target.value })
+                        }}
+                    />
                     <Input
                         placeholder="备注"
                         allowClear
@@ -243,9 +292,10 @@ const MiniProgramPages: React.FC = () => {
             />
         </div>
 
-        {visible && <NewMini
+        {/* {visible && <NewMini
             initialValues={initialValues}
             groupList={groupList}
+            corpList={corpList}
             visible={visible}
             onChange={() => {
                 setInitialValues(undefined)
@@ -256,10 +306,11 @@ const MiniProgramPages: React.FC = () => {
                 setInitialValues(undefined)
                 setVisible(false)
             }}
-        />}
-        {/* {visible && <DrawerMini
+        />} */}
+        {visible && <DrawerMini
             initialValues={initialValues}
             groupList={groupList}
+            corpList={corpList}
             visible={visible}
             onChange={() => {
                 setInitialValues(undefined)
@@ -270,9 +321,9 @@ const MiniProgramPages: React.FC = () => {
                 setInitialValues(undefined)
                 setVisible(false)
             }}
-        />} */}
+        />}
 
-        {qrCode?.visible && <ShowQrCode 
+        {qrCode?.visible && <ShowQrCode
             {...qrCode}
             onClose={() => setQrCode(undefined)}
         />}

+ 6 - 9
src/pages/weComTask/page/miniProgramPages/left.tsx

@@ -7,13 +7,14 @@ import { ReactComponent as TopVideoSvg } from '../../../../public/svg/topvideo.s
 import { ReactComponent as ImgSvg } from '../../../../public/svg/img.svg'
 import { ReactComponent as TextSvg } from '../../../../public/svg/text.svg'
 import { ReactComponent as FloatbuttonSvg } from '../../../../public/svg/floatbuttonSvg.svg'
+import { ReactComponent as WxAutoSvg } from '../../../../public/svg/wxAutoSvg.svg'
 import { DispatchMiniPageCreate } from "./drawerMini";
 import DragItem from "./dragItem";
 
 const Left: React.FC = () => {
 
     /**************************************/
-    const { setDragging, setDraggingCon, pageSpecs: { pageElementsSpecList } } = useContext(DispatchMiniPageCreate)!;
+    const { setDragging, setDraggingCon, pageSpecs: { pageElementsSpecList, globalElementsSpecList } } = useContext(DispatchMiniPageCreate)!;
     /**************************************/
 
     const getDragProps = useDrag({
@@ -39,13 +40,8 @@ const Left: React.FC = () => {
     return <Col flex="320px" className={style.right}>
         <div className={style.title}>顶部组件</div>
         <div className={style.assembly}>
-            {pageElementsSpecList?.[0].elementType === 'empty' ? <>
-                <DragItem icon={<TopImgSvg />} title="图片" dragProps={getDragProps(`TOP_IMAGE`)} />
-                <DragItem icon={<TopVideoSvg />} title="视频" dragProps={getDragProps(`TOP_VIDEO`)} />
-            </> : <>
-                <div className={style.disabled}> <TopImgSvg /> <span>图片</span> </div>
-                <div className={style.disabled}> <TopVideoSvg /> <span>视频</span></div>
-            </>}
+            <DragItem icon={<TopImgSvg />} title="图片" dragProps={getDragProps(`TOP_IMAGE`)} disabled={pageElementsSpecList?.[0].elementType !== 'empty'} />
+            <DragItem icon={<TopVideoSvg />} title="视频" dragProps={getDragProps(`TOP_VIDEO`)} disabled={pageElementsSpecList?.[0].elementType !== 'empty'} />
         </div>
 
         <div className={style.title}>基础组件</div>
@@ -56,7 +52,8 @@ const Left: React.FC = () => {
 
         <div className={style.title}>营销组件</div>
         <div className={style.assembly}>
-            <DragItem icon={<FloatbuttonSvg />} title="悬浮组件" dragProps={getDragPropsCon(`FLOAT_BUTTON`)} />
+            <DragItem icon={<FloatbuttonSvg />} title="悬浮组件" dragProps={getDragPropsCon(`FLOAT_BUTTON`)} disabled={globalElementsSpecList?.some(item => item.elementType === 'FLOAT_BUTTON')} />
+            <DragItem icon={<WxAutoSvg />} title="添加商家微信" dragProps={getDragPropsCon(`QR_CODE`)} />
         </div>
     </Col>
 }

+ 41 - 0
src/pages/weComTask/page/miniProgramPages/nearDiv.tsx

@@ -0,0 +1,41 @@
+// NearDiv.tsx
+import { useEffect, useRef } from 'react';
+import { nearManager } from './nearManager';
+import React from 'react';
+import useDrop from 'ahooks/lib/useDrop/useDrop';
+import './global.less'
+
+interface Props {
+    index: number
+    onDrop: (key: string, index: number, e: React.DragEvent<Element>) => void;
+}
+
+const NearDiv: React.FC<Props> = ({ index, onDrop }) => {
+    const ref = useRef<HTMLDivElement>(null);
+
+    useEffect(() => {
+        if (!ref.current) return;
+
+        nearManager.register(ref.current);
+
+        return () => {
+            nearManager.unregister(ref.current);
+        };
+    }, []);
+
+    const [dropProps] = useDrop({
+        onDom: (key, e) => {
+            onDrop(key, index, e);
+        }
+    });
+
+    return (
+        <div
+            ref={ref}
+            className="comptCon near-item near-hidden"
+            {...dropProps}
+        >请拖至此处</div>
+    );
+}
+
+export default React.memo(NearDiv)

+ 106 - 0
src/pages/weComTask/page/miniProgramPages/nearManager.ts

@@ -0,0 +1,106 @@
+type NearState = 'hidden' | 'visible' | 'active';
+
+type Item = {
+  el: HTMLDivElement;
+  rect: DOMRect;
+  state: NearState;
+};
+
+const SHOW_DIST = 20;
+const SHOW_DIST2 = SHOW_DIST * SHOW_DIST;
+
+class NearManager {
+  private items: Item[] = [];
+  private dragging = false;
+  private rafId: number | null = null;
+
+  constructor() {
+    window.addEventListener('pointermove', this.onMove);
+    window.addEventListener('dragover', this.onMove);
+    window.addEventListener('resize', this.updateRects);
+    window.addEventListener('scroll', this.updateRects, true);
+  }
+
+  setDragging(v: boolean) {
+    this.dragging = v;
+
+    if (!v) {
+      this.items.forEach((i) => this.setState(i, 'hidden'));
+    }
+  }
+
+  register(el: HTMLDivElement) {
+    this.items.push({
+      el,
+      rect: el.getBoundingClientRect(),
+      state: 'hidden',
+    });
+  }
+
+  unregister(el: HTMLDivElement) {
+    this.items = this.items.filter((i) => i.el !== el);
+  }
+
+  private updateRects = () => {
+    this.items.forEach(
+      (i) => (i.rect = i.el.getBoundingClientRect())
+    );
+  };
+
+  private onMove = (e: MouseEvent | PointerEvent | DragEvent) => {
+    if (!this.dragging) return;
+
+    const x = e.clientX;
+    const y = e.clientY;
+    if (x == null || y == null) return;
+
+    if (this.rafId) return;
+
+    this.rafId = requestAnimationFrame(() => {
+      this.rafId = null;
+
+      for (const item of this.items) {
+        const elAtPoint = document.elementFromPoint(x, y);
+
+        if (elAtPoint && item.el.contains(elAtPoint)) {
+          this.setState(item, 'active');
+          continue;
+        }
+
+        const d2 = distanceToRectSquared(x, y, item.rect);
+
+        if (d2 <= SHOW_DIST2) {
+          this.setState(item, 'visible');
+        } else {
+          this.setState(item, 'hidden');
+        }
+      }
+    });
+  };
+
+  private setState(item: Item, next: NearState) {
+    if (item.state === next) return;
+    item.state = next;
+
+    const el = item.el;
+    el.classList.remove(
+      'near-hidden',
+      'near-visible',
+      'near-active'
+    );
+
+    el.classList.add(`near-${next}`);
+  }
+}
+
+export const nearManager = new NearManager();
+
+function distanceToRectSquared(
+  x: number,
+  y: number,
+  rect: DOMRect
+) {
+  const dx = Math.max(rect.left - x, 0, x - rect.right);
+  const dy = Math.max(rect.top - y, 0, y - rect.bottom);
+  return dx * dx + dy * dy;
+}

+ 39 - 1
src/pages/weComTask/page/miniProgramPages/newMini.tsx

@@ -8,24 +8,35 @@ import { createLandingPageApi, editLandingPageApi } from "../../API/miniProgramP
 
 interface Props {
     groupList: { label: string, value: number }[]
+    corpList: { corpName: string, corpId: string, corpUserList: { corpUserName: string, corpUserId: string }[] }[]
     visible?: boolean
     onChange?: () => void
     onClose?: () => void
     initialValues?: any
 }
 
-const NewMini: React.FC<Props> = ({ visible, onChange, onClose, groupList, initialValues }) => {
+const NewMini: React.FC<Props> = ({ corpList, visible, onChange, onClose, groupList, initialValues }) => {
 
     /***********************************/
     const { message } = App.useApp();
     const [form] = Form.useForm();
     const elementType = Form.useWatch(['topSpec', 'elementType'], form)
     const elementsSpecList = Form.useWatch(['pageSpecsList', 'elementsSpecList'], form)
+    const corpId = Form.useWatch('corpId', form)
+    const [corpUserList, setCorpUserList] = useState<{ corpUserName: string, corpUserId: string }[]>([])
 
     const createLandingPage = useAjax((params) => createLandingPageApi(params))
     const editLandingPage = useAjax((params) => editLandingPageApi(params))
     /***********************************/
 
+    useEffect(() => {
+        if (corpId) {
+            setCorpUserList(corpList.find(item => item.corpId === corpId)?.corpUserList || [])
+        } else {
+            setCorpUserList([])
+        }
+    }, [corpList, corpId])
+
     const handleOk = () => {
         form.validateFields().then(valid => {
             const siteId = Date.now()
@@ -103,9 +114,36 @@ const NewMini: React.FC<Props> = ({ visible, onChange, onClose, groupList, initi
             <Form.Item label={<strong>落地页名称</strong>} name="name" rules={[{ required: true, message: '请输入落地页名称!' }]}>
                 <Input placeholder="请输入落地页名称" />
             </Form.Item>
+
             <Form.Item label={<strong>落地页备注</strong>} name="remark">
                 <Input placeholder="请输入落地页备注" />
             </Form.Item>
+            <Form.Item label={<strong>企微主体</strong>} name="corpId" rules={[{ required: true, message: '请选择主体!' }]}>
+                <Select
+                    placeholder='请选择主体'
+                    allowClear
+                    options={corpList.map(item => ({ label: item.corpName, value: item.corpId }))}
+                    showSearch
+                    filterOption={(input, option) =>
+                        (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+                    }
+                />
+            </Form.Item>
+            {corpId && <Form.Item label={<strong>企微客服号</strong>} name="corpUserIdList" rules={[{ required: true, message: '请选择企微客服号!' }]}>
+                <Select
+                    placeholder='请选择企微客服号'
+                    allowClear
+                    options={corpUserList.map(item => ({ label: item.corpUserName, value: item.corpUserId }))}
+                    showSearch
+                    filterOption={(input, option) =>
+                        (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+                    }
+                    mode="multiple"
+                />
+            </Form.Item>}
+            <Form.Item label={<strong>书名</strong>} name="bookName">
+                <Input placeholder="请输入书名名称" />
+            </Form.Item>
             <Form.Item label={<strong>项目组</strong>} name="projectGroupIdList" rules={[{ required: true, message: '请选择项目组!' }]}>
                 <Select
                     placeholder='请选择项目组'

+ 265 - 11
src/pages/weComTask/page/miniProgramPages/right.tsx

@@ -1,24 +1,278 @@
-import { Col } from 'antd';
+import { Col, ColorPicker, Input, InputNumber, Radio, Select, Slider, Space } from 'antd';
 import React, { useContext, useEffect, useState } from 'react';
 import style from './index.less'
 import './global.less'
 import { DispatchMiniPageCreate } from './drawerMini';
+import { RetweetOutlined, PlusOutlined, AlignLeftOutlined, AlignCenterOutlined, AlignRightOutlined, MenuOutlined } from '@ant-design/icons';
+import UploadLoad from '../../components/materialMould/uploadLoad';
+import { replaceSpecialTxt } from '@/utils/utils';
 
 const Right: React.FC = () => {
 
     /**************************************/
-    const { pageSpecs, setPageSpecs } = useContext(DispatchMiniPageCreate)!;
-    const { pageElementsSpecList } = pageSpecs
-    const activeSpec = pageElementsSpecList?.find(item => item?.elementType)
+    const { pageSpecs, setTopUrl, setCompt, setGlobal } = useContext(DispatchMiniPageCreate)!;
+    const { pageElementsSpecList, globalElementsSpecList } = pageSpecs
+    const activeSpec = pageElementsSpecList?.find((item: any) => item?.comptActive) || globalElementsSpecList?.find((item: any) => item?.comptActive)
     /**************************************/
-    // if (activeSpec) {
-    //     return <Col flex="380px" className={style.left}>
 
-    //     </Col>
-    // }
-    return <Col flex="380px" className={style.left}>
-
-    </Col>
+    if (activeSpec) {
+        return <Col flex="380px" className={style.left}>
+            {activeSpec?.elementType === 'TOP_IMAGE' ? <div className="widget">
+                <div className="caption section">
+                    <div className="caption-title">顶部组件:图片</div>
+                </div>
+                <div className="form section">
+                    <div className="form-caption">素材设置</div>
+                    <div className="adui-form-item" style={{ alignItems: 'flex-start' }}>
+                        <div className="adui-form-label">图片素材</div>
+                        <div className="adui-form-control">
+                            <UploadLoad
+                                type='image'
+                                uploadButton={<div className={`upload-img-item ${activeSpec?.url ? 'upload-img-item_uploaded' : ''}`}>
+                                    {activeSpec?.url ? <div className="upload-img-item-inner" style={{ backgroundImage: `url(${activeSpec?.url})` }}>
+                                        <div className='upload-img-item-action'>
+                                            <RetweetOutlined />
+                                        </div>
+                                    </div> : <div className="upload-img-item-inner">
+                                        <PlusOutlined />
+                                    </div>}
+                                </div>}
+                                onChange={(value) => {
+                                    setTopUrl(value)
+                                }}
+                            />
+                        </div>
+                    </div>
+                </div>
+            </div> : activeSpec?.elementType === 'TOP_VIDEO' ? <div className="widget">
+                <div className="caption section">
+                    <div className="caption-title">顶部组件:视频</div>
+                </div>
+            </div> : activeSpec?.elementType === 'TEXT' ? <div className="widget">
+                <div className="caption section">
+                    <div className="caption-title">文本</div>
+                </div>
+                <div className="form section">
+                    <div className="form-caption">推广文案</div>
+                    <Input.TextArea
+                        placeholder={`请输入(不包含<>&'"/\以及TAB、换行、回车键)`}
+                        autoSize={{ minRows: 4, maxRows: 6 }}
+                        value={activeSpec?.text}
+                        onChange={(e) => { setCompt('text', replaceSpecialTxt(e.target.value)) }}
+                    />
+                </div>
+                <div className="form section">
+                    <div className="form-caption">字符与段落</div>
+                    <div className="adui-form-item">
+                        <div className="adui-form-label">字符样式</div>
+                        <div className="adui-form-control">
+                            <div className="fl-sb">
+                                <div style={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'center' }}>
+                                    <Space size={20}>
+                                        <Select
+                                            value={activeSpec?.fontSize}
+                                            style={{ width: 60 }} onChange={(e) => { setCompt('fontSize', e) }}
+                                            options={[12, 14, 15, 16, 18, 20, 24, 36].map((item: number) => ({ label: item, value: item }))}
+                                        />
+                                        <Radio.Group onChange={(e) => { setCompt('fontWeight', e.target.value) }} value={activeSpec?.fontWeight}>
+                                            <Radio.Button value={'normal'}>常规</Radio.Button>
+                                            <Radio.Button value={'bold'}>加粗</Radio.Button>
+                                        </Radio.Group>
+                                    </Space>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div className="adui-form-item">
+                        <div className="adui-form-label">颜色</div>
+                        <div className="adui-form-control">
+                            <Space size={10}>
+                                <span>文本</span>
+                                <ColorPicker
+                                    onChange={(_, color) => { setCompt('color', color) }}
+                                    value={activeSpec?.color}
+                                />
+                            </Space>
+                        </div>
+                    </div>
+                    <div className="adui-form-item">
+                        <div className="adui-form-label">对齐方式</div>
+                        <div className="adui-form-control">
+                            <Radio.Group onChange={(e) => { setCompt('textAlign', e.target.value) }} value={activeSpec?.textAlign}>
+                                <Radio.Button value={'left'}><AlignLeftOutlined /></Radio.Button>
+                                <Radio.Button value={'center'}><AlignCenterOutlined /></Radio.Button>
+                                <Radio.Button value={'right'}><AlignRightOutlined /></Radio.Button>
+                                <Radio.Button value={'justify'}><MenuOutlined /></Radio.Button>
+                            </Radio.Group>
+                        </div>
+                    </div>
+                </div>
+                <div className="form section">
+                    <div className="form-caption">边距</div>
+                    <div className="adui-form-item" style={{ alignItems: 'center' }}>
+                        <div className="adui-form-label">上边距</div>
+                        <div className="adui-form-control">
+                            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                                <div style={{ flexGrow: 1 }}><Slider value={activeSpec?.paddingTop} max={50} onChange={(value: number) => { setCompt('paddingTop', value) }} /></div>
+                                <InputNumber min={0} max={50} step={1} value={activeSpec?.paddingTop} onChange={(value: number) => { setCompt('paddingTop', value) }} style={{ width: 80, marginLeft: 20 }} />
+                            </div>
+                        </div>
+                    </div>
+                    <div className="adui-form-item" style={{ alignItems: 'center' }}>
+                        <div className="adui-form-label">下边距</div>
+                        <div className="adui-form-control">
+                            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                                <div style={{ flexGrow: 1 }}><Slider value={activeSpec?.paddingBottom} max={50} onChange={(value: number) => { setCompt('paddingBottom', value) }} /></div>
+                                <InputNumber min={0} max={50} step={1} value={activeSpec?.paddingBottom} onChange={(value: number) => { setCompt('paddingBottom', value) }} style={{ width: 80, marginLeft: 20 }} />
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div> : activeSpec?.elementType === 'IMAGE' ? <div className="widget">
+                <div className="caption section">
+                    <div className="caption-title">图片</div>
+                </div>
+                <div className="form section">
+                    <div className="form-caption">素材设置</div>
+                    <div className="adui-form-item" style={{ alignItems: 'flex-start' }}>
+                        <div className="adui-form-label">图片素材</div>
+                        <div className="adui-form-control">
+                            <UploadLoad
+                                type='image'
+                                uploadButton={<div className={`upload-img-item ${activeSpec?.url ? 'upload-img-item_uploaded' : ''}`}>
+                                    {activeSpec?.url ? <div className="upload-img-item-inner" style={{ backgroundImage: `url(${activeSpec?.url ? activeSpec?.url : ""})` }}>
+                                        <div className='upload-img-item-action'>
+                                            <RetweetOutlined />
+                                        </div>
+                                    </div> : <div className="upload-img-item-inner">
+                                        <PlusOutlined />
+                                    </div>}
+                                </div>}
+                                onChange={(value) => {
+                                    setCompt('url', value)
+                                }}
+                            />
+                        </div>
+                    </div>
+                </div>
+                <div className="form section">
+                    <div className="form-caption">边距</div>
+                    <div className="adui-form-item" style={{ alignItems: 'center' }}>
+                        <div className="adui-form-label">上边距</div>
+                        <div className="adui-form-control">
+                            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                                <div style={{ flexGrow: 1 }}><Slider value={activeSpec?.paddingTop} max={50} onChange={(value: number) => { setCompt('paddingTop', value) }} /></div>
+                                <InputNumber min={0} max={50} step={1} value={activeSpec?.paddingTop} onChange={(value: number) => { setCompt('paddingTop', value) }} style={{ width: 80, marginLeft: 20 }} />
+                            </div>
+                        </div>
+                    </div>
+                    <div className="adui-form-item" style={{ alignItems: 'center' }}>
+                        <div className="adui-form-label">下边距</div>
+                        <div className="adui-form-control">
+                            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                                <div style={{ flexGrow: 1 }}><Slider value={activeSpec?.paddingBottom} max={50} onChange={(value: number) => { setCompt('paddingBottom', value) }} /></div>
+                                <InputNumber min={0} max={50} step={1} value={activeSpec?.paddingBottom} onChange={(value: number) => { setCompt('paddingBottom', value) }} style={{ width: 80, marginLeft: 20 }} />
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div> : activeSpec?.elementType === 'FLOAT_BUTTON' ? <div className="widget">
+                <div className="caption section">
+                    <div className="caption-title">悬浮组件</div>
+                </div>
+                <div className="form section">
+                    <div className="form-caption">素材设置</div>
+                    <div className="adui-form-item" style={{ alignItems: 'flex-start' }}>
+                        <div className="adui-form-label">图片素材</div>
+                        <div className="adui-form-control">
+                            <UploadLoad
+                                type='image'
+                                uploadButton={<div className={`upload-img-item ${activeSpec?.qrCodeFloatList?.[0] ? 'upload-img-item_uploaded' : ''}`}>
+                                    {activeSpec?.qrCodeFloatList?.[0] ? <div className="upload-img-item-inner" style={{ backgroundImage: `url(${activeSpec?.qrCodeFloatList?.[0] ? activeSpec?.qrCodeFloatList?.[0] : ""})` }}>
+                                        <div className='upload-img-item-action'>
+                                            <RetweetOutlined />
+                                        </div>
+                                    </div> : <div className="upload-img-item-inner">
+                                        <PlusOutlined />
+                                    </div>}
+                                </div>}
+                                onChange={(value) => {
+                                    setGlobal('qrCodeFloatList', [value])
+                                }}
+                            />
+                        </div>
+                    </div>
+                </div>
+            </div> : activeSpec?.elementType === "QR_CODE" ? <div className="widget">
+                <div className="caption section">
+                    <div className="caption-title">添加商家微信</div>
+                </div>
+                <div className="form section">
+                    <div className="form-caption">素材设置</div>
+                    <div className="adui-form-item" style={{ alignItems: 'flex-start' }}>
+                        <div className="adui-form-label">图片素材</div>
+                        <div className="adui-form-control">
+                            <UploadLoad
+                                type='image'
+                                uploadButton={<div className={`upload-img-item ${activeSpec?.imageList?.[0] ? 'upload-img-item_uploaded' : ''}`}>
+                                    {activeSpec?.imageList?.[0] ? <div className="upload-img-item-inner" style={{ backgroundImage: `url(${activeSpec?.imageList?.[0] ? activeSpec?.imageList?.[0] : ""})` }}>
+                                        <div className='upload-img-item-action'>
+                                            <RetweetOutlined />
+                                        </div>
+                                    </div> : <div className="upload-img-item-inner">
+                                        <PlusOutlined />
+                                    </div>}
+                                </div>}
+                                onChange={(value) => {
+                                    setCompt('imageList', [value])
+                                }}
+                            />
+                        </div>
+                    </div>
+                </div>
+                <div className="form section">
+                    <div className="form-caption">边距</div>
+                    <div className="adui-form-item" style={{ alignItems: 'center' }}>
+                        <div className="adui-form-label">上边距</div>
+                        <div className="adui-form-control">
+                            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                                <div style={{ flexGrow: 1 }}><Slider value={activeSpec?.paddingTop} max={50} onChange={(value: number) => { setCompt('paddingTop', value) }} /></div>
+                                <InputNumber min={0} max={50} step={1} value={activeSpec?.paddingTop} onChange={(value: number) => { setCompt('paddingTop', value) }} style={{ width: 80, marginLeft: 20 }} />
+                            </div>
+                        </div>
+                    </div>
+                    <div className="adui-form-item" style={{ alignItems: 'center' }}>
+                        <div className="adui-form-label">右边距</div>
+                        <div className="adui-form-control">
+                            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                                <div style={{ flexGrow: 1 }}><Slider value={activeSpec?.paddingRight} max={50} onChange={(value: number) => { setCompt('paddingRight', value) }} /></div>
+                                <InputNumber min={0} max={50} step={1} value={activeSpec?.paddingRight} onChange={(value: number) => { setCompt('paddingRight', value) }} style={{ width: 80, marginLeft: 20 }} />
+                            </div>
+                        </div>
+                    </div>
+                    <div className="adui-form-item" style={{ alignItems: 'center' }}>
+                        <div className="adui-form-label">下边距</div>
+                        <div className="adui-form-control">
+                            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                                <div style={{ flexGrow: 1 }}><Slider value={activeSpec?.paddingBottom} max={50} onChange={(value: number) => { setCompt('paddingBottom', value) }} /></div>
+                                <InputNumber min={0} max={50} step={1} value={activeSpec?.paddingBottom} onChange={(value: number) => { setCompt('paddingBottom', value) }} style={{ width: 80, marginLeft: 20 }} />
+                            </div>
+                        </div>
+                    </div>
+                    <div className="adui-form-item" style={{ alignItems: 'center' }}>
+                        <div className="adui-form-label">左边距</div>
+                        <div className="adui-form-control">
+                            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                                <div style={{ flexGrow: 1 }}><Slider value={activeSpec?.paddingLeft} max={50} onChange={(value: number) => { setCompt('paddingLeft', value) }} /></div>
+                                <InputNumber min={0} max={50} step={1} value={activeSpec?.paddingLeft} onChange={(value: number) => { setCompt('paddingLeft', value) }} style={{ width: 80, marginLeft: 20 }} />
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div> : null}
+        </Col>
+    }
+    return <Col flex="380px" className={style.left}></Col>
 };
 
 export default React.memo(Right);

+ 168 - 0
src/pages/weComTask/page/miniProgramPages/submit.tsx

@@ -0,0 +1,168 @@
+import { App, Button, Dropdown, Form, Input, message, Modal, Radio, Select } from "antd"
+import React, { useEffect, useState } from "react"
+import UploadImg from "../../components/materialMould/uploadImg"
+import UploadVideo from "../../components/materialMould/uploadVideo"
+import { CloseCircleOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons';
+import { useAjax } from "@/Hook/useAjax";
+import { createLandingPageApi, editLandingPageApi } from "../../API/miniProgramPages";
+
+interface Props {
+    groupList: { label: string, value: number }[]
+    corpList: { corpName: string, corpId: string, corpUserList: { corpUserName: string, corpUserId: string }[] }[]
+    pageSpecs: TASK_MINI_PAGE_CREATE.PageSpecsProps
+    visible?: boolean
+    onChange?: () => void
+    onClose?: () => void
+    initialValues?: any
+}
+
+const Submit: React.FC<Props> = ({ corpList, visible, pageSpecs, onChange, onClose, groupList, initialValues }) => {
+
+    /***********************************/
+    const { message } = App.useApp();
+    const [form] = Form.useForm();
+    const corpId = Form.useWatch('corpId', form)
+    const [corpUserList, setCorpUserList] = useState<{ corpUserName: string, corpUserId: string }[]>([])
+
+    const createLandingPage = useAjax((params) => createLandingPageApi(params))
+    const editLandingPage = useAjax((params) => editLandingPageApi(params))
+    /***********************************/
+
+    useEffect(() => {
+        if (corpId) {
+            setCorpUserList(corpList.find(item => item.corpId === corpId)?.corpUserList || [])
+        } else {
+            setCorpUserList([])
+        }
+    }, [corpList, corpId])
+
+    const handleOk = () => {
+        form.validateFields().then(valid => {
+            const { bgColor, globalElementsSpecList, pageElementsSpecList } = pageSpecs
+            let pageSpecsList = []
+            let qrCodeFloatList: { siteId: number, urlList: string[] }[] = []
+            if (globalElementsSpecList?.length > 0) {
+                pageSpecsList = [...pageElementsSpecList.map(item => {
+                    delete (item as any)?.comptActive
+                    return item
+                }), ...globalElementsSpecList.map(item => {
+                    delete item.comptActive
+                    return item
+                })]
+                const floatButton = globalElementsSpecList?.find(item => item.elementType === 'FLOAT_BUTTON')
+                if (floatButton) {
+                    qrCodeFloatList = [{
+                        siteId: floatButton.id,
+                        urlList: floatButton.qrCodeFloatList
+                    }]
+                }
+            }
+            const qrCodeList = pageElementsSpecList.filter(item => item.elementType === 'QR_CODE')?.map((item: TASK_MINI_PAGE_CREATE.QrCode) => ({ siteId: item.id, urlList: item.imageList }))
+            const { pageName, ...v } = valid
+            const params = {
+                content: JSON.stringify({
+                    pageName,
+                    bgColor,
+                    elementsSpecList: pageSpecsList
+                }),
+                qrCodeFloatList,
+                qrCodeList,
+                ...v
+            }
+            if (initialValues?.id) {
+                editLandingPage.run({ ...params, id: initialValues.id }).then(res => {
+                    if (res?.data) {
+                        message.success('修改成功')
+                        onChange?.()
+                    }
+                })
+            } else {
+                createLandingPage.run(params).then(res => {
+                    if (res?.data) {
+                        message.success('新增成功')
+                        onChange?.()
+                    }
+                })
+            }
+        }).catch(err => { })
+    }
+
+    return <Modal
+        title={<strong>落地页基础配置</strong>}
+        open={visible}
+        onCancel={onClose}
+        onOk={handleOk}
+        width={750}
+        confirmLoading={createLandingPage?.loading || editLandingPage?.loading}
+        maskClosable={false}
+    >
+        <Form
+            form={form}
+            name="newPageLink"
+            labelAlign='left'
+            labelCol={{ span: 5 }}
+            colon={false}
+            scrollToFirstError={{
+                behavior: 'smooth',
+                block: 'center'
+            }}
+            onFinishFailed={({ errorFields }) => {
+                message.error(errorFields?.[0]?.errors?.[0])
+            }}
+            onFinish={handleOk}
+            initialValues={initialValues}
+        >
+            <Form.Item label={<strong>落地页名称</strong>} name="name" rules={[{ required: true, message: '请输入落地页名称!' }]}>
+                <Input placeholder="请输入落地页名称" />
+            </Form.Item>
+
+            <Form.Item label={<strong>落地页标题</strong>} name="pageName" rules={[{ required: true, message: '请输入落地页标题!' }]}>
+                <Input placeholder="请输入落地页标题" />
+            </Form.Item>
+
+            <Form.Item label={<strong>落地页备注</strong>} name="remark">
+                <Input placeholder="请输入落地页备注" />
+            </Form.Item>
+            <Form.Item label={<strong>企微主体</strong>} name="corpId" rules={[{ required: true, message: '请选择主体!' }]}>
+                <Select
+                    placeholder='请选择主体'
+                    allowClear
+                    options={corpList.map(item => ({ label: item.corpName, value: item.corpId }))}
+                    showSearch
+                    filterOption={(input, option) =>
+                        (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+                    }
+                />
+            </Form.Item>
+            {corpId && <Form.Item label={<strong>企微客服号</strong>} name="corpUserIdList" rules={[{ required: true, message: '请选择企微客服号!' }]}>
+                <Select
+                    placeholder='请选择企微客服号'
+                    allowClear
+                    options={corpUserList.map(item => ({ label: item.corpUserName, value: item.corpUserId }))}
+                    showSearch
+                    filterOption={(input, option) =>
+                        (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+                    }
+                    mode="multiple"
+                />
+            </Form.Item>}
+            <Form.Item label={<strong>书名</strong>} name="bookName">
+                <Input placeholder="请输入书名名称" />
+            </Form.Item>
+            <Form.Item label={<strong>项目组</strong>} name="projectGroupIdList" rules={[{ required: true, message: '请选择项目组!' }]}>
+                <Select
+                    placeholder='请选择项目组'
+                    allowClear
+                    options={groupList}
+                    showSearch
+                    mode="multiple"
+                    filterOption={(input, option) =>
+                        (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+                    }
+                />
+            </Form.Item>
+        </Form>
+    </Modal>
+}
+
+export default React.memo(Submit)

+ 55 - 2
src/pages/weComTask/page/miniProgramPages/tableConfig.tsx

@@ -1,9 +1,10 @@
 import { ColumnsType } from "antd/es/table"
 import { Flex, Popconfirm, Image } from "antd"
+import { copy } from "@/utils/utils"
 
 
 export function TableConfig(
-    handleEdit?: (d: Record<string, any>, isCopy?: boolean) => void, 
+    handleEdit?: (d: Record<string, any>, isCopy?: boolean) => void,
     handleDel?: (data: number[]) => void,
     handleCode?: (id: number) => void
 ): ColumnsType<any> {
@@ -23,6 +24,43 @@ export function TableConfig(
             width: 150,
             ellipsis: true
         },
+        {
+            title: '落地页标题',
+            dataIndex: 'pageName',
+            key: 'pageName',
+            width: 150,
+            ellipsis: true,
+            render(_, record) {
+                return JSON.parse(record?.content || '{}')?.pageName || ''
+            },
+        },
+        {
+            title: '企微主体',
+            dataIndex: 'corpName',
+            key: 'corpName',
+            width: 120,
+            align: 'center',
+            ellipsis: true,
+            render: (v: string) => v || '--'
+        },
+        {
+            title: '企微客服号',
+            dataIndex: 'corpUserList',
+            key: 'corpUserList',
+            width: 160,
+            align: 'center',
+            ellipsis: true,
+            render: (v: any[]) => v?.map(item => item.corpUserName)?.join('、') || '--'
+        },
+        {
+            title: '书名',
+            dataIndex: 'bookName',
+            key: 'bookName',
+            width: 150,
+            align: 'center',
+            ellipsis: true,
+            render: (v: string) => v || '--'
+        },
         {
             title: '备注',
             dataIndex: 'remark',
@@ -31,6 +69,21 @@ export function TableConfig(
             ellipsis: true,
             render: (v: string) => v || '--'
         },
+        {
+            title: '小程序AppId',
+            dataIndex: 'appid',
+            key: 'appid',
+            width: 150,
+            align: 'center',
+            render: () => `wxed3542b04192b2ee`
+        },
+        {
+            title: '小程序路径',
+            dataIndex: 'path',
+            key: 'path',
+            width: 200,
+            render: (_, records) => <a onClick={() => copy(`pages/Ldpage/index?pageId=${records.id}`)}>pages/Ldpage/index?pageId={records.id}</a>
+        },
         {
             title: '项目组',
             dataIndex: 'projectGroupIdList',
@@ -85,7 +138,7 @@ export function TableConfig(
             title: '操作',
             dataIndex: 'cz',
             key: 'cz',
-            width: 200,
+            width: 250,
             fixed: 'right',
             render: (_, records) => {
                 return <Flex gap={4}>

+ 27 - 4
src/pages/weComTask/page/miniProgramPages/typings.d.ts

@@ -16,14 +16,17 @@ declare namespace TASK_MINI_PAGE_CREATE {
         pageSpecs: TASK_MINI_PAGE_CREATE.PageSpecsProps
         setPageSpecs: React.Dispatch<React.SetStateAction<TASK_MINI_PAGE_CREATE.PageSpecsProps>>
         installActive: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => void
+        setTopUrl: (url: string) => void
+        setCompt: (key: string, value: any) => void,
+        setGlobal: (key: string, value: any) => void
     }
 
     interface Text extends ActiveType, Padding {
         text: string,
         fontSize: 14 | 15 | 16 | 18 | 20 | 24 | 36,
-        fontColor: string,
-        textAlignment: 0 | 1 | 2,  // 取值 0: left, 1: middle, 2: right. 默认 0
-        fontStyle: 0 | 1,  // 0: 常规, 1: 加粗. 默认 0
+        color: string,
+        textAlign: 'left' | 'center' | 'right' | 'justify',  // 取值 0: left, 1: middle, 2: right. 默认 0
+        fontWeight: 'normal' | 'bold',  // 0: 常规, 1: 加粗. 默认 0
         elementType: 'TEXT'
     }
 
@@ -42,12 +45,32 @@ declare namespace TASK_MINI_PAGE_CREATE {
         elementType: 'IMAGE'
     }
 
+    interface QrCode extends ActiveType, Padding {
+        id: number
+        imageList: string[],
+        elementType: 'QR_CODE',
+        paddingLeft: number,
+        paddingRight: number,
+    }
+
     type Empty = {
         elementType: 'empty'
     }
 
+    interface FloatButton extends ActiveType {
+        title: string,
+        qrCodeFloatList: string[],
+        elementType: 'FLOAT_BUTTON',
+        id: number
+    }
+    type PageElementsSpecListProps = Array<TopImage | TopVideo | Image | Text | QrCode | Empty>
+    type GlobalElementsSpecListProps = Array<FloatButton>
+
     interface PageSpecsProps {
         bgColor: string,
-        pageElementsSpecList: Array<TopImage | TopVideo | Image | Text | Empty>
+        pageElementsSpecList: PageElementsSpecListProps
+        globalElementsSpecList?: GlobalElementsSpecListProps
     }
+
+    type ComptItem = Image | Text | QrCode
 }

+ 1 - 0
src/public/svg/wxAutoSvg.svg

@@ -0,0 +1 @@
+<svg width="28" height="28" viewBox="0 0 28 28"><defs><linearGradient x1="50%" y1="99.707%" x2="50%" y2="5.583%" id="a"><stop stop-color="#999" offset="0%"></stop><stop stop-color="#737373" offset="100%"></stop></linearGradient></defs><g fill="none" fill-rule="evenodd"><path d="M6.5 7h15c1.38 0 2.5 1.343 2.5 3v8c0 1.657-1.12 3-2.5 3h-15C5.12 21 4 19.657 4 18v-8c0-1.657 1.12-3 2.5-3z" fill="url(#a)" fill-rule="nonzero"></path><path d="M14 9.5c2.485 0 4.5 1.833 4.5 4.093s-2.015 4.093-4.5 4.093c-.496 0-.974-.073-1.42-.208-.002.01-.434.24-1.296.69a.367.367 0 01-.533-.378l.025-.178c.1-.685.151-1.026.155-1.026-.88-.747-1.431-1.811-1.431-2.993 0-2.26 2.015-4.093 4.5-4.093zM11.75 13a.75.75 0 100 1.5.75.75 0 000-1.5zM14 13a.75.75 0 100 1.5.75.75 0 000-1.5zm2.25 0a.75.75 0 100 1.5.75.75 0 000-1.5z" fill="#FFF"></path></g></svg>

+ 13 - 0
src/utils/utils.ts

@@ -407,4 +407,17 @@ export const removeEmptyValues = (obj: { [x: string]: any }) => {
     }
   }
   return obj;
+}
+
+/**
+ * 替换特殊字符
+ * @param text 
+ * @returns 
+ */
+export const replaceSpecialTxt = (text: string | number | null | undefined) => {
+    if (text) {
+        return text.toString().replace(/[<>]/ig, '')
+    } else {
+        return text
+    }
 }