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

+ 3 - 2
src/layout/index.tsx

@@ -1,6 +1,6 @@
 import { SetStateAction, useCallback, useEffect, useState } from 'react'
 import { Layout, Menu, Space, ConfigProvider, Watermark, App, message } from 'antd';
-import { DesktopOutlined, MessageOutlined, SendOutlined, CloudOutlined, TeamOutlined, HomeOutlined, PaperClipOutlined, ContainerOutlined, AlipayCircleOutlined, StopOutlined, QrcodeOutlined, DatabaseOutlined, ReadOutlined, MobileOutlined, FundViewOutlined, RadarChartOutlined, BarChartOutlined, WechatOutlined, BookOutlined, FileImageOutlined, EyeOutlined, UserOutlined, AlertOutlined, FieldTimeOutlined, ApiOutlined } from '@ant-design/icons';
+import { DesktopOutlined, MessageOutlined, SendOutlined, CloudOutlined, TeamOutlined, HomeOutlined, PaperClipOutlined, ContainerOutlined, AlipayCircleOutlined, StopOutlined, QrcodeOutlined, DatabaseOutlined, ReadOutlined, MobileOutlined, FundViewOutlined, RadarChartOutlined, BarChartOutlined, WechatOutlined, BookOutlined, FileImageOutlined, EyeOutlined, UserOutlined, AlertOutlined, FieldTimeOutlined, ApiOutlined, SlackOutlined } from '@ant-design/icons';
 import { ReactComponent as LaunchSvg } from '../public/svg/launch.svg'
 import { ReactComponent as AdLaunchSvg } from '../public/svg/adLaunch.svg'
 import { ReactComponent as MaterialSvg } from '../public/svg/material.svg'
@@ -97,7 +97,8 @@ const IconMap = {
     eye: <EyeOutlined />,
     alert: <AlertOutlined />,
     log: <ContainerOutlined />,
-    apiOutlined: <ApiOutlined />
+    apiOutlined: <ApiOutlined />,
+    miniprogram: <SlackOutlined />
 };
 
 export const DispatchContext = React.createContext<{ config: any } | null>(null);

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

@@ -0,0 +1,75 @@
+import request from "@/utils/request";
+const { api } = process.env.CONFIG;
+
+
+export interface CreateLandingPageProps {
+    name: string,
+    content: string,
+    qrCodeList: string[],
+    projectGroupIdList: number[],
+    remark?: string
+}
+/**
+ * 新增落地页
+ * @param data 
+ * @returns 
+ */
+export function createLandingPageApi(data: CreateLandingPageProps) {
+    return request({
+        url: api + `/corpOperation/landing/page/create`,
+        method: 'POST',
+        data
+    })
+}
+
+/**
+ * 删除落地页
+ * @param data 
+ * @returns 
+ */
+export function delLandingPageApi(params: { idList: number[] }) {
+    return request({
+        url: api + `/corpOperation/landing/page/delete`,
+        method: 'DELETE',
+        params
+    })
+}
+
+export interface EditLandingPageProps extends CreateLandingPageProps {
+    id: number
+}
+/**
+ * 修改
+ * @param data 
+ * @returns 
+ */
+export function editLandingPageApi(data: EditLandingPageProps) {
+    return request({
+        url: api + `/corpOperation/landing/page/update`,
+        method: 'POST',
+        data
+    })
+}
+
+export interface GetLandingPageListProps {
+    pageNum: number,
+    pageSize: number,
+    name?: string,
+    startTime?: string,
+    endTime?: string,
+    remark?: string,
+    projectGroupIdList?: number[]
+}
+
+/**
+ * 查询落地页列表
+ * @param data 
+ * @returns 
+ */
+export function getLandingPageListApi(data: GetLandingPageListProps) {
+    return request({
+        url: api + `/corpOperation/landing/page/listOfPage`,
+        method: 'POST',
+        data
+    })
+}

+ 67 - 0
src/pages/weComTask/components/materialMould/uploadVideo.tsx

@@ -0,0 +1,67 @@
+import { useAjax } from "@/Hook/useAjax";
+import { message, Upload } from "antd";
+import { RcFile } from "antd/es/upload";
+import React, { useEffect, useState } from "react"
+import { saveMediaApi, upLoadMediaApi } from "../../API/weMaterial/weMaterial";
+import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
+import { useOss } from "@/Hook/useOss";
+import dayjs from 'dayjs'
+
+interface Props {
+    value?: string,
+    onChange?: (data: { value: string, file: RcFile, name: string }) => void
+}
+
+const UploadVideo: React.FC<Props> = ({ value, onChange }) => {
+
+    /********************************/
+    const upLoadMedia = useAjax((params) => upLoadMediaApi(params))
+    const saveMedia = useAjax((params) => saveMediaApi(params))
+    const ossUpload = useOss(true)
+    const [videoUrl, setVideoUrl] = useState<string>('');
+    /********************************/
+
+    useEffect(() => {
+        setVideoUrl(value || '')
+    }, [value])
+
+    const uploadButton = (
+        <div>
+            {upLoadMedia.loading ? <LoadingOutlined /> : <PlusOutlined />}
+            <div style={{ marginTop: 8 }}>上传视频</div>
+        </div>
+    );
+
+    const upload = (file: RcFile) => {
+        let name = dayjs().valueOf().toString()
+        ossUpload.run(file, name).then(res => {
+            if (res?.data) {
+                let [fileName, nameSuffix] = file.name.split('.')
+                saveMedia.run({ fileName: name || fileName, mediaType: 'video', suffix: nameSuffix, url: res?.data })
+                onChange?.(res.data)
+            }
+        })
+    }
+
+    return <>
+        <Upload
+            name="avatar"
+            listType="picture-card"
+            accept='video/mp4'
+            action="#"
+            showUploadList={false}
+            customRequest={() => { }}
+            beforeUpload={(file: RcFile): any => {
+                if (file.size > 10485760) {
+                    message.error('视频大小大于10M,请压缩在上传')
+                    return
+                }
+                upload(file)
+            }}
+        >
+            {videoUrl ? <video src={videoUrl} style={{ width: '100%' }} /> : uploadButton}
+        </Upload>
+    </>
+}
+
+export default React.memo(UploadVideo)

+ 26 - 0
src/pages/weComTask/page/miniProgramPages/drawerMini.tsx

@@ -0,0 +1,26 @@
+import { Drawer } from "antd"
+import React from "react"
+
+interface Props {
+    groupList: { label: string, value: number }[]
+    visible?: boolean
+    onChange?: () => void
+    onClose?: () => void
+    initialValues?: any
+}
+const DrawerMini: React.FC<Props> = ({ visible, onChange, onClose, groupList, initialValues }) => {
+
+
+
+    return <Drawer
+        title={<strong>{initialValues?.id ? initialValues?.isCopy ? '复制落地页' : '修改' + initialValues.name + '落地页' : '新增落地页'}</strong>}
+        closable={{ 'aria-label': 'Close Button' }}
+        onClose={onClose}
+        open={visible}
+        width={'80%'}
+    >
+        
+    </Drawer>
+}
+
+export default React.memo(DrawerMini)

+ 250 - 0
src/pages/weComTask/page/miniProgramPages/index.tsx

@@ -0,0 +1,250 @@
+import { App, Button, Card, DatePicker, Input, Pagination, Popconfirm, Select, Table } from "antd"
+import SearchBox from "../../components/searchBox"
+import { SearchOutlined, PlusOutlined } from '@ant-design/icons';
+import { useEffect, useRef, useState } from "react";
+import style from '../bookLink/index.less'
+import NewMini from "./newMini";
+import { getProjectGroupsAllListApi } from "../../API/groupManage";
+import { useAjax } from "@/Hook/useAjax";
+import dayjs from 'dayjs';
+import { delLandingPageApi, getLandingPageListApi, GetLandingPageListProps } from "../../API/miniProgramPages";
+import { useSize } from "ahooks";
+import { TableConfig } from "./tableConfig";
+import { randomString } from "@/utils/utils";
+
+
+/**
+ * 小程序落地页
+ * @returns 
+ */
+const MiniProgramPages: React.FC = () => {
+
+    /******************************************/
+    const ref = useRef<HTMLDivElement>(null)
+    const size = useSize(ref)
+    const { message } = App.useApp();
+    const [visible, setVisible] = useState<boolean>(false)
+    const [queryParams, setQueryParams] = useState<GetLandingPageListProps>({ pageNum: 1, pageSize: 20 })
+    const [queryParamsNew, setQueryParamsNew] = useState<GetLandingPageListProps>({ pageNum: 1, pageSize: 20 })
+    const [groupList, setGroupList] = useState<{ label: string, value: number }[]>([])
+    const [selectedRows, setselectedRows] = useState<any[]>([])
+    const [initialValues, setInitialValues] = useState<any>()
+
+    const getProjectGroupsAllList = useAjax(() => getProjectGroupsAllListApi())
+    const getLandingPageList = useAjax((params) => getLandingPageListApi(params))
+    const delLandingPage = useAjax((params) => delLandingPageApi(params))
+    /******************************************/
+
+    useEffect(() => {
+        getProjectGroupsAllList.run().then(res => {
+            setGroupList(res?.data?.map(item => ({ label: item.name, value: item.id })) || [])
+        })
+    }, [])
+
+    useEffect(() => {
+        getLandingPageList.run(queryParamsNew)
+    }, [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
+            }
+            return true
+        })
+
+        const newInitialValues = {
+            name,
+            id,
+            projectGroupIdList: projectGroupIdList.map(item => item.projectGroupId),
+            qrCodeFloatList: qrCodeFloatList?.[0]?.urlList || [''],
+            remark,
+            pageSpecsList: {
+                ...pageSpecsList,
+                elementsSpecList
+            },
+            floatButtonSpec,
+            topSpec,
+            isCopy: isCopy || true
+        }
+        console.log(newInitialValues)
+        if (isCopy) {
+            delete newInitialValues.id
+            newInitialValues.name = newInitialValues.name + '_副本' + randomString(true, 3, 5)
+        }
+        setInitialValues(newInitialValues)
+        setVisible(true)
+    }
+
+    const handleDel = (ids: number[]) => {
+        const hide = message.loading('删除中...', 0)
+        delLandingPage.run({ idList: ids.join(',') }).then(res => {
+            hide()
+            if (res?.data) {
+                message.success('删除成功');
+                getLandingPageList.refresh();
+            }
+        }).catch(() => hide())
+    }
+
+    return <Card
+        styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: 'calc(100vh - 88px)' } }}
+    >
+        <div>
+            <SearchBox
+                bodyPadding={`10px 16px 4px`}
+                buttons={<>
+                    <Button type="primary" onClick={() => {
+                        setQueryParamsNew({ ...queryParams, pageNum: 1 })
+                    }} icon={<SearchOutlined />}>搜索</Button>
+                    <Button type="primary" icon={<PlusOutlined />} onClick={() => setVisible(true)}>新增落地页</Button>
+                    <Popconfirm
+                        title="确定删除?"
+                        onConfirm={() => { handleDel?.(selectedRows.map(i => i.id)) }}
+                        disabled={selectedRows?.length === 0}
+                    >
+                        <Button danger disabled={selectedRows?.length === 0}>删除</Button>
+                    </Popconfirm>
+                </>}
+            >
+                <>
+                    <Input
+                        placeholder="落地页名称"
+                        allowClear
+                        value={queryParams?.name}
+                        onChange={(e) => {
+                            setQueryParams({ ...queryParams, name: e.target.value })
+                        }}
+                    />
+                    <Input
+                        placeholder="备注"
+                        allowClear
+                        value={queryParams?.remark}
+                        onChange={(e) => {
+                            setQueryParams({ ...queryParams, remark: e.target.value })
+                        }}
+                    />
+                    <Select
+                        placeholder='请选择项目组'
+                        allowClear
+                        style={{ minWidth: 150 }}
+                        maxTagCount={1}
+                        options={[{ label: '空项目组', value: 0 }, ...groupList || []]}
+                        showSearch
+                        filterOption={(input, option) =>
+                            (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+                        }
+                        mode="multiple"
+                        value={queryParams?.projectGroupIdList}
+                        onChange={(value) => setQueryParams({ ...queryParams, projectGroupIdList: value })}
+                    />
+                    <DatePicker.RangePicker
+                        placeholder={["创建时间开始", "创建时间结束"]}
+                        value={queryParams?.startTime ? [dayjs(queryParams?.startTime), dayjs(queryParams?.endTime)] : undefined}
+                        onChange={(_, options) => {
+                            const newQueryForm = { ...queryParams }
+                            if (options?.[0]) {
+                                newQueryForm.startTime = options?.[0]
+                                newQueryForm.endTime = options?.[1]
+                            } else {
+                                delete newQueryForm?.startTime
+                                delete newQueryForm?.endTime
+                            }
+                            setQueryParams(newQueryForm)
+                        }}
+                    />
+                </>
+            </SearchBox>
+        </div>
+        <div className={style.bookLinkTable} ref={ref}>
+            <Table
+                dataSource={getLandingPageList?.data?.data?.records}
+                columns={TableConfig(handleEdit, handleDel)}
+                bordered
+                pagination={false}
+                rowKey={'id'}
+                size='small'
+                className='resetTable'
+                loading={getLandingPageList?.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 }) => {
+                                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)
+                    }
+                }}
+            />
+        </div>
+        <div className={style.bookLinkPagination}>
+            <Pagination
+                size="small"
+                total={getLandingPageList?.data?.data?.total || 0}
+                showSizeChanger
+                showQuickJumper
+                pageSize={getLandingPageList?.data?.data?.size || 20}
+                current={getLandingPageList?.data?.data?.current || 1}
+                onChange={(page: number, pageSize: number) => {
+                    // ref.current?.scrollTo({ top: 0 })
+                    setTimeout(() => {
+                        setQueryParams({ ...queryParams, pageNum: page, pageSize })
+                        setQueryParamsNew({ ...queryParamsNew, pageNum: page, pageSize })
+                    }, 50)
+                }}
+                showTotal={(total: number) => <span style={{ fontSize: 12 }}>共 {total} 条</span>}
+            />
+        </div>
+
+        {visible && <NewMini
+            initialValues={initialValues}
+            groupList={groupList}
+            visible={visible}
+            onChange={() => {
+                setInitialValues(undefined)
+                setVisible(false)
+                getLandingPageList?.refresh()
+            }}
+            onClose={() => {
+                setInitialValues(undefined)
+                setVisible(false)
+            }}
+        />}
+    </Card>
+}
+
+export default MiniProgramPages

+ 250 - 0
src/pages/weComTask/page/miniProgramPages/newMini.tsx

@@ -0,0 +1,250 @@
+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 }[]
+    visible?: boolean
+    onChange?: () => void
+    onClose?: () => void
+    initialValues?: any
+}
+
+const NewMini: React.FC<Props> = ({ 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 createLandingPage = useAjax((params) => createLandingPageApi(params))
+    const editLandingPage = useAjax((params) => editLandingPageApi(params))
+    /***********************************/
+
+    const handleOk = () => {
+        form.validateFields().then(valid => {
+            const siteId = Date.now()
+            const { pageSpecsList, floatButtonSpec, topSpec, qrCodeFloatList, ...v } = valid
+            floatButtonSpec.elementType = 'FLOAT_BUTTON'
+            floatButtonSpec.id = siteId
+            pageSpecsList.elementsSpecList.unshift(topSpec)
+            pageSpecsList.elementsSpecList.push(floatButtonSpec)
+            const qrCodeList = pageSpecsList.elementsSpecList.filter(item => item.elementType === 'QR_CODE')?.map(item => ({ siteId: item.id, urlList: item.imageList }))
+            const params = {
+                content: JSON.stringify(pageSpecsList),
+                qrCodeFloatList: [{
+                    siteId,
+                    urlList: 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>{initialValues?.id ? initialValues?.isCopy ? '复制落地页' : '修改' + initialValues.name + '落地页' : '新增落地页'}</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 || {
+                topSpec: {
+                    elementType: 'TOP_IMAGE',
+                },
+                pageSpecsList: {
+                    elementsSpecList: [{ elementType: 'TEXT' }],
+                    bgColor: '#EAEAEF'
+                },
+                floatButtonSpec: {
+                    elementType: 'FLOAT_BUTTON',
+                    title: '长按添加客服看下一章'
+                },
+                qrCodeFloatList: ['']
+            }}
+        >
+            <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="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.Item label={<strong>顶部素材</strong>} required>
+                <Form.Item name={['topSpec', 'elementType']} rules={[{ required: true, message: '请选择顶部素材类型!' }]}>
+                    <Radio.Group
+                        options={[
+                            { value: 'TOP_IMAGE', label: '图片' },
+                            { value: 'TOP_VIDEO', label: '视频' }
+                        ]}
+                    />
+                </Form.Item>
+                {elementType === 'TOP_IMAGE' ? <Form.Item name={['topSpec', 'url']} rules={[{ required: true, message: '请上传图片!' }]}>
+                    <UploadImg isCropper />
+                </Form.Item> : <Form.Item name={['topSpec', 'url']} rules={[{ required: true, message: '请上传视频!' }]}>
+                    <UploadVideo />
+                </Form.Item>}
+            </Form.Item>
+            <Form.List name={['pageSpecsList', 'elementsSpecList']} >
+                {(fields, { add, remove }) => {
+                    return <>
+                        {fields.map(({ key, name, ...restField }, index) => {
+                            const elementsSpec = elementsSpecList?.[index]
+                            const imageList = elementsSpec?.imageList
+                            return <div key={index} style={{ backgroundColor: 'rgba(0,0,0,0.05)', padding: 10, borderRadius: 8, marginBottom: 10 }}>
+                                <h3 style={{ display: 'flex', justifyContent: 'space-between', margin: 0, paddingLeft: 10, marginBottom: 5 }}>
+                                    <span>{elementsSpec?.elementType === 'QR_CODE' && '客服号二维码图片'}</span>
+                                    {fields?.length > 1 && <Button
+                                        type="dashed"
+                                        danger
+                                        onClick={() => remove(index)}
+                                        icon={<MinusOutlined />}
+                                        size='small'
+                                    >
+                                        移除内容
+                                    </Button>}
+                                </h3>
+                                {elementsSpec?.elementType === 'TEXT' ? <Form.Item
+                                    {...restField}
+                                    label={<strong>文本内容</strong>}
+                                    name={[name, 'text']}
+                                    rules={[{ required: true, message: '请输入文本内容!' }]}
+                                >
+                                    <Input.TextArea placeholder="请输入文案内容" rows={4} />
+                                </Form.Item> : elementsSpec?.elementType === 'QR_CODE' ? <>
+                                    <Form.List name={[name, 'imageList']}>
+                                        {(fields, { add, remove }) => {
+                                            return <div style={{ display: 'flex', flexWrap: 'wrap', padding: '0 5px' }}>
+                                                {fields.map(({ key, name, ...restField }, index) => {
+                                                    return <div key={index} style={{ position: 'relative', margin: '5px 5px 0 5px' }}>
+                                                        <Form.Item
+                                                            {...restField}
+                                                            name={[name]}
+                                                            rules={[{ required: true, message: '请上传企微客服二维码!' }]}
+                                                        >
+                                                            <UploadImg isCropper />
+                                                        </Form.Item>
+                                                        {imageList?.length > 1 && <div style={{ position: 'absolute', top: -10, right: -10 }}><Button icon={<CloseCircleOutlined />} danger onClick={() => remove(index)} type="text" size="small" /></div>}
+                                                    </div>
+                                                })}
+                                                <Form.Item noStyle>
+                                                    <Button type="primary" disabled onClick={() => add()} style={{ width: '100%' }} icon={<PlusOutlined />}>
+                                                        新增客服二维码图片
+                                                    </Button>
+                                                </Form.Item>
+                                            </div>
+                                        }}
+                                    </Form.List>
+                                </> : elementsSpec?.elementType === 'IMAGE' ? <>
+                                    <Form.Item
+                                        {...restField}
+                                        label={<strong>图片内容</strong>}
+                                        name={[name, 'url']}
+                                        rules={[{ required: true, message: '请上传图片!' }]}
+                                    >
+                                        <UploadImg isCropper />
+                                    </Form.Item>
+                                </> : null}
+
+                            </div>
+                        })}
+                        <Form.Item noStyle>
+                            <Dropdown
+                                menu={{
+                                    items: [
+                                        { key: '1', label: '文本', onClick: () => add({ elementType: 'TEXT' }) },
+                                        { key: '2', label: '图片', onClick: () => add({ elementType: 'IMAGE' }) },
+                                        { key: '3', label: '客服号二维码', onClick: () => add({ elementType: 'QR_CODE', id: Date.now(), imageList: [''] }) },
+                                    ]
+                                }}
+                                placement="topLeft"
+                            >
+                                <Button type="dashed" style={{ width: '100%', marginBottom: 20 }} icon={<PlusOutlined />}>
+                                    新增组件
+                                </Button>
+                            </Dropdown>
+                        </Form.Item>
+                    </>
+                }}
+            </Form.List>
+            <Form.Item label={<strong>底部悬浮</strong>} required>
+                <Form.Item name={['floatButtonSpec', 'title']} rules={[{ required: true, message: '请输入悬浮组件标题!' }]}>
+                    <Input placeholder="请输入悬浮组件标题" disabled />
+                </Form.Item>
+                <Form.List name={['qrCodeFloatList']}>
+                    {(fields, { add, remove }) => {
+                        return <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
+                            {fields.map(({ key, name, ...restField }, index) => {
+                                return <div key={index} style={{ position: 'relative' }}>
+                                    <Form.Item
+                                        {...restField}
+                                        name={[name]}
+                                        rules={[{ required: true, message: '请上传企微客服二维码!' }]}
+                                    >
+                                        <UploadImg isCropper />
+                                    </Form.Item>
+                                    {/* <div style={{ position: 'absolute', top: -10, right: -10 }}><Button icon={<CloseCircleOutlined />} danger onClick={() => remove(index)} type="text" size="small" /></div> */}
+                                </div>
+                            })}
+                            {/* <Form.Item noStyle>
+                                <Button type="dashed" onClick={() => add()} style={{ width: '100%' }} icon={<PlusOutlined />}>
+                                    新增客服二维码
+                                </Button>
+                            </Form.Item> */}
+                        </div>
+                    }}
+                </Form.List>
+            </Form.Item>
+        </Form>
+    </Modal>
+}
+
+export default React.memo(NewMini)

+ 102 - 0
src/pages/weComTask/page/miniProgramPages/tableConfig.tsx

@@ -0,0 +1,102 @@
+import { ColumnsType } from "antd/es/table"
+import { Flex, Popconfirm, Image } from "antd"
+
+
+export function TableConfig(handleEdit?: (d: Record<string, any>, isCopy?: boolean) => void, handleDel?: (data: number[]) => void): ColumnsType<any> {
+
+    const arr: ColumnsType<any> = [
+        {
+            title: 'ID',
+            dataIndex: 'id',
+            key: 'id',
+            width: 70,
+            align: 'center'
+        },
+        {
+            title: '落地页名称',
+            dataIndex: 'name',
+            key: 'name',
+            width: 150,
+            ellipsis: true
+        },
+        {
+            title: '备注',
+            dataIndex: 'remark',
+            key: 'remark',
+            width: 150,
+            ellipsis: true,
+            render: (v: string) => v || '--'
+        },
+        {
+            title: '项目组',
+            dataIndex: 'projectGroupIdList',
+            key: 'projectGroupIdList',
+            width: 120,
+            align: 'center',
+            ellipsis: true,
+            render: (v: { projectGroupName: string }[]) => v?.map(item => item?.projectGroupName)?.join('、') || '--'
+        },
+        {
+            title: '客服号二维码',
+            dataIndex: 'qrCodeList',
+            key: 'qrCodeList',
+            width: 250,
+            render: (v: { urlList: string[] }[]) => v?.map(item => item?.urlList?.map((item, i) => <Image
+                key={i}
+                height={20}
+                alt="basic"
+                src={item}
+            />)) || '--'
+        },
+        {
+            title: '悬浮客服号二维码',
+            dataIndex: 'qrCodeFloatList',
+            key: 'qrCodeFloatList',
+            width: 250,
+            render: (v: { urlList: string[] }[]) => v?.map(item => item?.urlList?.map((item, i) => <Image
+                key={i}
+                height={20}
+                alt="basic"
+                src={item}
+            />)) || '--'
+        },
+        {
+            title: '创建人',
+            dataIndex: 'createBy',
+            key: 'createBy',
+            align: 'center',
+            width: 70
+        },
+        {
+            title: '创建时间',
+            dataIndex: 'createTime',
+            key: 'createTime',
+            align: 'center',
+            width: 140,
+            render: (value) => {
+                return <span style={{ fontSize: 12 }}>{value}</span>
+            }
+        },
+        {
+            title: '操作',
+            dataIndex: 'cz',
+            key: 'cz',
+            align: 'center',
+            width: 100,
+            fixed: 'right',
+            render: (_, records) => {
+                return <Flex gap={4} justify='center'>
+                    <a onClick={() => handleEdit?.(records, true)}>复制</a>
+                    <a onClick={() => handleEdit?.(records)}>修改</a>
+                    <Popconfirm
+                        title="确定删除?"
+                        onConfirm={() => { handleDel?.([records.id]) }}
+                    >
+                        <a style={{ color: 'red' }}>删除</a>
+                    </Popconfirm>
+                </Flex>
+            }
+        },
+    ]
+    return arr
+}