wjx 2 éve
szülő
commit
2b54c9cf8e

+ 15 - 1
config/routerConfig.ts

@@ -691,7 +691,21 @@ const launchSystem = {
             name: '头条落地页',
             access: 'staticHtml',
             component: './launchSystem/headline/staticHtml',
-        }
+        },
+        {
+            path: '/launchSystem/material',
+            name: '素材管理',
+            access: 'material',
+            icon: 'DatabaseOutlined',
+            routes: [
+                {
+                    name: '本地素材',
+                    path: '/launchSystem/material/cloud',
+                    access: 'cloud',
+                    component: './launchSystem/material/cloud',
+                },
+            ],
+        },
     ]
 }
 /** 企业微信 */

+ 89 - 0
src/components/FileBoxAD/components/fileModal/index.tsx

@@ -0,0 +1,89 @@
+import WxSelect from '@/components/WxSelect'
+import { Input, InputNumber, Modal, Tag } from 'antd'
+import React, { useMemo } from 'react'
+import { useModel } from 'umi'
+/**新建文件夹,修改非图文素材名称 */
+function FileModal(props: { isAll?: boolean }) {
+    const { isAll } = props
+    const { state, set, fileOk, nameOk, offEditFile } = useModel(isAll ? 'useOperating.useBdMedia' : 'useOperating.useBdMediaPup')
+    const { fileVisible, actionItem, fileName, sort, videoDescription, videoTitle } = state
+    const { state: { selectWx }, initSelectWx } = useModel('useOperating.useWxGroupList')//公众号筛选
+    const title = useMemo(() => {
+        if (actionItem?.folder) {
+            return '编辑文件夹'
+        }
+        switch (actionItem?.fileType) {
+            case 'image':
+                return '编辑图片';
+            case 'voice':
+                return '编辑音频';
+            case 'video':
+                return '编辑视频';
+        }
+        return '新建文件夹'
+    }, [actionItem])
+    console.log(state)
+    return <Modal
+        visible={fileVisible}
+        destroyOnClose
+        onOk={(e) => { actionItem ? nameOk(selectWx) : fileOk(e, undefined, selectWx); initSelectWx() }}
+        onCancel={() => { offEditFile(); initSelectWx() }}
+        width={400}
+        title={title}
+        closable={false}
+    >
+        <Tag color='warning'>数值越大越靠前</Tag>
+        <div style={{ display: 'flex', flexFlow: 'row', marginBottom: 10, alignItems: 'center', marginTop: 10 }}>
+            <label style={{ width: '15%' }}>排序:</label>
+            <InputNumber
+                style={{ width: '100%' }}
+                value={sort}
+                placeholder='请输入序号, 数值越大越靠前'
+                onChange={(e: number) => {
+                    set({ sort: e })
+                }} 
+            />
+        </div>
+
+        {
+            actionItem?.fileType === 'video' ? <div>
+                <div style={{ display: 'flex', flexFlow: 'row', marginBottom: 10, alignItems: 'center' }}>
+                    <label style={{ width: '15%' }}>名称:</label>
+                    <Input
+                        value={videoTitle}
+                        allowClear
+                        placeholder='请输入名称'
+                        onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+                            set({ videoTitle: event.target.value })
+                        }}
+                    /></div>
+                <div style={{ display: 'flex', flexFlow: 'row', alignItems: 'center' }}>
+                    <label style={{ width: '15%' }}>描述:</label>
+                    <Input.TextArea
+                        placeholder='请输入描述内容'
+                        allowClear
+                        value={videoDescription}
+                        onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
+                            set({ videoDescription: event.target.value })
+                        }}
+                    />
+                </div>
+                <WxSelect />
+            </div> : <>
+                <div style={{ display: 'flex', flexFlow: 'row', marginBottom: 10, alignItems: 'center' }}>
+                    <label style={{ width: '15%' }}>名称:</label>
+                    <Input
+                        value={fileName}
+                        allowClear
+                        placeholder='请输入名称'
+                        onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+                            set({ fileName: event.target.value })
+                        }}
+                    />
+                </div>
+                <WxSelect />
+            </>
+        }
+    </Modal>
+}
+export default React.memo(FileModal)

+ 47 - 0
src/components/FileBoxAD/components/fileModal/sortModal.tsx

@@ -0,0 +1,47 @@
+import { InputNumber, Modal, Tag } from 'antd'
+import React, { useEffect, useState } from 'react'
+import { useModel } from 'umi'
+
+
+interface Props {
+    isAll?: boolean
+}
+function SortModal(props: Props) {
+    const { isAll } = props
+    const { state, offEditFile, configSort, list, set } = useModel(isAll ? 'useOperating.useBdMedia' : 'useOperating.useBdMediaPup')
+    const { sortVisible, actionItem } = state
+    const [sort, setSort] = useState<number>(0)
+
+    useEffect(() => {
+        setSort(actionItem?.sort || 0)
+    }, [actionItem?.sort])
+
+    const sortHandle = () => {
+        configSort.run({ sysMediaId: actionItem?.id, sort: sort }).then(res => {
+            if (res) {
+                list?.refresh()
+                set({ sortVisible: false, actionItem: null })
+            }
+        })
+    }
+
+    return <Modal
+        visible={sortVisible}
+        width={400}
+        title={'编辑排序'}
+        closable={false}
+        onOk={sortHandle}
+        onCancel={() => { offEditFile() }}
+    >
+        <Tag color='warning'>数值越大越靠前</Tag>
+        <InputNumber
+            style={{ width: '100%', marginTop: 10 }}
+            value={sort}
+            placeholder='请输入序号, 数值越大越靠前'
+            onChange={(e: number) => {
+                setSort(e)
+            }} 
+        />
+    </Modal>
+}
+export default SortModal

+ 120 - 0
src/components/FileBoxAD/components/imgModal/index.tsx

@@ -0,0 +1,120 @@
+import React, { useCallback, useState } from 'react'
+import { Modal } from 'antd'
+import FormItem from '@/components/Formitem'
+import { FormConfig } from '@/components/Formitem/type'
+import { useModel } from 'umi'
+import WxSelect from '@/components/WxSelect'
+/**新建非图文素材 */
+let ImgModal = React.memo((props: { isAll?: boolean }) => {
+    const { isAll } = props
+    const [fromsubmit, setFromsubmit] = useState<() => Promise<any>>()//存放from提交事件
+    const callback = useCallback((fnc: () => Promise<any>) => { setFromsubmit(fnc) }, [])//回调获取from提交事件
+    const { state, typeEnum, fileOk, offEditFile } = useModel(isAll ? 'useOperating.useBdMedia' : 'useOperating.useBdMediaPup')
+    const { imgVisrible, fileType, actionItem } = state
+    const { state: { selectWx }, initSelectWx} = useModel('useOperating.useWxGroupList')//公众号筛选
+    return <Modal
+        title={(actionItem ? '编辑' : '新建') + typeEnum[fileType as string]}
+        visible={imgVisrible}
+        onOk={(e) => { fileOk(e, fromsubmit, selectWx);initSelectWx() }}
+        onCancel={()=>{
+            offEditFile();
+            initSelectWx()
+        }}
+        destroyOnClose
+        maskClosable={false}
+    >
+        <FormItem
+            formConfig={config(typeEnum[fileType as string])}
+            flow='column'
+            getFormSubmit={callback}
+            defaultProps={{ sort: 0 }}
+            isAll={isAll}
+        />
+        <WxSelect />
+    </Modal>
+})
+export default ImgModal
+let config: (name: string,) => FormConfig[] = (name) => {
+    let videoArr: FormConfig[] = [
+        {
+            label: name + '名称',
+            tag: 'input',
+            name: 'videoTitle',
+            width: 200,
+        },
+        
+        {
+            label: name + '描述',
+            tag: 'TextArea',
+            name: 'videoDescription',
+            width: 300,
+        },
+        {
+            label: '排序',
+            tag: 'inputNumber',
+            name: 'sort',
+            width: 200,
+        },
+        {
+            label: '上传' + name,
+            tag: 'inputFile',
+            name: 'file',
+            required: true,
+            message: '请上传文件'
+        }
+    ]
+    let imgArr: FormConfig[] = [
+        {
+            label: name + '名称',
+            tag: 'input',
+            name: 'fileName',
+            width: 200,
+        },
+        {
+            label: '排序',
+            tag: 'inputNumber',
+            name: 'sort',
+            width: 200,
+        },
+        {
+            label: '上传' + name,
+            tag: 'imgCrop',
+            name: 'file',
+            required: true,
+            message: '请上传文件'
+        }
+    ]
+    let arr: FormConfig[] = [
+        {
+            label: name + '名称',
+            tag: 'input',
+            name: 'fileName',
+            width: 200,
+        },
+        {
+            label: '排序',
+            tag: 'inputNumber',
+            name: 'sort',
+            width: 200,
+        },
+        {
+            label: '上传' + name,
+            tag: 'inputFile',
+            name: 'file',
+            required: true,
+            message: '请上传文件'
+        }
+    ]
+    if (name === '编辑') {
+        return [
+            {
+                label: name + '名称',
+                tag: 'input',
+                name: 'fileName',
+                width: 200
+            },
+        ]
+    }
+    return name === '视频' ? videoArr : name === '图片' ? imgArr : arr
+}
+

+ 211 - 0
src/components/FileBoxAD/components/newsModal/index.less

@@ -0,0 +1,211 @@
+.left {
+  width: 100%;
+  .left_top {
+    display: flex;
+    justify-content: left;
+    align-items: center;
+    padding: 10px;
+    border-bottom: 1px solid #efefef;
+    font-weight: 500;
+  }
+  .left_ul {
+    height: calc(100vh - 150px);
+    overflow-y: auto;
+    > ul {
+      display: flex;
+      flex-flow: column;
+      padding: 10px;
+      overflow-y: auto;
+      li {
+        position: relative;
+        border: 1px solid #efefef;
+        border-bottom: 0;
+        > p {
+          display: flex;
+          padding: 15px;
+          flex-flow: row;
+          justify-content: space-between;
+          margin-bottom: 0;
+          img {
+            width: 60px;
+            height: 60px;
+            border-radius: 3px;
+            object-fit: cover;
+          }
+        }
+        > div {
+          opacity: 0;
+          display: flex;
+          justify-content: space-between;
+          padding: 0 10px;
+          align-items: center;
+          color: #fff;
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          width: 100%;
+          z-index: 99;
+          height: 30px;
+          background: rgba(0, 0, 0, 0.6);
+          > span {
+            cursor: pointer;
+            > span {
+              margin-right: 10px;
+            }
+          }
+        }
+      }
+      li.last {
+        border-bottom: 1px solid #efefef;
+        > div {
+          > span {
+            > span {
+              &:nth-child(2) {
+                display: none;
+              }
+            }
+          }
+        }
+      }
+      li.first {
+        border: 0;
+        > span {
+          position: absolute;
+          bottom: 0;
+          left: 0;
+          right: 0;
+          height: 30px;
+          background: rgba(0, 0, 0, 0.6);
+          color: #fff;
+          line-height: 30px;
+          padding-left: 10px;
+        }
+        > div {
+          > span {
+            > span {
+              &:nth-child(1) {
+                display: none;
+              }
+            }
+          }
+        }
+        > p {
+          position: relative;
+          width: 100%;
+          height: 0px;
+          padding-top: 40%;
+          border-radius: 2px;
+          margin-bottom: 0;
+          img {
+            position: absolute;
+            left: 0px;
+            top: 0px;
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+        }
+      }
+      li:hover {
+        > div {
+          opacity: 1;
+        }
+      }
+      li.action {
+        border: 1px solid rgb(59, 116, 255);
+      }
+    }
+    > span {
+      height: 50px;
+      border: 1px dashed #bbbbbb;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      margin: 0 10px;
+    }
+    > span:hover {
+      border-color: rgb(59, 116, 255);
+      color: rgb(59, 116, 255);
+    }
+  }
+}
+.right_fb {
+  height: calc(100vh - 112px);
+  overflow-y: scroll;
+  > div {
+    width: 100%;
+    display: flex;
+    flex-flow: column;
+  }
+  .upload {
+    border: 1px dashed #d8d8d8;
+    border-radius: 3px;
+    cursor: pointer;
+    margin: 10px 0;
+    position: relative;
+    img {
+      object-fit: cover;
+      width: 100%;
+      height: 100%;
+    }
+    .add {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-flow: column;
+      height: 100%;
+    }
+    .pos {
+      position: absolute;
+      top: 0;
+      left: 0;
+      height: 100%;
+      width: 100%;
+      opacity: 0;
+      color: #fff;
+    }
+    .pos:hover {
+      opacity: 1;
+      background: rgba(0, 0, 0, 0.6);
+    }
+  }
+  .min {
+    width: 88px;
+    height: 88px;
+  }
+  .max {
+    width: 220px;
+    height: 95px;
+  }
+  .right_textArea {
+    span:last-child {
+      float: right;
+      text-align: right;
+    }
+  }
+}
+.box {
+  border: 1px solid #efefef;
+  height: 100%;
+  > div {
+    &:nth-child(1) {
+      border-right: 1px solid #efefef;
+    }
+    &:last-child {
+      padding: 0 10px;
+      border-left: 1px solid #efefef;
+    }
+  }
+}
+.title1 {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 200px;
+}
+.title {
+  width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}

+ 54 - 0
src/components/FileBoxAD/components/newsModal/index.tsx

@@ -0,0 +1,54 @@
+import React, { useImperativeHandle, useState } from 'react'
+import { Col, Drawer, Row } from 'antd'
+import style from './index.less'
+import Left from './left'
+import Right from './right'
+import Editor from '@/components/Editor/Box'
+import { useModel } from 'umi'
+//isWx是否是微信用来判断调用不同的接口逻辑默认不是
+const DrawerPage = React.forwardRef((props: any, ref) => {
+    const { styleRight = '', isAll = true, callback = null } = props
+    const [visible, setVisible] = useState<boolean>(false)//抽屉
+    const [isEdit, setIsEdit] = useState<boolean>(false)//是否编辑
+    const [editId, setEditId] = useState<string>('')//编辑的ID
+    const { drawerDispatch, actionData } = useModel('useOperating.useMaterialDrawer', model => ({ drawerDispatch: model.dispatch, actionData: model.state.actionData }))
+    //映射ref方法
+    useImperativeHandle(ref, () => ({
+        showDrawer,
+    }))
+    //抽屉显示
+    let showDrawer = (props?: any) => {
+        setEditId(props?.editId || '')
+        setIsEdit(props?.isEdit || false)//设置是否编辑
+        setVisible(true)//开启弹窗
+        localStorage.setItem('collapsed', '1')
+    };
+    //抽屉关闭
+    let onClose = () => {
+        // isEdit ? drawerDispatch({ type: 'initData' }) : drawerDispatch({ type: 'action', params: { actionId: 1 } })//编辑取消后清空数据
+        drawerDispatch({ type: 'initData' })
+        setVisible(false)//关闭抽屉
+        setIsEdit(false)//设回flase以免新建关闭后销毁数据
+        localStorage.setItem('collapsed', '0')
+    };
+    return <Drawer
+        placement="right"
+        closable={false}
+        onClose={onClose}
+        visible={visible}
+        getContainer={false}
+        width='90%'
+        destroyOnClose
+        style={styleRight ? { right: styleRight } : {}}
+    >
+        <Row className={style.box}>
+            <Col flex='1'><Left isWx={props?.isWx} /></Col>
+            <Col flex='3'>{visible && <Editor onClose={onClose} editId={editId} editData={actionData?.dataArr} syncNews={props?.syncNews} isWx={props?.isWx} isShowWx={props?.isShowWx} isAll={isAll} callback={callback}/>}</Col>
+            <Col flex='1'><Right isShowWx={props?.isShowWx} /></Col>
+        </Row>
+    </Drawer>
+})
+
+export default DrawerPage
+
+

+ 158 - 0
src/components/FileBoxAD/components/newsModal/left.tsx

@@ -0,0 +1,158 @@
+import React, { useCallback, useState } from 'react'
+import style from './index.less'
+import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'
+import { Avatar, Input, message, Modal, Space } from 'antd'
+import { useModel } from 'umi'
+import { exportMediaByUrl1 } from '@/services/operating/material'
+const Left = React.memo((props: { isWx?: boolean }) => {
+    const { isWx } = props
+    const { drawerState: { dataArr, actionId }, dispatch } = useModel('useOperating.useMaterialDrawer', model => ({ drawerState: model.state, dispatch: model.dispatch }))
+    const { actionWX } = useModel('useOperating.useWxGroupList', model => ({ actionWX: model.state.actionWX }))
+    const { ueditorRef } = useModel('useOperating.useMaterialDrawer', model => ({ ueditorRef: model.state.ueditorRef }))
+    const [showNews, setShowNews] = useState<boolean>(false)//导入文章
+    const [newsUrl, setNewsUrl] = useState<string>('')//文章地址
+
+    //导入文章
+    const handleImportNews = useCallback(() => {
+        let str = newsUrl
+        if (str.indexOf(',') === -1) {
+            str = str.replace(/\s/ig, ',')
+        }
+        let arr = str.split(/[,,]/)
+        let newArr: string[] = []
+        arr.forEach((url: string) => {
+            url = url.replace(/\s*/g, '')
+            if (url !== "" && url) {
+                newArr.push(url)
+            }
+        })
+        let isOk = newArr.every((url: string, index: number) => {
+            if (url.search(/http[s]?:\/\/mp.weixin.qq.com/ig) === 0) {
+                return true
+            } else {
+                message.error(`第${index + 1}的地址错误,请输入正确的文章地址!`)
+            }
+        })
+        if (newArr.length > (9 - actionId)) {
+            message.error('你输入的链接条数超过了当前剩余可新增的条数')
+            return
+        }
+        if (isOk) {
+            let promiseAll: any[] = []
+            newArr.forEach((url: string, index: number) => {
+                // promiseAll.push(exportMediaByUrl1(url).then((res) => { return res.json() }))
+                promiseAll.push(exportMediaByUrl1(encodeURIComponent(url)).then((res) => { return res.json() }))
+            })
+            Promise.all(promiseAll).then((res: any) => {
+                let arr: any[] = []
+                if (res) {
+                    res.forEach((r: { data: any, code: 200 }, index: number) => {
+                        if (r.code === 200) {
+                            let { content, contentSourceUrl, digest, thumbMediaUrl, title } = r.data
+                            contentSourceUrl = contentSourceUrl.replace(/['"]*/g, '')
+                            content = content.replace('data-src', 'src').replace(/\<mpvoice[\s\S]*\<\/mpvoice\>/ig, '')
+                            index === 0 && ueditorRef?.setContent(content, false)
+                            arr.push({ menuId: actionId + index, content, contentSourceUrl, digest, thumbMediaUrl, title })
+                        }
+                    })
+                    arr = arr.sort((a: { menuId: number }, b: { menuId: number }) => { return a.menuId - b.menuId })
+                    dispatch({ type: 'importNews', params: { data: { dataArrs: arr, actionId } } })
+                }
+            }).catch(err => { console.log(err) })
+            setShowNews(false)
+        }
+    }, [newsUrl, actionId, ueditorRef])
+    return <div className={style.left}>
+        {
+            isWx && <div className={style.left_top}>
+                <Space>
+                    <Avatar src={actionWX?.headImg || "https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"} />
+                    <span>{actionWX?.nickName}</span>
+                </Space>
+            </div>
+        }
+        <div className={style.left_ul}>
+            <ul>
+                {
+                    dataArr?.map((item: any, index: number) => {
+                        return index === 0
+                            ?
+                            <li
+                                className={`${style.first} ${actionId === item.menuId && style.action}`}
+                                key={item.menuId}
+                                onClick={() => {
+                                    dispatch({ type: 'action', params: { menuId: item.menuId } })
+                                }}
+                            >
+                                <p>
+                                    {/* <img src='https://mmbiz-qpic-cn.weituibao.com/mmbiz_png/o0slOoU0qk1qibibvCYeHB7tCwW6HGia7wntuqFtE7IYfqkp8lsCegVicBdRNQTFgZdQRLctqVIebWqu5EOib5ejHMQ/0?wx_fmt=png&wxb_id=4so7eVwgLV_RCXsMR1e4Y4hgYPtVj_qfNimo_frijeI' /> */}
+                                    <img src={item.thumbMediaUrl || item.thumbUrl || 'https://s.weituibao.com/static/1552098829922/bigfm.png'} alt="" />
+                                </p>
+                                <span className={style.title}>{item.title || '标题'}</span>
+                                <div>
+                                    <span>
+                                        <ArrowUpOutlined onClick={() => dispatch({ type: 'mobile', params: { eq: index, up: true } })} />
+                                        <ArrowDownOutlined onClick={() => dispatch({ type: 'mobile', params: { eq: index } })} />
+                                    </span>
+                                    <DeleteOutlined onClick={() => dispatch({ type: 'delDataArr', params: { menuId: item.menuId } })} />
+                                </div>
+                            </li>
+                            :
+                            <li
+                                className={`${index === dataArr.length - 1 && style.last} ${actionId === item.menuId && style.action}`}
+                                key={item.menuId}
+                                onClick={() => {
+                                    dispatch({ type: 'action', params: { menuId: item.menuId } })
+                                }
+                                }
+                            >
+                                <p>
+                                    <span className={style.title1}>{item.title || '标题'}</span> <img src={item.thumbMediaUrl || item.thumbUrl || 'https://s.weituibao.com/static/1552098829922/bigfm.png'} alt="" />
+                                </p>
+                                <span />
+                                <div>
+                                    <span>
+                                        <ArrowUpOutlined onClick={() => dispatch({ type: 'mobile', params: { eq: index, up: true } })} />
+                                        <ArrowDownOutlined onClick={() => dispatch({ type: 'mobile', params: { eq: index } })} />
+                                    </span>
+                                    <DeleteOutlined onClick={() => dispatch({ type: 'delDataArr', params: { menuId: item.menuId } })} />
+                                </div>
+                            </li>
+                    })
+                }
+            </ul>
+            {
+                dataArr?.length < 8 && <span onClick={() => dispatch({ type: 'addDataArr' })} ><PlusOutlined />新建</span>
+            }
+            <span onClick={() => { setShowNews(true) }} style={{ marginTop: 10 }}><PlusOutlined />导入文章</span>
+            <Modal
+                visible={showNews}
+                title='导入文章'
+                onCancel={() => { setShowNews(false) }}
+                onOk={handleImportNews}
+                destroyOnClose
+            >
+                <Input.TextArea
+                    placeholder={
+                        ` 请填写文章url地址,支持批量,批量传入需要在每个链接后面加上逗号或者换行,从你当前选中的篇数开始导入替换,一次只能导入8篇,输入的链接地址不能超过可新建的篇数,假如你从第5篇开始导入那只能导入4篇
+                        
+方式1:支持末尾加,号分割导入                     https://mp.weixin.qq.com/s/C4qcDnPwfAlf4Y2P9qeThA,https://mp.weixin.qq.com/s/tVGyg6rxR6BoppjqIbKoGg
+方式2:支持换行分割导入
+https://mp.weixin.qq.com/s/C4qcDnPwfAlf4Y2P9qeThA
+https://mp.weixin.qq.com/s/tVGyg6rxR6BoppjqIbKoGg
+`
+                    }
+                    rows={10}
+                    onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
+                        let url = e.target.value
+                        if (url) {
+                            setNewsUrl(url)
+                        }
+                    }}
+
+                />
+            </Modal>
+        </div>
+    </div>
+})
+export default Left

+ 130 - 0
src/components/FileBoxAD/components/newsModal/right.tsx

@@ -0,0 +1,130 @@
+import React, { useCallback, useMemo, useState } from 'react'
+import { Divider, Input, InputNumber, Tabs, Tag } from 'antd'
+import style from './index.less'
+import { PlusOutlined } from '@ant-design/icons';
+import SmMaterialModal from '@/components/MaterialModal/smModalBox';
+import { useModel } from 'umi';
+import WxSelect from '@/components/WxSelect';
+const { TabPane } = Tabs;
+
+let Right = React.memo((props: any) => {
+    const { drawerDispatch, drawerState: { actionId, dataArr, sort } } = useModel('useOperating.useMaterialDrawer', model => ({
+        drawerDispatch: model.dispatch,
+        drawerState: model.state
+    }))
+    const { show, hide, state: { visible } } = useModel('useOperating.useMaterialModal')
+    const [digest, setDigest] = useState<string>('') //摘要
+    const [authorStr, setAuthor] = useState<string>('') //作者
+    const [contentSourceUrlStr, setContentSourceUrl] = useState<string>('') //原文链接
+
+    //点击上传打开弹窗获取数据
+    let upload = useCallback(() => {
+        show()
+    }, [])
+    //跟随选择更新图片
+    const defaultValue: any = useMemo(() => {
+        let obj = {}
+        dataArr.forEach((item: any) => {
+            if (item.menuId === actionId) {
+                setDigest(item.digest || '')
+                setAuthor(item.author || '')
+                setContentSourceUrl(item.contentSourceUrl || '')
+                obj = item
+            }
+        })
+        return obj
+    }, [dataArr, actionId, sort])
+    //字数并插入dataArr
+    let strLeng = useCallback((v: React.ChangeEvent<HTMLTextAreaElement>) => {
+        drawerDispatch({ type: 'pushData', params: { menuId: actionId, digest: v.target.value, } })
+        setDigest(v.target.value)
+    }, [actionId])
+    //作者插入
+    let author = useCallback((v: React.ChangeEvent<HTMLInputElement>) => {
+        drawerDispatch({ type: 'pushData', params: { menuId: actionId, author: v.target.value, } })
+        setAuthor(v.target.value)
+    }, [actionId])
+    //原文链接插入
+    let contentSourceUrl = useCallback((v: React.ChangeEvent<HTMLTextAreaElement>) => {
+        drawerDispatch({ type: 'pushData', params: { menuId: actionId, contentSourceUrl: v.target.value } })
+        setContentSourceUrl(v.target.value)
+    }, [actionId])
+    return <>
+        <Tabs type="line">
+            <TabPane tab="发布" key="1" className={style.right_fb}>
+                <div className={style.right_img}>
+                    {
+                        actionId === 1 ? <span>封面(建议尺寸900*500像素)</span> : <span>封面(建议尺寸200*200像素)</span>
+                    }
+                    <span
+                        className={`${style.upload} ${actionId === 1 ? style.max : style.min}`}
+                        onClick={upload}
+                    >
+                        {
+                            (defaultValue?.thumbMediaUrl || defaultValue?.thumbUrl) && <img src={defaultValue?.thumbMediaUrl || defaultValue?.thumbUrl} alt="" />
+                        }
+                        <span className={`${style.add} ${(defaultValue?.thumbMediaUrl || defaultValue?.thumbUrl) && style.pos}`}>
+                            <PlusOutlined style={{ fontSize: 16 }} />
+                            上传封面
+                        </span>
+                    </span>
+                    {/* <span><Switch checkedChildren="开启" unCheckedChildren="关闭" defaultChecked={false} />封面显示在正文</span> */}
+                </div>
+                <Divider dashed />
+                <div className={style.right_textArea}>
+                    <span>摘要(选填)</span>
+                    <Input.TextArea rows={5} allowClear maxLength={120} onChange={strLeng} value={digest} />
+                    <span>{digest.length}/120</span>
+                </div>
+                <Divider dashed />
+                <div className={style.right_author}>
+                    <span>作者(选填)</span>
+                    <Input allowClear onChange={author} value={authorStr} />
+                </div>
+                <Divider dashed />
+                <div className={style.right_src}>
+                    <span>原文链接(必填)</span>
+                    <Input.TextArea rows={2} placeholder='请勿添加其他公众号的主页链接' allowClear onChange={contentSourceUrl} value={contentSourceUrlStr} />
+                </div>
+                <div className={style.right_src} style={{ marginTop: 10 }}>
+                    <span>排序(必填)<Tag color='warning'>数值越大越靠前</Tag></span>
+                    <InputNumber
+                        style={{ width: '100%' }}
+                        value={sort}
+                        placeholder='请输入序号, 数值越大越靠前'
+                        onChange={(e: number) => {
+                            drawerDispatch({ type: 'setSort', params: { sort: e } })
+                        }}
+                    />
+                </div>
+                <div>
+                    <WxSelect />
+                </div>
+            </TabPane>
+        </Tabs>
+        {
+            visible && <SmMaterialModal
+                visible={visible}
+                onCancel={hide}
+                title='选择图片'
+                onOk={(props: any) => {
+                    console.log(props)
+                    let obj: any = {}
+                    if (props?.mediaId) {
+                        obj.wxMediaId = props?.id
+                        obj.thumbUrl = props?.url
+                    } else {
+                        obj.thumbUrl = props?.url
+                    }
+                    drawerDispatch({ type: 'pushData', params: { menuId: actionId, thumbMediaUrl: props.url, ...obj } })
+                    hide()
+                }}
+                mediaType={1}
+                isShowWx={props?.isShowWx}
+                isAllData
+                noFile={false}
+            />
+        }
+    </>
+})
+export default Right

+ 106 - 0
src/components/FileBoxAD/components/syncModal/index.less

@@ -0,0 +1,106 @@
+.ico {
+  -webkit-animation: Tada 3s both infinite;
+  -moz-animation: Tada 3s both infinite;
+  -ms-animation: Tada 3s both infinite;
+  animation: Tada 3s both infinite;
+}
+.colud {
+  display: flex;
+  flex-flow: row nowrap;
+  > div:nth-child(2) {
+    flex: 1;
+  }
+  span.ant-checkbox {
+    display: none;
+  }
+}
+.box {
+  > span {
+    &:first-child {
+      display: none;
+    }
+    &:last-child{
+      display: inline-block;
+    }
+  }
+}
+.card_title{
+  font-size: 12px;
+  color: #999;
+  display: block;
+}
+.card_action {
+  position: relative;
+  margin: 0 auto;
+  // text-align: center;
+  color: #4abe84;
+  background-color: #fff;
+  box-shadow: 0px 2px 7px 0px rgba(85, 110, 97, 0.35);
+  border-radius: 7px;
+  border: 1px solid rgba(74, 190, 132, 1);
+  overflow: hidden;
+  &::before {
+    content: '';
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    border: 12px solid #4abe84;
+    border-top-color: transparent;
+    border-left-color: transparent;
+  }
+  &::after {
+    content: '';
+    width: 5px;
+    height: 10px;
+    position: absolute;
+    right: 5px;
+    bottom: 3px;
+    border: 2px solid #fff;
+    border-top-color: transparent;
+    border-left-color: transparent;
+    transform: rotate(45deg);
+  }
+}
+@keyframes Tada {
+  0% {
+    transform: scale(1);
+    transform: scale(1);
+  }
+
+  70%,
+  73% {
+    transform: scale(0.9) rotate(-3deg);
+    transform: scale(0.9) rotate(-3deg);
+  }
+
+  77%,
+  83%,
+  90%,
+  97% {
+    transform: scale(1.1) rotate(3deg);
+    transform: scale(1.1) rotate(3deg);
+  }
+
+  80%,
+  87%,
+  93% {
+    transform: scale(1.1) rotate(-3deg);
+    transform: scale(1.1) rotate(-3deg);
+  }
+
+  100% {
+    transform: scale(1) rotate(0);
+    transform: scale(1) rotate(0);
+  }
+}
+.card_box {
+  > div {
+    padding: 10px;
+  }
+}
+.item_img {
+  width: 25px;
+  border-radius: 2px;
+  margin-right: 5px;
+  background-color: #efefef;
+}

+ 313 - 0
src/components/FileBoxAD/components/syncModal/index.tsx

@@ -0,0 +1,313 @@
+import { Button, Card, Checkbox, Col, Row, Spin } from 'antd'
+import Modal from 'antd/lib/modal/Modal'
+import React, { useCallback, useEffect, useImperativeHandle, useState } from 'react'
+import { useModel } from 'umi'
+import Left from '@/components/ActionModal/left'
+import style from './index.less'
+import NoFoundPage from '@/pages/empty'
+type Props = {
+}
+let SyncModal = React.forwardRef((props: Props, ref) => {
+    //映射ref
+    useImperativeHandle(ref, () => ({
+        show
+    }))
+    const { mpAccounts, actionWX, dispatch, actionArr, getDataList, getAllOfMember } = useModel('useOperating.useWxGroupList',
+        models => ({
+            mpAccounts: models.state.mpAccounts,
+            actionWX: models.state.actionWX,
+            dispatch: models.dispatch,
+            actionArr: models.state.actionArr,
+            getDataList: models.getDataList,
+            getAllOfMember: models.getAllOfMember
+        }))
+    const { sync } = useModel('useOperating.useBdMedia')
+    const [visible, setVisible] = useState<boolean>(false)//显示隐藏
+    const [groupSyncWx, setGroupSyncWx] = useState<any[]>([])//当前组未选中的微信
+    const [selectState, setSelectState] = useState<{ isAll: boolean, isChecked: boolean }>({ isAll: false, isChecked: false })
+    const [sysMediaId, setSysMediaId] = useState<any>()
+
+    //全选
+    let allSelect = useCallback(() => {
+        let isGroupWx: any[] = []//选中的数据
+        let newActionArr: any[] = []
+        let fiterMp: any[] = []
+        mpAccounts?.forEach((mp: { id: number, verifyTypeInfo: number }) => {
+            if (mp?.verifyTypeInfo !== -1) {//过滤未认证
+                fiterMp.push(mp)
+            }
+            actionArr?.forEach((wx: { id: number }) => {
+                if (wx.id === mp.id) {
+                    isGroupWx.push(wx)
+                }
+            })
+        })
+        if (isGroupWx?.length === fiterMp?.length || isGroupWx?.length === fiterMp?.length - groupSyncWx?.length) {//清空
+            newActionArr = actionArr?.filter((wx: { id: number }) => {
+                if (isGroupWx.every((groupwx: { id: number }) => wx.id !== groupwx.id)) {
+                    return wx
+                }
+                return
+            })
+            setSelectState({ isAll: false, isChecked: false })
+            dispatch({ type: 'setActionArr', params: { actionArr: newActionArr } })
+        } else {//全选
+            newActionArr = actionArr?.filter((wx: { id: number }) => {
+                if (isGroupWx.every((groupwx: { id: number }) => wx.id !== groupwx.id)) {
+                    return wx
+                }
+                return
+            })
+            newActionArr = [...newActionArr, ...fiterMp]
+            setSelectState({ isAll: false, isChecked: true })
+            dispatch({ type: 'setActionArr', params: { actionArr: newActionArr } })
+        }
+    }, [selectState, mpAccounts, actionArr, actionWX, groupSyncWx])
+    //反选
+    let returnSelct = useCallback(() => {
+        let isGroupWx: any[] = []
+        let returnWx: any[] = []
+        let newActionArr: any[] = []
+        let fiterMp: any[] = []
+        mpAccounts?.forEach((mp: { id: number, verifyTypeInfo: number }) => {
+            if (mp?.verifyTypeInfo !== -1) {//过滤未认证
+                fiterMp.push(mp)
+            }
+            actionArr?.forEach((wx: { id: number }) => {
+                if (wx.id === mp.id) {
+                    isGroupWx.push(wx)
+                }
+            })
+        })
+        returnWx = fiterMp?.filter((mp: { id: number }) => {
+            if (isGroupWx.every((wx: { id: number }) => mp.id !== wx.id)) {
+                return mp
+            }
+            return
+        })
+        newActionArr = actionArr?.filter((wx: { id: number }) => {
+            if (isGroupWx.every((groupwx: { id: number }) => wx.id !== groupwx.id)) {
+                return wx
+            }
+            return
+        })
+        if (returnWx.length !== 0 && groupSyncWx.length !== returnWx.length) {//假如选中个数的大于0
+            setSelectState({ isAll: true, isChecked: true })
+        } else {//假如等于0
+            setSelectState({ isAll: false, isChecked: false })
+        }
+        if (returnWx.length === fiterMp.length && groupSyncWx.length === 0) {
+            setSelectState({ isAll: false, isChecked: true })//假如反选值与当前组的个数一样证明全选
+        }
+        newActionArr = [...newActionArr, ...returnWx]
+        dispatch({ type: 'setActionArr', params: { actionArr: newActionArr } })
+    }, [selectState, mpAccounts, actionArr, actionWX, groupSyncWx])
+    //每次切换组检测全选状态
+    useEffect(() => {
+        let isGroupWx: any[] = []
+        let groupSyncWx: any[] = []
+        mpAccounts?.forEach((mp: { id: number }) => {
+            actionArr?.forEach((wx: { id: number }) => {
+                if (wx.id === mp.id) {
+                    isGroupWx.push(wx)
+                }
+            })
+        })
+        setGroupSyncWx(groupSyncWx)
+        // }
+        if (isGroupWx?.length === mpAccounts?.length) {
+            setSelectState({ isAll: false, isChecked: true })
+        } else {
+            setSelectState({ isAll: true, isChecked: true })
+        }
+        if (isGroupWx?.length === 0) {
+            setSelectState({ isAll: false, isChecked: false })
+        }
+    }, [mpAccounts, actionArr])
+    //弹窗传参接受
+    let show = useCallback((param: { sysMediaId: number }) => {
+        setSysMediaId(param?.sysMediaId)
+        setVisible(true)
+    }, [])
+    //关闭弹窗
+    let handleCancel = useCallback(() => {
+        dispatch({ type: 'setActionArr', params: { actionArr: [] } })
+        setVisible(false)
+    }, [])
+    //确定同步
+    let handleOk = useCallback(() => {
+        if (!actionWX) {
+            return
+        }
+        let mpIds: any[] = []//新建同步
+        actionArr?.forEach((item: any) => {//选中的微信号循环处理
+            mpIds.push(item.id)
+        })
+        sync.run({ mpIds, sysMediaId: sysMediaId }).then((res: any) => {
+            console.log(res)
+            handleCancel()
+        })
+    }, [actionArr, sysMediaId])
+    //选中事件
+    let action = useCallback((item: any) => {
+        let isGroupWx: any[] = []//选中的数据
+        if (actionArr?.some((obj: { appid: number }) => obj.appid === item.appid)) {//删除
+            let newArr = actionArr?.filter((obj: { appid: number }) => obj.appid !== item.appid)
+            mpAccounts?.forEach((mp: { id: number }) => {
+                newArr?.forEach((wx: { id: number }) => {
+                    if (wx.id === mp.id) {
+                        isGroupWx.push(wx)
+                    }
+                })
+            })
+            if (isGroupWx.length === 0) {//假如为空
+                setSelectState({ isAll: false, isChecked: false })
+            } else {
+                setSelectState({ isAll: true, isChecked: true })
+            }
+            dispatch({ type: 'setActionArr', params: { actionArr: newArr } })
+        } else {//添加
+            let arr = [...actionArr, item]
+            if (mpAccounts?.every((mp: { id: number }) => arr?.some((wx: { id: number }) => mp.id === wx.id))) {//假如全部存在全选
+                setSelectState({ isAll: false, isChecked: true })
+            } else {
+                setSelectState({ isAll: true, isChecked: true })
+            }
+            dispatch({ type: 'setActionArr', params: { actionArr: arr } })
+        }
+    }, [actionArr, mpAccounts])
+    useEffect(() => {
+        let newArr: any[] = []
+        if (getDataList?.data) {
+            getDataList?.data?.forEach((data: { mpAccounts: any }) => {
+                if(data?.mpAccounts){
+                    newArr = [...data?.mpAccounts]
+                }
+            })
+        }
+        if (getAllOfMember?.data) {
+            getAllOfMember?.data?.forEach((data: { userMpAccounts: any }) => {
+                data?.userMpAccounts?.forEach((mp: { id: number }) => {
+                    if (newArr.every((arr: { id: number }) => mp?.id !== arr.id)) {
+                        newArr.push(mp)
+                    }
+                })
+            })
+        }
+    }, [getDataList?.data, getAllOfMember?.data,])
+    //配合左侧搜索改变公众号帮助选中
+    useEffect(() => {
+        if (actionWX) {
+            action(actionWX)
+        }
+    }, [actionWX])
+    return <>
+        <Modal
+            title="选择公众号同步"
+            width={1040}
+            visible={visible}
+            onOk={handleOk}
+            onCancel={handleCancel}
+            okText='确定'
+            cancelText='取消'
+            destroyOnClose
+            confirmLoading={sync?.loading}
+        >
+            <Spin tip='Loading...同步素材较慢,请耐心等待,可关闭窗口不影响其他操作' spinning={sync?.loading}
+            >
+                {
+                    actionWX ? <Row className={style.colud}  >
+                        <Col >
+                            <Left />
+                        </Col>
+                        <Col>
+                            <Card
+                                style={{ height: '420px', overflow: 'hidden', minWidth: 290 }}
+                                title={
+                                    <>
+                                        <span style={{ marginRight: 10 }}>组下成员</span>
+                                        <Checkbox
+                                            indeterminate={selectState.isAll}
+                                            onChange={allSelect}
+                                            checked={selectState.isChecked}
+                                        >
+                                            <span style={{ fontSize: 12, color: '#999' }}>全选</span>
+                                        </Checkbox>
+                                        <Button size='small' onClick={returnSelct}>反选</Button>
+                                    </>
+                                }
+                                bodyStyle={{ height: 350, overflowY: 'auto' }}
+                            >
+                                <Checkbox.Group style={{ width: '100%', }} >
+                                    <Row gutter={[10, 0]}>
+                                        {
+
+                                            mpAccounts?.map((item: any) => {
+                                                let isAction: any = actionArr?.some((obj: any) => obj?.appid === item?.appid)
+                                                return <Col key={item.id} span={24}>
+                                                    <Checkbox value={item} className={style.box} onClick={() => action(item)} >
+                                                        <div
+                                                            className={`${isAction ? style.card_action : undefined} ${style.card_box}`}
+                                                        >
+                                                            <div
+                                                                style={{ display: 'flex', alignItems: 'center' }} /**data?.syncVo?.some((obj:{appId:string}) => obj.appId === item.appid) ? {} :  */
+                                                            // description="This is the description"
+                                                            >
+                                                                <img src={item?.headImg || localStorage?.bookImg} className={style.item_img} />
+                                                                {item.nickName}
+                                                            </div>
+                                                        </div>
+                                                    </Checkbox>
+                                                </Col>
+                                            })
+                                        }
+                                    </Row>
+                                </Checkbox.Group>
+                            </Card>
+                        </Col>
+                        <Col>
+                            <Card
+                                style={{ height: 420, overflow: 'hidden', minWidth: 280 }}
+                                title={
+                                    <>
+                                        <span style={{ marginRight: 10 }}>已勾选</span>
+                                        <Button size='small' onClick={() => { dispatch({ type: 'setActionArr', params: { actionArr: [] } }) }}>清空</Button>
+                                    </>
+                                }
+                                bodyStyle={{ height: 350, overflowY: 'auto' }}
+                            >
+                                <Checkbox.Group style={{ width: '100%' }} >
+                                    <Row gutter={[10, 0]}>
+                                        {
+
+                                            actionArr?.map((item: any) => {
+                                                let isAction: any = actionArr?.some((obj: any) => obj?.appid === item?.appid)
+                                                return <Col key={item.id}>
+                                                    <Checkbox value={item} className={style.box} onClick={() => action(item)}>
+                                                        <div
+                                                            className={`${isAction ? style.card_action : undefined} ${style.card_box}`}
+                                                        >
+                                                            <div
+                                                                style={{ display: 'flex', alignItems: 'center' }} /**data?.syncVo?.some((obj:{appId:string}) => obj.appId === item.appid) ? {} :  */
+                                                            // description="This is the description"
+                                                            >
+                                                                <img src={item?.headImg || localStorage?.bookImg} className={style.item_img} />
+                                                                {item.nickName}
+                                                            </div>
+                                                        </div>
+                                                    </Checkbox>
+                                                </Col>
+                                            })
+                                        }
+                                    </Row>
+                                </Checkbox.Group>
+                            </Card>
+                        </Col>
+                    </Row> : <NoFoundPage />
+                }
+            </Spin>
+        </Modal >
+    </>
+})
+
+export default SyncModal

+ 15 - 0
src/components/FileBoxAD/components/tree/index.less

@@ -0,0 +1,15 @@
+.tree{
+    // min-width: 199px;
+    border-right: 1px solid #efefef;
+    height: calc(100vh - 200px);
+    margin-right: 5px;
+    overflow-x:auto;
+    overflow-y: auto;
+    >div{
+        // width: 350px;
+    }
+    img{
+        width: 15px;
+        margin-right: 5px;
+    }
+}

+ 83 - 0
src/components/FileBoxAD/components/tree/index.tsx

@@ -0,0 +1,83 @@
+import { Tree } from 'antd'
+import React, { useMemo, useCallback, useState } from 'react'
+import style from './index.less'
+const fileImg = require('../../../../../public/file.png')
+function TreeBox(props: { data: any[], belongUser: any }): any {
+    const [item, setItem] = useState<any>('')
+    const [actionId, setActionId] = useState<any>('')//拖动选中的当前文件夹ID
+    const [hoverId, setHoverId] = useState<any>('')//hoverId
+    const [expandedKeys, setExpandedKeys] = useState<any>([])
+    let arr = useMemo(() => {
+        let keys: any[] = []
+        let menuArr = [
+            {
+                id: 0,
+                title: props.belongUser == 1 ? '个人本地' : '公共本地',
+                icon: <img src={fileImg} />,
+                childMedias: props?.data
+            }
+        ]
+        function handle(arr: any[]) {
+            let newArr = arr?.map((item) => {
+                if (Array.isArray(item.childMedias)) {
+                    let children: any[] = handle(item.childMedias)
+                    keys.push(item.id)
+                    return { ...item, children, icon: <img src={fileImg} />, key: item.id }
+                } else {
+                    return { ...item, icon: <img src={fileImg} />, key: item.id }
+                }
+            })
+            return newArr
+        }
+        let newArr = menuArr?.map((item) => {
+            if (Array.isArray(item.childMedias)) {
+                let children: any[] = handle(item.childMedias)
+                keys.push(item.id)
+                return { ...item, children, icon: <img src={fileImg} />, key: item.id }
+            } else {
+                return { ...item, icon: <img src={fileImg} />, key: item.id }
+            }
+        })
+        setExpandedKeys(keys)
+        return newArr
+    }, [props.data, props.belongUser])
+    // 点击树
+    const treeClick = useCallback((selectedKeys, e) => {
+        setItem(e.selectedNodes[0])
+    }, [])
+    return [<div className={`${style.tree}`} id='tree'>
+        <Tree
+            treeData={arr}
+            showIcon
+            draggable
+            blockNode
+            defaultExpandAll={true}
+            autoExpandParent={true}
+            expandedKeys={expandedKeys}
+            onExpand={(expandedKeys) => {
+                setExpandedKeys(expandedKeys)
+            }}
+            onSelect={treeClick}
+            onDragStart={(info) => {
+                info.event.preventDefault()
+            }}
+            titleRender={(nodeData: any) => {
+                return <span
+                    style={hoverId === nodeData.id ? { color: 'red', backgroundColor: '#efefef', display: 'inline-block', width: 'calc(100% - 24px)' } : { display: 'inline-block', width: 'calc(100% - 24px)' }}
+                    onDrop={() => {
+                        if (nodeData.id !== actionId) {
+                            setActionId(nodeData.id)
+                        }
+                    }}
+                >{nodeData.title}</span>
+            }}
+            allowDrop={(info: any) => {
+                if (info?.dropNode?.id !== hoverId) {
+                    setHoverId(info?.dropNode?.id)
+                }
+                return false
+            }}
+        />
+    </div>, item, actionId, setActionId, setHoverId]
+}
+export default TreeBox

+ 352 - 0
src/components/FileBoxAD/index.less

@@ -0,0 +1,352 @@
+.files {
+  width: 100%;
+  height: calc(100vh - 240px);
+  overflow-y: auto;
+  position: relative;
+}
+.flex_box_img {
+  width: 100px;
+  cursor: pointer;
+}
+.flex_box {
+  width: 200px;
+  height: 200px;
+  display: flex;
+  justify-content: center;
+  flex-flow: column;
+  align-items: center;
+  padding: 10px 0;
+  position: relative;
+  &:hover {
+    background-color: #c5d8f0;
+    border-radius: 5px;
+    border: 1px solid #c5d8f0;
+    box-sizing: border-box;
+    .select {
+      display: flex;
+      background-color: #7cc6ee;
+    }
+  }
+}
+.flex_box_name {
+  width: 85%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  cursor: pointer;
+  text-align: center;
+  &:hover {
+    color: #1890ff;
+  }
+}
+.flex_box_id {
+  width: 85%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  cursor: pointer;
+  text-align: center;
+  &:hover {
+    color: #1890ff;
+  }
+}
+.rt {
+  margin: 0 5px;
+  color: #c5d8f3;
+}
+.path {
+  margin-bottom: 15px;
+  //   margin-top: 10px;
+}
+.path_color {
+  color: #09aaff;
+  cursor: pointer;
+}
+.file_news_content {
+  width: 100%;
+  columns:300px 5;
+  column-gap: 20px;
+  > div {
+    width: 100%;
+    break-inside: avoid;
+    margin-bottom: 20px;
+    display: flex;
+    justify-content: center;
+  }
+}
+.file_content {
+  width: 100%;
+  columns:200px 7;
+  column-gap: 20px;
+  > div {
+    width: 100%;
+    break-inside: avoid;
+    margin-bottom: 20px;
+    display: flex;
+    justify-content: center;
+  }
+}
+.select {
+  position: absolute;
+  left: 5px;
+  top: 5px;
+  border-radius: 50%;
+  width: 21px;
+  height: 21px;
+  display: none;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  > span {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+}
+.select_btn {
+  margin-bottom: 10px;
+  > span {
+    color: #09aaff;
+    cursor: pointer;
+  }
+}
+.image_box,
+.video_box,
+.audio_box {
+  width: 200px;
+  height: 200px;
+  display: flex;
+  justify-content: center;
+  flex-flow: column;
+  align-items: center;
+  padding: 10px 20px;
+  position: relative;
+  img {
+    height: 100px;
+    width: 130px;
+    object-fit: contain;
+  }
+  audio {
+  }
+  &:hover {
+    background-color: #c5d8f0;
+    border-radius: 5px;
+    border: 1px solid #c5d8f0;
+    box-sizing: border-box;
+    .select {
+      display: flex;
+      background-color: #7cc6ee;
+    }
+  }
+}
+
+.menu {
+  position: fixed;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-flow: column;
+  z-index: 999;
+  list-style: none;
+  padding: 15px 0;
+  min-width: 100px;
+  margin: 0;
+  border: 1px solid #dde0e4;
+  border-radius: 5px;
+  box-shadow: 0 0 8px #ccc;
+  background-color: #fff;
+  > li {
+    cursor: pointer;
+    height: 30px;
+    line-height: 30px;
+    padding: 0 10px;
+    text-align: center;
+    width: 100%;
+    &:hover {
+      background-color: #a4d9f5;
+    }
+  }
+}
+.add {
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100%;
+  width: 100%;
+  background-color: rgba(0, 0, 0, 0.6);
+  color: #fff;
+  font-size: 30px;
+  display: none;
+  align-items: center;
+  justify-content: center;
+}
+.news_box {
+  width: 280px;
+  display: flex;
+  justify-content: center;
+  flex-flow: column;
+  align-items: center;
+  padding: 15px 30px;
+  box-sizing: border-box;
+  position: relative;
+  > div {
+    height: 60px;
+    display: flex;
+    flex-flow: row;
+    align-items: center;
+    position: relative;
+    box-sizing: border-box;
+    width: 100%;
+    justify-content: space-between;
+    margin-top: 2px;
+    &:hover {
+      > p {
+        display: flex;
+        cursor: pointer;
+      }
+    }
+    div {
+      order: 2;
+    }
+    img {
+      object-fit: cover;
+      width: 50px;
+      height: 50px;
+    }
+    span {
+      display: -webkit-box;
+      -webkit-box-orient: vertical;
+      -webkit-line-clamp: 2;
+      overflow: hidden;
+      order: 1;
+      height: 40px;
+      width: 75%;
+    }
+    &:nth-child(2) {
+      position: relative;
+      height: 120px;
+      overflow: hidden;
+      img {
+        object-fit: cover;
+        width: 100%;
+        height: 120px;
+      }
+      > span {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        height: 20px;
+        background-color: rgba(0, 0, 0, 0.6);
+        color: #fff;
+        line-height: 20px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        display: inline-block;
+        width: 100%;
+        box-sizing: border-box;
+      }
+    }
+  }
+  &:hover {
+    background-color: #c5d8f0;
+    border-radius: 5px;
+    // border: 1px solid #c5d8f0;
+    box-sizing: border-box;
+    .select {
+      display: flex;
+      background-color: #7cc6ee;
+    }
+  }
+}
+.knews_box {
+  width: 300px;
+  display: flex;
+  justify-content: center;
+  flex-flow: column;
+  align-items: center;
+  padding: 15px 30px;
+  box-sizing: border-box;
+  position: relative;
+  height: 200px;
+  > div {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+    background-color: #f0f5f7;
+    padding: 10px;
+    border-radius: 5px;
+    height: 95px;
+    box-sizing: border-box;
+    img{
+      width: 50px;
+      height: 50px;
+      margin-top: 25px;
+      object-fit: cover;
+    }
+    span{
+      font-weight: 500;
+      margin-bottom: 5px;
+    }
+    p{
+      font-size: 12px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      display: -webkit-box;
+      -webkit-line-clamp: 2;
+      -webkit-box-orient: vertical;
+      color: #999;
+      padding-top: 10px;
+      width: 160px;
+    }
+  }
+  &:hover {
+    background-color: #c5d8f0;
+    border-radius: 5px;
+    // border: 1px solid #c5d8f0;
+    box-sizing: border-box;
+    .select {
+      display: flex;
+      background-color: #7cc6ee;
+    }
+  }
+}
+.flex_box_font {
+}
+
+.action {
+  background-color: #c5d8f0;
+  border-radius: 5px;
+  // border: 1px solid #90d8ff;
+  box-sizing: border-box;
+  .select {
+    background-color: #09aaff;
+    display: flex;
+  }
+  &:hover {
+    background-color: #c5d8f0;
+    border-radius: 5px;
+    // border: 1px solid #90d8ff;
+    box-sizing: border-box;
+    .select {
+      display: flex;
+      background-color: #09aaff;
+    }
+  }
+}
+.pagination {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 10px;
+}
+.wxSelect{
+  position: absolute;
+  top:0;
+  right: 20px;
+  width: 200px;
+  >div{
+    margin-top: 0 !important;
+  }
+}

+ 363 - 0
src/components/FileBoxAD/index.tsx

@@ -0,0 +1,363 @@
+import useCopy from "@/Hook/useCopy"
+import React, { useCallback, useEffect, useState } from "react"
+import { Image, message, Pagination, Popconfirm, Spin } from 'antd'
+import style from './index.less'
+import FileModal from './components/fileModal'
+import { useModel } from "umi"
+import ImgModal from "./components/imgModal"
+import Updata from "../MaterialModal/updata"
+import WxSelect from "../WxSelect"
+import TreeBox from "./components/tree"
+import SortModal from "./components/fileModal/sortModal"
+import PreviewLocalModal from "@/components/MaterialModal/previewLocalModal"
+
+interface News {
+    id: number,
+    sysMediaId: number,
+    sortIndex: number,
+    title: string,
+    thumbUrl: string,
+    author: string,
+    digest: string,
+    showCoverPic: boolean,
+    content: string,//内容只在调取详情时获取
+    contentSourceUrl: string,//文章地址
+    needOpenComment: boolean,
+    onlyFansCanComment: boolean
+}
+interface Item {
+    id: number,
+    folder: boolean,//是否是文件夹
+    fileType: 'video' | 'image',//类型
+    number: string,//编号
+    title: string,//名称
+    url: string,//链接
+    belongType: string,//所属类型
+    createTime: string,//创建时间
+    description: string,//k图文描述
+    news?: News[]//图文内容
+    knewsThumbUrl: string, // 图片链接
+    knewsThumbId: number,//本地图片ID
+    knewsThumbInfo: any,//k客服详情
+    videoTitle: string,//视频标题
+}
+interface Props {
+    showSync?: (props?: any) => void,//同步弹窗事件
+    isAll?: boolean,//是否允许全选默认开启
+    height?: any,//当使用为弹窗组件时设置高度以免太高
+    noFile?: boolean,//是否禁止选择文件夹
+    isBd?: boolean,//是否强制使用本地数据
+}
+function FlieBox(props: Props) {
+
+    const { showSync, isAll = true, height, noFile = false, isBd } = props
+    const { state, set, dels, list, get, onFile, allFile, delPupOn, delPupOff, changeClickFile, editFile, fileClick, treeClick, pathClick, getList, edit_media_folder, get_folder_tree } = useModel(isAll || isBd ? 'useLaunchAdq.useBdMedia' : 'useLaunchAdq.useBdMediaPup')
+    const { fileVisible, belongUser, selectFile, delPupId, xy, rightClickPup, path, publicPath, parentId, imgVisrible, fileType, sortVisible } = state
+    const { copy } = useCopy()
+    const [updataVisible, setUpdataVisible] = useState<boolean>(false)
+    const fileImg = require('../../../public/file.png')
+    const [moveId, setMoveId] = useState<any>('')//移动的素材ID
+    const [treeEl, item, folderId, setActionId, setHoverId] = TreeBox({ data: get_folder_tree.data, belongUser })
+    const [previewShow, setPreviewShow] = useState<boolean>(false)  // 预览弹窗控制
+    const [previewId, setPreviewId] = useState<number>() // 要预览的ID
+
+    /**复制编号 */
+    const copyId = useCallback((e: React.MouseEvent<HTMLSpanElement, MouseEvent>, value: string) => {
+        e.stopPropagation()//阻止冒泡传递到文件夹被点击事件
+        copy(value)
+    }, [])
+    /**全局右键菜单 */
+    const Menu = useCallback((props: { isItem?: boolean }) => {
+        if (props.isItem && isAll) {
+            return <ul style={{ top: xy?.y, left: xy?.x }} className={style.menu} >
+                {
+                    isAll && <li onClick={allFile}>全选/反选</li>
+                }
+                <li onClick={(e) => { delPupOn(rightClickPup.id); onFile(e, rightClickPup, isAll, true) }}> 删除</li>
+                {
+                    !rightClickPup?.folder && <li onClick={() => {
+                        setPreviewShow(true)
+                        setPreviewId(rightClickPup?.id)
+                    }}>预览</li>
+                }
+                {
+                    !rightClickPup?.folder && <li onClick={() => {
+                        showSync && showSync({ sysMediaId: rightClickPup?.id })
+                    }}>同步</li>
+                }
+                <li onClick={() => {
+                    editFile()
+                }}>编辑</li>
+                <li onClick={() => { set({ actionItem: rightClickPup, sortVisible: true }) }}>编辑排序</li>
+                <li onClick={() => { set({ fileVisible: true }) }}>新建文件夹</li>
+                <li onClick={() => { set({ imgVisrible: true }) }}>新建素材</li>
+                {
+                    (fileType !== 'knews' && fileType !== 'news') && <li onClick={() => { setUpdataVisible(true) }}>批量新增素材</li>
+                }
+                {
+                    isAll && <li onClick={dels}>删除选中文件</li>
+                }
+            </ul>
+        }
+        return <ul style={{ top: xy?.y, left: xy?.x }} className={style.menu}>
+            {
+                isAll && <li onClick={allFile}>全选/反选</li>
+            }
+            {
+
+            }
+            {//防止K图文无限嵌套创建判断
+                (isAll !== false || fileType !== 'knews') ? <li onClick={() => { set({ fileVisible: true }) }}>新建文件夹</li> : <li>此处无法新建操作</li>
+            }
+            {
+                (isAll !== false || fileType !== 'knews') && <li onClick={() => { set({ imgVisrible: true }) }}>新建素材</li>
+            }
+            {
+                (fileType !== 'knews' && fileType !== 'news') && <li onClick={() => { setUpdataVisible(true) }}>批量新增素材</li>
+            }
+            {
+                isAll && <li onClick={dels}>删除选中文件</li>
+            }
+        </ul>
+    }, [xy, rightClickPup, allFile, fileType, previewShow, previewId])
+    /**鼠标右键 */
+    const rightMenu = useCallback((e: any, isItem?: any) => {
+        e.stopPropagation()
+        e.preventDefault()
+        let x = e.clientX;
+        let y = e.clientY;
+        set({ xy: { x, y } })
+        if (isItem) {
+            set({ rightClickPup: isItem })
+            if (!isItem?.folder && isItem?.fileType === 'knews') {
+                set({
+                    knewsdefaultData: {//客服消息编辑回填弹窗中的内容预先存放
+                        newsList: [
+                            {
+                                title: isItem?.title,
+                                knewsThumbUrl: isItem?.knewsThumbUrl,
+                                knewsLink: isItem?.knewsLink,
+                                description: isItem?.description,
+                                knewsThumbInfo: isItem?.knewsThumbInfo,
+                                knewsThumbId: isItem?.knewsThumbId,
+                                sort: isItem?.sort || 0
+                            }
+                        ]
+                    }
+                })
+            }
+        } else {
+            set({ rightClickPup: { id: 'all' } })
+        }
+        return false;
+    }, [])
+    /**左键点击页面隐藏菜单 */
+    useEffect(() => {
+        function fnc(e: any) {
+            set({ rightClickPup: { id: '' } })
+        }
+        document.addEventListener('click', fnc)
+        return () => {
+            document.removeEventListener('click', fnc)
+        }
+    }, [])
+    // /**切换个人|公共|父目录ID变化清空选中的文件*/
+    useEffect(() => {
+        set({ selectFile: [] })
+        if (belongUser == '1' && path) {//处理切换个人|公共时保留路径层级的请求
+            set({ parentId: path[path?.length - 1]?.id || null })
+        }
+        if (belongUser == '0' && publicPath) {
+            set({ parentId: publicPath[publicPath?.length - 1]?.id || null })
+        }
+    }, [belongUser, parentId, path?.length, publicPath?.length])
+    /**卸载组件处理 */
+    useEffect(() => {
+        return () => {
+            set({ parentId: null })
+        }
+    }, [])
+    /**获取目录树*/
+    useEffect(() => {
+        // if (isBd) {
+        get_folder_tree.run({ belongUser, fileType })
+        // }
+    }, [isAll, isBd, fileType, belongUser])
+    // 点击目录树进入对应目录
+    useEffect(() => {
+        if (item) {
+            let { icon, children, ...arg } = item
+            treeClick(arg)
+        }
+    }, [item])
+    // 拖动事件配置
+    const moveConfig = useCallback((item) => {
+        return {
+            draggable: true,
+            onDragStart: (ev: any) => {
+                let img = document.createElement('img')
+                img.src = fileImg;
+                ev.dataTransfer.setDragImage(img, 0, 0)
+                setMoveId(item.id)
+            },
+            onDragEnd: (e: any) => {
+                if (folderId !== '' && moveId !== '') {
+                    edit_media_folder.run({ sysMediaId: moveId, folderId }).then(res => {
+                        message.success('操作成功')
+                        list.refresh()
+                    })
+                }
+                setActionId('')
+                setHoverId('')
+                setMoveId('')
+            },
+        }
+    }, [folderId, moveId])
+    return <div style={{ display: 'flex', flexFlow: 'row' }}>
+        {
+            get_folder_tree?.data?.length > 0 && <div style={{ flexShrink: 0 }}>
+                {treeEl}
+            </div>
+        }
+        <div style={{ flexShrink: 1 }}>
+            <div className={style.files} onContextMenu={rightMenu} style={height ? { height } : {}}>
+                {/* 关联公众号筛选 */}
+                <div className={style.wxSelect}>
+                    <WxSelect mode={1} isAll={isAll} />
+                </div>
+                {/* 层级路径 */}
+                <div className={style.path} >
+                    {//存在渲染个人本地路径不存在执行公共本地
+                        belongUser == '1' && path?.map((item: { title: string }, index: number) => {
+                            return <span key={index} onClick={() => { pathClick({ ...item, index }) }} >
+                                <span className={path?.length !== index + 1 ? style.path_color : ''}>{item?.title}</span>
+                                {path?.length !== index + 1 && <span className={style.rt} >{'>'}</span>}
+                            </span>
+                        })
+                    }
+                    {
+                        belongUser == '0' && publicPath?.map((item: { title: string }, index: number) => {
+                            return <span key={index} onClick={() => { pathClick({ ...item, index }) }} >
+                                <span className={publicPath?.length !== index + 1 ? style.path_color : ''}>{item?.title}</span>
+                                {publicPath?.length !== index + 1 && <span className={style.rt} >{'>'}</span>}
+                            </span>
+                        })
+                    }
+                </div>
+                {/* 内容 */}
+                <div className={(fileType === 'news' || fileType === 'knews') ? style.file_news_content : style.file_content} >
+                    {
+                        list?.data?.records?.map((item: Item) => {
+                            if (item.folder) {
+                                {/* 文件夹模板 */ }
+                                return <Popconfirm
+                                    title="确定要删除吗?"
+                                    onConfirm={() => { dels(item) }}
+                                    okText="是"
+                                    cancelText="否"
+                                    onCancel={delPupOff}
+                                    visible={delPupId === item.id}
+                                    key={item.id}
+                                >
+                                    <Spin tip='正在请求素材详情,请耐心等待...' spinning={get?.loading}>
+                                        <div
+                                            className={`${style.flex_box} ${selectFile?.some((id: number) => id === item.id) ? style.action : ''}`}
+                                            onContextMenu={(e) => { rightMenu(e, item) }}
+                                            onClick={(e) => { changeClickFile(e, item, isAll, noFile) }}
+                                            onDragStart={(e: any) => {
+                                                e.preventDefault()
+                                            }}
+                                        >
+                                            <span className={`${style.select}`} onClick={(e) => { changeClickFile(e, item, isAll, noFile) }}>
+                                                <span role="img" aria-label="check" style={{ color: '#fff' }}><svg viewBox="64 64 896 896" focusable="false" data-icon="check" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"></path></svg></span>
+                                            </span>
+                                            <img src={fileImg}
+                                                className={style.flex_box_img}
+                                                onClick={(e: any) => { e.stopPropagation(); fileClick(item) }}
+                                                onDragOver={(ev) => {
+                                                    ev.preventDefault()
+                                                }}
+                                                onDrop={() => {
+                                                    if (item.id !== folderId) {
+                                                        setActionId(item.id)
+                                                    }
+                                                }}
+                                            />
+                                            <span className={style.flex_box_name} >{item?.title}</span>
+                                            <span className={style.flex_box_id} onClick={(e) => { copyId(e, item?.number) }} >{item?.number}</span>
+                                        </div>
+                                    </Spin>
+                                </Popconfirm>
+                            } else {
+                                {/* 图片模板 ,视频模板,音频模板*/ }
+                                let El = item.fileType === 'image' ?
+                                    <Image src={item.url} onClick={(e) => { e.stopPropagation() }}
+                                    /> :
+                                    item.fileType === 'video' ?
+                                        <video src={item.url} style={{ width: 130, height: 100 }} controls />
+                                        :
+                                        <audio src={item.url} controls style={{ width: 150 }} />
+                                return <Popconfirm
+                                    title="确定要删除吗?"
+                                    onConfirm={() => { dels(item.id) }}
+                                    okText="是"
+                                    cancelText="否"
+                                    onCancel={delPupOff}
+                                    visible={delPupId === item.id}
+                                    key={item.id}
+                                >
+                                    <Spin tip='正在请求素材详情,请耐心等待...' spinning={get?.loading}>
+                                        <div
+                                            className={`${style.image_box} ${selectFile?.some((id: number) => id === item.id) ? style.action : ''}`}
+                                            onContextMenu={(e) => { rightMenu(e, item) }}
+                                            onClick={(e) => { changeClickFile(e, item, isAll) }}
+                                            {...moveConfig(item)}
+                                        >
+                                            <span className={`${style.select}`} onClick={(e) => { changeClickFile(e, item, isAll) }}>
+                                                <span role="img" aria-label="check" style={{ color: '#fff' }}><svg viewBox="64 64 896 896" focusable="false" data-icon="check" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"></path></svg></span>
+                                            </span>
+                                            {El}
+                                            <span className={style.flex_box_name} onClick={(e) => { copyId(e, item?.url) }}>{item?.videoTitle || item?.title}</span>
+                                            <span className={style.flex_box_id} onClick={(e) => { copyId(e, item?.number) }} >{item?.number}</span>
+                                        </div>
+                                    </Spin>
+                                </Popconfirm>
+                            }
+                            return null
+                        })
+                    }
+                </div>
+                {/* 鼠标右键菜单 */}
+                {rightClickPup.id ? rightClickPup.id === 'all' ? <Menu /> : <Menu isItem /> : null}
+                {/* 新建文件弹窗 */}
+                {fileVisible && <FileModal isAll={isAll} />}
+                {/* 编辑排序 */}
+                {sortVisible && <SortModal isAll={isAll} />}
+                {/* 新建非图文 */}
+                {imgVisrible && <ImgModal isAll={isAll} />}
+                {/* 批量新建素材 */}
+                {updataVisible && <Updata mediaType={fileType === 'image' ? '1' : fileType === 'video' ? '3' : '2'} callBcak={setUpdataVisible} type={1} isAll={isAll} />}
+            </div>
+            {
+                list?.data?.records?.length > 0 && <div className={style.pagination}>
+                    {/* 分页 */}
+                    <Pagination
+                        showSizeChanger
+                        onChange={(page: number, pageSize?: number) => {
+                            getList({ pageSize, pageNum: page })
+                        }}
+                        onShowSizeChange={(current: number, size: number) => {
+                            getList({ pageSize: size, pageNum: current })
+                        }}
+                        current={list?.data?.current}
+                        defaultPageSize={20}
+                        total={list?.data?.total}
+                    />
+                </div>
+            }
+        </div>
+
+        {previewShow && <PreviewLocalModal visible={previewShow} onClose={() => setPreviewShow(false)} previewId={previewId} />}
+    </div>
+}
+export default React.memo(FlieBox)

+ 432 - 0
src/models/useLaunchAdq/useBdMedia.ts

@@ -0,0 +1,432 @@
+import getMD5 from '@/components/MD5';
+import { useAjax } from '@/Hook/useAjax';
+import { bdSysMediaList, bdSysMediaAdd, delMedia, bdSysMediaEdit, getFileUrl, syncMedia, getMedia, configSortApi, syncForSend, wxSysMediaAdd, editWxlist, getFolderTree, editMediaFolder } from '@/services/launchAdq/material';
+import { message } from 'antd';
+import { Dispatch, useCallback, useReducer, useState } from 'react'
+import { request } from 'umi';
+
+type State = {
+    fileVisible?: boolean,//文件夹弹窗
+    imgVisrible?: boolean,//img,voice,video弹窗
+    newsVisrible?: boolean,//news弹窗
+    knewsVisrible?: boolean,//客服图文弹窗
+    fileName?: string,//文件夹名称
+    sort?: number, // 排序
+    videoTitle?: string,//video文件名称
+    videoDescription?: string,//视频描述
+    belongUser?: any,//0公共本地|1个人本地
+    fileType?: 'image' | 'news' | 'voice' | 'video' | 'knews',//类型
+    parentId?: any,//上级目录ID,顶级使用null
+    url?: string,//素材地址
+    pathId?: string,//路径ID
+    selectFile?: number[],//选中的文件列表
+    rightClickPup?: any,// /**右键菜单开关 all为全部 id 为目标文件 空为不显示*/
+    xy?: { x: number, y: number },// /**鼠标位置 菜单弹窗位置使用*/
+    delPupId?: any,//按了单个删除存放的id为了让弹窗区分文件
+    actionItem?: any,//当前选中的Itme
+    path?: any[],//个人路径
+    publicPath?: any[],//本地路径
+    file?: File,//素材文件
+    selectItem?: any,//单选素材时存放选中的素材
+    knewsdefaultData?: any,//k图文编辑时的默认内容
+    sortVisible?:boolean,//排序弹窗
+}
+export type Action = {
+    type: 'set' | 'init',
+    params?: any
+}
+const typeEnum = {
+    'image': '图片',
+    'news': '图文素材',
+    'voice': '音频',
+    'video': '视频',
+    'knews': 'K-图文'
+}
+function reducer(state: State, action: Action) {
+    let { type, params } = action
+    let newState = JSON.parse(JSON.stringify(state))
+    newState.file = state.file
+    switch (type) {
+        case 'set':
+            Object.keys(params as State).forEach((key: string) => {
+                newState[key] = (params as State)[key]
+            })
+            return newState
+        case 'init':
+            return { ...initData, ...params }
+        default:
+            return state;
+    }
+}
+const initData: State = {
+    fileVisible: false,
+    knewsVisrible: false,
+    fileName: '',
+    videoTitle: '',
+    videoDescription: '',
+    belongUser: '1',
+    fileType: 'image',
+    parentId: null,
+    selectFile: [],
+    delPupId: '',
+    rightClickPup: { id: '' },
+    publicPath: [{ title: '公共本地', number: '0' }],
+    path: [{ title: '个人本地', number: '0' }],
+    imgVisrible: false,
+    newsVisrible: false,
+    sortVisible:false,
+    sort: 0
+}
+/**本地素材管理器 */
+function useBdMediaPup() {
+    const [state, dispatch]: [State, Dispatch<Action>] = useReducer(reducer, initData)
+    const { fileName, sort, belongUser, fileType, parentId, selectFile, delPupId, rightClickPup, actionItem, path, publicPath, file, videoTitle, videoDescription } = state
+    const list = useAjax((params) => bdSysMediaList(params))
+    const add = useAjax((params) => bdSysMediaAdd(params), { msgNmae: '新增' })
+    const addWx = useAjax((params) => wxSysMediaAdd(params), { msgNmae: '新增' })
+    const del = useAjax((params) => delMedia(params), { msgNmae: '删除' })
+    const edit = useAjax((params) => bdSysMediaEdit(params), { msgNmae: '编辑' })
+    const sync = useAjax((params) => syncMedia(params), { msgNmae: '同步' })
+    const syncId = useAjax((params) => syncForSend(params), { msgNmae: '同步' })
+    const configSort = useAjax((params) => configSortApi(params), { msgNmae: '排序' })
+    const get_folder_tree = useAjax((params: any) => getFolderTree(params))
+    const edit_media_folder = useAjax((params: any) => editMediaFolder(params))
+    const get = useAjax((params) => getMedia(params))//获取图文详情
+    const eidtWxlists = useAjax((params) => editWxlist(params))
+    const [isOk, setIsOk] = useState<boolean>(true)
+    //请求上传地址
+    const fileUrl = useAjax((params: { type: string }) => getFileUrl(params), {
+        manual: true,
+    })
+    /**初始化数据 */
+    const init = useCallback((params: any) => {
+        console.log('init===>', params)
+        dispatch({ type: 'init', params })
+    }, [])
+    /**设置state */
+    const set = useCallback((params: State) => {
+        // console.log('params===>',params)
+        if (params) {
+            dispatch({ type: 'set', params })
+        }
+    }, [])
+    /**新增K-客服模板消息 */
+    const knewsOk = useCallback((props: any) => {
+        let { title, knewsThumbUrl, knewsLink, description, knewsThumbId } = props?.newsList[0]
+        let obj = {
+            parentId,
+            belongUser,
+            folder: false,
+            fileType,
+            title: title,
+            knewsThumbUrl: knewsThumbUrl,
+            knewsLink: knewsLink,
+            description: description,
+            sort: props.sort
+        }
+        if (knewsThumbId) {//数据中存在封面图本地ID加入本地ID
+            obj['knewsThumbId'] = knewsThumbId
+        }
+        if (rightClickPup?.id && rightClickPup?.id !== 'all') {//编辑时传入ID
+            obj['sysMediaId'] = rightClickPup?.id
+            edit.run(obj).then((res) => {
+                if (res) {
+                    list.refresh()
+                    set({ knewsVisrible: false, knewsdefaultData: null })
+                }
+            })
+            eidtWxlists.run({ sysMediaId: rightClickPup?.id, mpIds: props?.mpIds || [] }).then((res) => {
+                console.log(res)
+            })
+        } else {
+            add.run({ ...obj, mpIds: props?.mpIds || [] }).then((res) => {
+                if (res) {
+                    list.refresh()
+                    set({ knewsVisrible: false, knewsdefaultData: null })
+                }
+            })
+        }
+    }, [add, edit, list, parentId, belongUser, fileType, rightClickPup])
+    /**新增文件夹,非图文素材 */
+    const fileOk = useCallback((e: any, fnc?: () => Promise<any>, selectWx?: number[]) => {
+        e?.stopPropagation()
+        if (fnc) {//存在代表素材
+            fnc().then(res => {
+                if (!res) {
+                    return
+                }
+                if (res?.file && file) {
+                    /**修改文件名以用户设置的文件title命名*/
+                    let newFile = new File([file], res?.fileName ? res?.fileName + '.' + file?.name?.split('.')[1] : file?.name, { type: file?.type })
+                    let formData = new FormData();
+
+                    if (fileType === 'voice') {
+                        if (newFile.size > 2097152) {
+                            message.error('请上传小于2M的音频')
+                            return
+                        }
+                    }
+                    if (fileType === 'video' || fileType === 'image') {
+                        if (newFile.size > 10485760) {
+                            message.error('请上传小于10M的素材')
+                            return
+                        }
+                    }
+
+                    /**向阿里云请求上传地址*/
+                    fileUrl.run({ type: newFile.type }).then(res1 => {
+                        Object.keys(res1).forEach((key: string) => {
+                            if (key !== 'url') {
+                                formData.append(key, res1[key])
+                            }
+                        })
+                        formData.append('file', newFile)
+                        /**向阿里云返回的上传地址上传文件*/
+                        request(res1?.ossUrl, { method: 'post', body: formData }).then(async (res2: { code: number, data: { url: string } }) => {
+                            if (res2.code === 200) {
+                                message.success('上传成功')
+                                if (res) {
+                                    /**取到返回的文件地址向后端发送具体数据*/
+                                    if (res2?.data?.url) {
+                                        let fileMd5 = await getMD5(file)
+                                        let obj = { title: res?.fileName, fileType, folder: false, parentId, belongUser, url: res2?.data?.url, mpIds: selectWx, sort: res?.sort, fileMd5, fileSize: newFile?.size }
+                                        if (fileType === 'video') {
+                                            obj['videoTitle'] = res?.videoTitle || res?.title
+                                            obj['videoDescription'] = res?.videoDescription
+                                        }
+                                        add.run(obj).then((res) => {
+                                            console.log(res)
+                                            list.refresh()//刷新页面
+                                            offEditFile()//关闭弹窗并清空相关数据
+                                        })
+                                    }
+                                }
+                            } else {
+                                message.error('上传失败!')
+                            }
+                        })
+                    })
+                }
+            })
+        } else {
+            if (fileName) {
+                let obj = { title: fileName, fileType, folder: true, parentId, belongUser, mpIds: selectWx, sort }
+                add.run(obj).then((res) => {
+                    get_folder_tree.refresh()
+                    list.refresh()//刷新页面
+                    offEditFile()//关闭弹窗并清空相关数据
+                })
+            }
+        }
+    }, [fileName, fileType, parentId, belongUser, list, file])
+    /**编辑非图文素材名称*/
+    const nameOk = useCallback((selectWx?: any) => {
+        if (fileName && actionItem) {
+            let obj = { title: fileName, belongUser, sysMediaId: actionItem?.id, fileType: actionItem?.fileType, folder: actionItem?.folder, url: actionItem?.url, sort }
+            if (fileType === 'video') {
+                obj['videoTitle'] = videoTitle
+                obj['videoDescription'] = videoDescription
+            }
+            edit.run(obj).then((res) => {
+                list.refresh()//刷新页面
+                offEditFile()//关闭弹窗并清空相关数据
+            })
+        }
+        //
+        eidtWxlists.run({ sysMediaId: actionItem?.id, mpIds: selectWx || [] }).then((res) => {
+            console.log(res)
+        })
+    }, [fileName, actionItem, edit, belongUser, fileType, videoTitle, videoDescription])
+    /**删除文件 */
+    const dels = useCallback((id?: any) => {
+        let arr = typeof id === 'number' ? [id] : selectFile
+        let len = arr?.length || 0
+        if (len) {
+            arr?.map((id, index) => {
+                del.run(id).then(() => {
+                    set({ selectFile: selectFile?.filter(i => i !== id) })//清理已删除文件
+                    if (index === len - 1) {
+                        list.refresh()//最后一次刷新页面
+                    }
+                })
+            })
+        }
+    }, [list, selectFile])
+    /**获取本地素材数据列表 */
+    const getList = useCallback((props?: any) => {
+        let obj = { pageSize: 20, pageNum: 1, fileType, belongUser, parentId, ...props }
+        list.run(obj).then((res) => {
+            setIsOk(true)
+        })
+    }, [list, fileType, belongUser, parentId])
+    /**选中文件 single 开启可单选为了在右键删除选择时只选一个*/
+    const onFile = useCallback((e: any, item: { id: any, folder?: boolean }, isAll?: boolean, single?: boolean) => {
+        let { id } = item
+        e?.stopPropagation()
+        if (isAll && !single) {
+            set({ selectFile: [...new Set([...selectFile as number[], id])] })
+        } else {
+            if (item?.folder && !single) {//假如是文件不让选择
+                message.error('不能选择文件夹')
+                return
+            }
+            set({ selectFile: [item.id], selectItem: item })
+        }
+    }, [selectFile])
+    /**点击文件夹 */
+    const fileClick = useCallback((item) => {
+        if (isOk) {
+            setIsOk(false)
+            console.log(item, belongUser == '1')
+            if (belongUser == '1' && path) {
+                set({ path: [...path, item] })
+            }
+            if (belongUser == '0' && publicPath) {
+                set({ publicPath: [...publicPath, item] })
+            }
+            set({ parentId: item.id })
+            getList({ parentId: item.id })//请求对应文件夹列表
+        }
+
+    }, [path, publicPath, fileType, belongUser, isOk])
+    /**点击目录树*/
+    const treeClick = useCallback((item) => {
+        if (isOk) {
+            setIsOk(false)
+            // console.log(item, belongUser == '1')
+            if (belongUser == '1' && path) {
+                set(item?.id === 0 ? { path: [path[0]] } : { path: [path[0], item] })
+            }
+            if (belongUser == '0' && publicPath) {
+                set(item?.id === 0 ? { publicPath: [publicPath[0]] } : { publicPath: [publicPath[0], item] })
+            }
+            set({ parentId: item.id })
+            getList({ parentId: item.id })//请求对应文件夹列表
+        }
+    }, [path, publicPath, fileType, belongUser, isOk])
+    /**点击路径 */
+    const pathClick = useCallback((item) => {
+        let newPath: any[] = []
+        if (belongUser == '1') {
+            path?.forEach((paths, index) => {//用传入的index和path循环的index对比小于等于index代表该保留的路径
+                if (index <= item.index) {
+                    newPath.push(paths)
+                }
+            })
+            set({ path: newPath })
+        } else {
+            publicPath?.forEach((paths, index) => {//用传入的index和path循环的index对比小于等于index代表该保留的路径
+                if (index <= item.index) {
+                    newPath.push(paths)
+                }
+            })
+            set({ publicPath: newPath })
+        }
+        set({ parentId: item.parentId })
+        console.log(item)
+        getList({ pageSize: 20, pageNum: 1, fileType, belongUser, parentId: item.id })
+        console.log('文件夹被点击')
+    }, [path, fileType, belongUser, publicPath])
+    /**取消文件 */
+    const offFile = useCallback((e: any, item: { id: any }) => {
+        let { id } = item
+        set({ selectFile: selectFile?.filter(i => i !== id) })
+    }, [selectFile])
+    /**判断是取消还是选中文件的操作在点击打钩时使用 */
+    const changeClickFile = useCallback((e: any, item: { id: any, folder?: boolean }, isAll?: boolean) => {
+        let { id } = item
+        e?.stopPropagation()//阻止冒泡传递到文件夹被点击事件
+        let state = selectFile?.some((i) => i === id)
+        if (isAll) {
+            if (state) {//存在就是删除
+                set({ selectFile: selectFile?.filter(i => i !== id) })
+            } else {//否则新增
+                set({ selectFile: [...selectFile as number[], id] })
+            }
+        } else {//单选情况存在于选择素材弹窗组件
+            if (item?.folder && fileType !== 'image') {//假如是文件不让选择
+                message.error('不能选择文件夹')
+                return
+            }
+            if (state) {//存在就是删除
+                set({ selectFile: selectFile?.filter(i => i !== id) })
+                set({ selectItem: null })
+            } else {//否则新增
+                set({ selectFile: [id] })
+                set({ selectItem: item })
+            }
+        }
+    }, [selectFile, fileType])
+    /**开启删除弹窗 */
+    const delPupOn = useCallback((delPupId) => {
+        set({ delPupId })
+    }, [])
+    /**关闭删除弹窗并去除选中 */
+    const delPupOff = useCallback(() => {
+        set({ delPupId: '' })
+        offFile(null, { id: delPupId })
+    }, [delPupId,])
+    /**编辑 */
+    const editFile = useCallback((e?: any,) => {
+        e?.stopPropagation()
+        onFile(null, rightClickPup, true, true)
+        // if (rightClickPup?.fileType !== 'news') {//不是图文开启编辑名字弹窗
+        let obj = { fileVisible: true, actionItem: rightClickPup, fileName: rightClickPup.title, sort: rightClickPup.sort }
+        if (rightClickPup?.fileType === 'video') {
+            obj['videoTitle'] = rightClickPup?.videoTitle || rightClickPup?.title
+            obj['videoDescription'] = rightClickPup?.videoDescription
+        }
+        set(obj)
+        // }
+    }, [rightClickPup])
+    /**取消编辑后清空选中存放的数据并关闭弹窗*/
+    const offEditFile = useCallback(() => {
+        set({ fileVisible: false, imgVisrible: false,sortVisible:false, actionItem: '', fileName: '', selectFile: selectFile?.filter(id => id !== actionItem?.id), sort: 0 })
+    }, [selectFile, actionItem])
+    /**全选反选文件*/
+    const allFile = useCallback(() => {
+        let allArr: any[] = []
+        list?.data?.records?.forEach((item: { id: any }) => {
+            allArr.push(item.id)
+        })
+        set({ selectFile: allArr.filter((i) => selectFile?.every(id => id !== i)) })
+    }, [selectFile, list])
+    /**图文素材弹窗开关 */
+    const showNews = useCallback(() => {
+
+    }, [])
+    return {
+        state,
+        init,
+        set,
+        fileOk,
+        getList,
+        dels,
+        onFile,
+        changeClickFile,
+        allFile,
+        delPupOn,
+        delPupOff,
+        editFile,
+        offEditFile,
+        nameOk,
+        fileClick,
+        treeClick,
+        pathClick,
+        knewsOk,
+        configSort,
+        fileUrl,
+        addWx,
+        list,
+        add,
+        sync,
+        get,
+        edit,
+        syncId,
+        typeEnum,
+        eidtWxlists,
+        get_folder_tree,
+        edit_media_folder
+    }
+}
+export default useBdMediaPup

+ 430 - 0
src/models/useLaunchAdq/useBdMediaPup.ts

@@ -0,0 +1,430 @@
+import getMD5 from '@/components/MD5';
+import { useAjax } from '@/Hook/useAjax';
+import { bdSysMediaList, bdSysMediaAdd, delMedia, bdSysMediaEdit, getFileUrl, syncMedia, getMedia, configSortApi, syncForSend, wxSysMediaAdd, editWxlist, getFolderTree, editMediaFolder } from '@/services/launchAdq/material';
+import { message } from 'antd';
+import { Dispatch, useCallback, useReducer, useState } from 'react'
+import { request } from 'umi';
+/**新本地素材弹窗 */
+type State = {
+    fileVisible?: boolean,//文件夹弹窗
+    imgVisrible?: boolean,//img,voice,video弹窗
+    newsVisrible?: boolean,//news弹窗
+    knewsVisrible?: boolean,//客服图文弹窗
+    fileName?: string,//文件夹名称
+    sort?: number, // 排序
+    videoTitle?: string,//video文件名称
+    videoDescription?: string,//视频描述
+    belongUser?: any,//0公共本地|1个人本地
+    fileType?: 'image' | 'news' | 'voice' | 'video' | 'knews',//类型
+    parentId?: any,//上级目录ID,顶级使用null
+    url?: string,//素材地址
+    pathId?: string,//路径ID
+    selectFile?: number[],//选中的文件列表
+    rightClickPup?: any,// /**右键菜单开关 all为全部 id 为目标文件 空为不显示*/
+    xy?: { x: number, y: number },// /**鼠标位置 菜单弹窗位置使用*/
+    delPupId?: any,//按了单个删除存放的id为了让弹窗区分文件
+    actionItem?: any,//当前选中的Itme
+    path?: any[],//个人路径
+    publicPath?: any[],//本地路径
+    file?: File,//素材文件
+    selectItem?: any,//单选素材时存放选中的素材
+    knewsdefaultData?: any,//k图文编辑时的默认内容
+    sortVisible?:boolean,//排序弹窗
+}
+export type Action = {
+    type: 'set' | 'init',
+    params?: any
+}
+const typeEnum = {
+    'image': '图片',
+    'news': '图文素材',
+    'voice': '音频',
+    'video': '视频',
+    'knews': 'K-图文'
+}
+function reducer(state: State, action: Action) {
+    let { type, params } = action
+    let newState = JSON.parse(JSON.stringify(state))
+    newState.file = state.file
+    switch (type) {
+        case 'set':
+            Object.keys(params as State).forEach((key: string) => {
+                newState[key] = (params as State)[key]
+            })
+            return newState
+        case 'init':
+            return { ...initData, ...params }
+        default:
+            return state;
+    }
+}
+const initData: State = {
+    fileVisible: false,
+    knewsVisrible: false,
+    videoTitle: '',
+    videoDescription: '',
+    fileName: '',
+    belongUser: '1',
+    fileType: 'image',
+    parentId: null,
+    selectFile: [],
+    delPupId: '',
+    rightClickPup: { id: '' },
+    publicPath: [{ title: '公共本地', number: '0' }],
+    path: [{ title: '个人本地', number: '0' }],
+    imgVisrible: false,
+    newsVisrible: false,
+    sortVisible:false,
+    sort: 0
+}
+/**本地素材管理器 */
+function useBdMediaPup() {
+    const [state, dispatch]: [State, Dispatch<Action>] = useReducer(reducer, initData)
+    const { fileName, sort, belongUser, fileType, parentId, selectFile, delPupId, rightClickPup, actionItem, path, publicPath, file, videoTitle, videoDescription } = state
+    const list = useAjax((params) => bdSysMediaList(params))
+    const add = useAjax((params) => bdSysMediaAdd(params), { msgNmae: '新增' })
+    const addWx = useAjax((params) => wxSysMediaAdd(params), { msgNmae: '新增' })
+    const del = useAjax((params) => delMedia(params), { msgNmae: '删除' })
+    const edit = useAjax((params) => bdSysMediaEdit(params), { msgNmae: '编辑' })
+    const sync = useAjax((params) => syncMedia(params), { msgNmae: '同步' })
+    const syncId = useAjax((params) => syncForSend(params), { msgNmae: '同步' })
+    const configSort = useAjax((params) => configSortApi(params), { msgNmae: '排序' })
+    const get = useAjax((params) => getMedia(params))//获取图文详情
+    const get_folder_tree = useAjax((params: any) => getFolderTree(params))
+    const edit_media_folder = useAjax((params: any) => editMediaFolder(params))
+    const [isOk, setIsOk] = useState<boolean>(true)
+    const eidtWxlists = useAjax((params) => editWxlist(params))
+    //请求上传地址
+    const fileUrl = useAjax((params: { type: string }) => getFileUrl(params), {
+        manual: true,
+    })
+    /**初始化数据 */
+    const init = useCallback((params: any) => {
+        dispatch({ type: 'init', params })
+    }, [])
+    /**设置state */
+    const set = useCallback((params: State) => {
+        if (params) {
+            dispatch({ type: 'set', params })
+        }
+    }, [])
+    /**新增K-客服模板消息 */
+    const knewsOk = useCallback((props: any) => {
+        let { title, knewsThumbUrl, knewsLink, description, knewsThumbId } = props?.newsList[0]
+        let obj = {
+            parentId,
+            belongUser,
+            folder: false,
+            fileType,
+            title: title,
+            knewsThumbUrl: knewsThumbUrl,
+            knewsLink: knewsLink,
+            description: description,
+            sort: props.sort
+        }
+        if (knewsThumbId) {//数据中存在封面图本地ID加入本地ID
+            obj['knewsThumbId'] = knewsThumbId
+        }
+        if (rightClickPup?.id && rightClickPup?.id !== 'all') {//编辑时传入ID
+            obj['sysMediaId'] = rightClickPup?.id
+            edit.run(obj).then((res) => {
+                if (res) {
+                    list.refresh()
+                    set({ knewsVisrible: false, knewsdefaultData: null })
+                }
+            })
+            eidtWxlists.run({ sysMediaId: rightClickPup?.id, mpIds: props?.mpIds || [] }).then((res) => {
+                console.log(res)
+            })
+        } else {
+            add.run({ ...obj, mpIds: props?.mpIds }).then((res) => {
+                if (res) {
+                    list.refresh()
+                    set({ knewsVisrible: false, knewsdefaultData: null })
+                }
+            })
+        }
+    }, [add, edit, list, parentId, belongUser, fileType, rightClickPup])
+    /**新增文件夹,非图文素材 */
+    const fileOk = useCallback((e: any, fnc?: () => Promise<any>, selectWx?: number[]) => {
+        e?.stopPropagation()
+        if (fnc) {//存在代表素材
+            fnc().then(res => {
+                if (!res) {
+                    return
+                }
+                if (res?.file && file) {
+                    /**修改文件名以用户设置的文件title命名*/
+                    let newFile = new File([file], res?.fileName ? res?.fileName + '.' + file?.name?.split('.')[1] : file?.name, { type: file?.type })
+                    let formData = new FormData();
+
+                    if (fileType === 'voice') {
+                        if (newFile.size > 2097152) {
+                            message.error('请上传小于2M的音频')
+                            return
+                        }
+                    }
+                    if (fileType === 'video' || fileType === 'image') {
+                        if (newFile.size > 10485760) {
+                            message.error('请上传小于10M的素材')
+                            return
+                        }
+                    }
+
+                    /**向阿里云请求上传地址*/
+                    fileUrl.run({ type: newFile.type }).then(res1 => {
+                        Object.keys(res1).forEach((key: string) => {
+                            if (key !== 'url') {
+                                formData.append(key, res1[key])
+                            }
+                        })
+                        formData.append('file', newFile)
+                        /**向阿里云返回的上传地址上传文件*/
+                        request(res1?.ossUrl, { method: 'post', body: formData }).then(async (res2: { code: number, data: { url: string } }) => {
+                            if (res2.code === 200) {
+                                message.success('上传成功')
+                                if (res) {
+                                    /**取到返回的文件地址向后端发送具体数据*/
+                                    if (res2?.data?.url) {
+                                        let fileMd5 = await getMD5(file)
+                                        let obj = { title: res?.fileName, fileType, folder: false, parentId, belongUser, url: res2?.data?.url, mpIds: selectWx, sort: res?.sort, fileMd5, fileSize: newFile?.size }
+                                        if (fileType === 'video') {
+                                            obj['videoTitle'] = res?.videoTitle || res?.title
+                                            obj['videoDescription'] = res?.videoDescription
+                                        }
+                                        add.run(obj).then((res) => {
+                                            list.refresh()//刷新页面
+                                            offEditFile()//关闭弹窗并清空相关数据
+                                        })
+                                    }
+                                }
+                            } else {
+                                message.error('上传失败!')
+                            }
+                        })
+                    })
+                }
+            })
+        } else {
+            if (fileName) {
+                let obj = { title: fileName, fileType, folder: true, parentId, belongUser, mpIds: selectWx, sort }
+                add.run(obj).then((res) => {
+                    get_folder_tree.refresh()
+                    list.refresh()//刷新页面
+                    offEditFile()//关闭弹窗并清空相关数据
+                })
+            }
+        }
+    }, [fileName, fileType, parentId, belongUser, list, file])
+    /**编辑非图文素材名称*/
+    const nameOk = useCallback((selectWx: any) => {
+        if (fileName && actionItem) {
+            let obj = { title: fileName, belongUser, sysMediaId: actionItem?.id, fileType: actionItem?.fileType, folder: actionItem?.folder, url: actionItem?.url, sort }
+            if (fileType === 'video') {
+                obj['videoTitle'] = videoTitle
+                obj['videoDescription'] = videoDescription
+            }
+            edit.run(obj).then((res) => {
+                list.refresh()//刷新页面
+                offEditFile()//关闭弹窗并清空相关数据
+            })
+        }
+        //
+        eidtWxlists.run({ sysMediaId: actionItem?.id, mpIds: selectWx || [] }).then((res) => {
+            console.log(res)
+        })
+    }, [fileName, actionItem, edit, belongUser, fileType, videoTitle, videoDescription])
+    /**删除文件 */
+    const dels = useCallback((id?: any) => {
+        let arr = typeof id === 'number' ? [id] : selectFile
+        let len = arr?.length || 0
+        if (len) {
+            arr?.map((id, index) => {
+                del.run(id).then(() => {
+                    set({ selectFile: selectFile?.filter(i => i !== id) })//清理已删除文件
+                    if (index === len - 1) {
+                        list.refresh()//最后一次刷新页面
+                    }
+                })
+            })
+        }
+    }, [list, selectFile])
+    /**获取本地素材数据列表 */
+    const getList = useCallback((props?: any) => {
+        console.log('fileType',fileType)
+        let obj = { pageSize: 20, pageNum: 1, fileType, belongUser, parentId, ...props }
+        list.run(obj).then((res) => {
+            setIsOk(true)
+        })
+    }, [list, fileType, belongUser, parentId])
+    /**选中文件 single 开启可单选为了在右键删除选择时只选一个*/
+    const onFile = useCallback((e: any, item: { id: any, folder?: boolean }, isAll?: boolean, single?: boolean) => {
+        let { id } = item
+        e?.stopPropagation()
+        if (isAll && !single) {
+            set({ selectFile: [...new Set([...selectFile as number[], id])] })
+        } else {
+            if (item?.folder && !single) {//假如是文件不让选择
+                message.error('不能选择文件夹')
+                return
+            }
+            set({ selectFile: [item.id], selectItem: item })
+        }
+    }, [selectFile])
+    /**点击文件夹 */
+    const fileClick = useCallback((item) => {
+        if (isOk) {
+            setIsOk(false)
+            if (belongUser == '1' && path) {
+                set({ path: [...path, item] })
+            }
+            if (belongUser == '0' && publicPath) {
+                set({ publicPath: [...publicPath, item] })
+            }
+            set({ parentId: item.id })
+            getList({ parentId: item.id })//请求对应文件夹列表
+        }
+
+    }, [path, publicPath, fileType, belongUser, isOk])
+     /**点击目录树*/
+     const treeClick=useCallback((item) => {
+        if (isOk) {
+            setIsOk(false)
+            console.log(item, belongUser == '1')
+            if (belongUser == '1' && path) {
+                    set({ path: [path[0], item] })
+            }
+            if (belongUser == '0' && publicPath) {
+                set({ publicPath: [publicPath[0], item] })
+            }
+            set({ parentId: item.id })
+            getList({ parentId: item.id })//请求对应文件夹列表
+        }
+    }, [path, publicPath, fileType, belongUser, isOk])
+    /**点击路径 */
+    const pathClick = useCallback((item) => {
+        let newPath: any[] = []
+        if (belongUser == '1') {
+            path?.forEach((paths, index) => {//用传入的index和path循环的index对比小于等于index代表该保留的路径
+                if (index <= item.index) {
+                    newPath.push(paths)
+                }
+            })
+            set({ path: newPath })
+        } else {
+            publicPath?.forEach((paths, index) => {//用传入的index和path循环的index对比小于等于index代表该保留的路径
+                if (index <= item.index) {
+                    newPath.push(paths)
+                }
+            })
+            set({ publicPath: newPath })
+        }
+        set({ parentId: item.parentId })
+        getList({ pageSize: 20, pageNum: 1, fileType, belongUser, parentId: item.id })
+    }, [path, fileType, belongUser, publicPath])
+    /**取消文件 */
+    const offFile = useCallback((e: any, item: { id: any }) => {
+        let { id } = item
+        set({ selectFile: selectFile?.filter(i => i !== id) })
+    }, [selectFile])
+    /**判断是取消还是选中文件的操作在点击打钩时使用 */
+    const changeClickFile = useCallback((e: any, item: { id: any, folder?: boolean }, isAll?: boolean, noFile?: boolean) => {
+        console.log('2222222---->', noFile);
+        let { id } = item
+        e?.stopPropagation()//阻止冒泡传递到文件夹被点击事件
+        let state = selectFile?.some((i) => i === id)
+        if (isAll) {
+            if (state) {//存在就是删除
+                set({ selectFile: selectFile?.filter(i => i !== id) })
+            } else {//否则新增
+                set({ selectFile: [...selectFile as number[], id] })
+            }
+        } else {//单选情况存在于选择素材弹窗组件
+            if (!noFile) {
+                if (item?.folder) {//假如是文件不让选择
+                    message.error('不能选择文件夹')
+                    return
+                }
+            }
+            
+            if (state) {//存在就是删除
+                set({ selectFile: selectFile?.filter(i => i !== id) })
+                set({ selectItem: null })
+            } else {//否则新增
+                set({ selectFile: [id] })
+                set({ selectItem: item })
+            }
+        }
+    }, [selectFile, fileType])
+    /**开启删除弹窗 */
+    const delPupOn = useCallback((delPupId) => {
+        set({ delPupId })
+    }, [])
+    /**关闭删除弹窗并去除选中 */
+    const delPupOff = useCallback(() => {
+        set({ delPupId: '' })
+        offFile(null, { id: delPupId })
+    }, [delPupId,])
+    /**编辑 */
+    const editFile = useCallback((e?: any,) => {
+        e?.stopPropagation()
+        onFile(null, rightClickPup, true, true)
+        // if (rightClickPup?.fileType !== 'news') {//不是图文开启编辑名字弹窗
+        let obj = { fileVisible: true, actionItem: rightClickPup, fileName: rightClickPup.title, sort: rightClickPup.sort }
+        if (rightClickPup?.fileType === 'video') {
+            obj['videoTitle'] = rightClickPup?.videoTitle || rightClickPup?.title
+            obj['videoDescription'] = rightClickPup?.videoDescription
+        }
+        set(obj)
+        // }
+    }, [rightClickPup])
+    /**取消编辑后清空选中存放的数据并关闭弹窗*/
+    const offEditFile = useCallback(() => {
+        set({ fileVisible: false, imgVisrible: false,sortVisible:false, actionItem: '', fileName: '', selectFile: selectFile?.filter(id => id !== actionItem?.id), sort: 0 })
+    }, [selectFile, actionItem])
+    /**全选反选文件*/
+    const allFile = useCallback(() => {
+        let allArr: any[] = []
+        list?.data?.records?.forEach((item: { id: any }) => {
+            allArr.push(item.id)
+        })
+        set({ selectFile: allArr.filter((i) => selectFile?.every(id => id !== i)) })
+    }, [selectFile, list])
+    /**图文素材弹窗开关 */
+    const showNews = useCallback(() => {
+
+    }, [])
+    return {
+        state,
+        init,
+        set,
+        fileOk,
+        getList,
+        dels,
+        onFile,
+        changeClickFile,
+        allFile,
+        delPupOn,
+        delPupOff,
+        editFile,
+        offEditFile,
+        nameOk,
+        fileClick,
+        treeClick,
+        pathClick,
+        knewsOk,
+        configSort,
+        addWx,
+        list,
+        add,
+        sync,
+        get,
+        edit,
+        syncId,
+        typeEnum,
+        eidtWxlists,
+        get_folder_tree,
+        edit_media_folder
+    }
+}
+export default useBdMediaPup

+ 17 - 0
src/pages/launchSystem/material/cloud/index.less

@@ -0,0 +1,17 @@
+.colud {
+    // display: flex;
+    // flex-flow: row nowrap;
+    // > div:nth-child(2) {
+    //   flex: 1;
+    // }
+    width: 100%;
+    margin-bottom: 0;
+    > div {
+      > div {
+        &:first-child {
+          margin-bottom: 0;
+        }
+      }
+    }
+  }
+  

+ 50 - 0
src/pages/launchSystem/material/cloud/index.tsx

@@ -0,0 +1,50 @@
+import FlieBox from '@/components/FileBoxAD'
+import HocError from '@/Hoc/HocError'
+import { Tabs } from 'antd'
+import React, { useEffect, useRef } from 'react'
+import { useModel } from 'umi'
+import style from './index.less'
+import SyncModal from '@/components/FileBox/components/syncModal'
+
+
+const { TabPane } = Tabs;
+
+function Cloud() {
+    const { state, set, getList, init, typeEnum } = useModel('useLaunchAdq.useBdMedia')
+    let refSync: { current: { show: (id: string, appIds: string[]) => void } } | any = useRef()//syncModal获取实例方法
+    const { fileType, belongUser, parentId } = state
+    /**加载组件或数据更新执行请求列表 */
+    useEffect(() => {
+        if (belongUser === '1' || belongUser === '0') {
+            getList() 
+        }
+    }, [fileType, belongUser, parentId])
+
+    return <div className={style.colud}>
+        <Tabs
+            type="card"
+            onChange={(activeKey: any) => {
+                init({ fileType: activeKey })//切换类型时先初始化数据,并设置类型
+            }}
+            className={style.card}
+            activeKey={fileType}
+        >
+            {
+                ['image', 'video'].map((key: any) => {
+                    return <TabPane tab={typeEnum[key]} key={key} style={{ backgroundColor: '#fff', padding: '0 15px' }} >
+                        <Tabs onChange={(activeKey: any) => { set({ belongUser: activeKey }) }} activeKey={belongUser}>
+                            <TabPane tab={'个人本地'} key={1} />
+                            <TabPane tab={'公共本地'} key={0} />
+                        </Tabs>
+                        <FlieBox showSync={refSync?.current?.show} />
+                    </TabPane>
+                })
+            }
+        </Tabs>
+        {/**同步弹窗 */}
+        <SyncModal ref={refSync} />
+    </div>
+
+}
+
+export default HocError(Cloud)

+ 373 - 0
src/services/launchAdq/material.ts

@@ -0,0 +1,373 @@
+import { queryStr } from '@/utils/query';
+import { request } from 'umi';
+import { api } from '../api'
+export interface AddTag {
+  id?: string,
+  tagName: string,
+  type: string
+}
+export interface MediaList {
+  describeInfo?: string,
+  groupId?: string,
+  id?: string,
+  mediaAuthor?: string,
+  mediaContent?: string,
+  mediaContentSourceUrl?: string,
+  mediaDigest?: string,
+  mediaTagIds?: string | string[],
+  mediaThumbMediaId?: string,
+  mediaType?: string,
+  title?: string,
+  url?: string
+}
+export interface GetMediaList {
+  mediaTagId?: string,
+  pageNum: string,
+  pageSize: string,
+  mediaType: string,
+  tagIds?: string | string[],
+  groupId?: string,
+}
+
+export interface CreateGraphic {
+  groupId: number,
+  mediaTagIds: number[],
+  sysMediaGraphics: {
+    author: string,
+    content: string,
+    contentSourceUrl?: string,
+    digest?: string,
+    needOpenComment?: boolean,
+    onlyFansCanComment?: boolean,
+    showCoverPic?: boolean,
+    title: string,
+    url: string
+  }[]
+}
+export interface EditGraphic extends CreateGraphic {
+  id: string,
+  appId: string
+}
+export interface GetGraphicList {
+  mediaType?: string,
+  groupId?: string,
+  pageNum: number,
+  pageSize: number,
+  tagIds?: number[]
+}
+// export interface GetWeChatData {
+//   appId: string,
+//   count: string | number,
+//   offset: string
+// }
+export interface GetWeChatData {
+  mpId: number,
+  mediaType: string,
+  pageNum: number,
+  pageSize: number
+}
+export interface GetWeChatITData {
+  appId: string,
+  count: string | number,
+  offset: string,
+  type: string
+}
+// export async function getGzhData() {
+//   return request('/api/data/gzhsc');
+// }
+/**获取上传接口 */
+export async function getFileUrl(params: { type: string }) {
+  return request(`${api}/system/oss/ossUpload?type=${params.type}`);
+}
+/*************************标签**********************************/
+/**新增标签 */
+export async function addTag(params?: AddTag) {
+  return request(api + '/system/mediaTag/create', {
+    method: 'POST',
+    data: params,
+  });
+}
+/**编辑标签 */
+export async function editTag(params?: AddTag) {
+  return request(api + '/system/mediaTag/edit', {
+    method: 'POST',
+    data: params,
+  });
+}
+
+/**删除标签 */
+export async function delTag(params: { id: string }) {
+  return request(`${api}/system/mediaTag/${params.id}`, {
+    method: 'DELETE',
+  });
+}
+/**获取标签列表*/
+export async function getTagList(params: { type: string }) {
+  return request(`${api}/system/mediaTag/list?type=${params.type}`);
+}
+/*************************分类**********************************/
+/**新增标签 */
+export async function addClass(params?: { groupName: string }) {
+  return request(api + '/system/sysMediaGroup/create', {
+    method: 'POST',
+    data: params,
+  });
+}
+/**编辑标签 */
+export async function editClass(params?: { groupName: string, id: string }) {
+  return request(api + '/system/sysMediaGroup/edit', {
+    method: 'POST',
+    data: params,
+  });
+}
+
+/**删除标签 */
+export async function delClass(params: { id: string }) {
+  return request(`${api}/system/sysMediaGroup/${params.id}`, {
+    method: 'DELETE',
+  });
+}
+/**获取标签列表*/
+export async function getClass(params: any) {
+  return request(`${api}/system/sysMediaGroup?type=${params?.type}`);
+}
+/*************************素材**********************************/
+/** 新增非图文本地素材*/
+export async function addMediaList(params?: MediaList) {
+  return request(api + '/system/sysMedia/create', {
+    method: 'POST',
+    data: params,
+  });
+}
+/** 编辑非图文本地素材*/
+export async function editMediaList(params?: MediaList) {
+  return request(api + '/system/sysMedia/edit', {
+    method: 'POST',
+    data: params,
+  });
+}
+/** 新增图文本地素材*/
+export async function createGraphic(params: CreateGraphic) {
+  return request(api + '/system/sysMedia/createGraphic', {
+    method: 'POST',
+    data: params,
+  });
+}
+/** 新增图文微信素材*/
+export async function createGraphicMaterial(params: CreateGraphic) {
+  return request(api + '/system/sysMedia/addWxGraphicMaterial', {
+    method: 'POST',
+    data: params,
+  });
+}
+/** 编辑图文本地素材*/
+export async function editGraphic(params: EditGraphic) {
+  return request(api + '/system/sysMedia/editGraphic', {
+    method: 'POST',
+    data: params,
+  });
+}
+/** 编辑图文微信素材*/
+export async function editGraphicWxMaterial(params: EditGraphic) {
+  return request(api + '/system/sysMedia/updateWxMaterial', {
+    method: 'POST',
+    data: params,
+  });
+}
+/** 获取本地素材*/
+export async function getMediaList(params: GetMediaList) {
+  return request(`${api}/adq/sysMedia/list${queryStr(params)}`);
+}
+/** 删除本地素材*/
+export async function delMediaList(params: { id: string }) {
+  return request(`${api}/system/sysMedia/${params.id}`, {
+    method: 'DELETE',
+  });
+}
+/**同步本地图文素材 */
+export async function syncPermanentGraphicMedias(params: { appId: string[], mediaId: string }) {
+  return request(api + '/system/sysMedia/syncPermanentGraphicMaterial', {
+    method: 'POST',
+    data: params,
+  });
+}
+/**同步本地非图文素材 */
+export async function syncMedias(params: { appId: string[], mediaId: string }) {
+  return request(api + '/system/sysMedia/syncPermanent', {
+    method: 'POST',
+    data: params,
+  });
+}
+/**获取微信图文素材 */
+export async function getWeChatITData(params: GetWeChatITData) {
+  return request(`${api}/system/sysMedia/batchGetMaterialNews${queryStr(params)}`);
+}
+
+
+// /**获取微信非图文素材 */
+// export async function getWeChatData(params: GetWeChatData) {
+//   return request(`${api}/system/sysMedia/batchGetMateria${queryStr(params)}`);
+// }
+/**获取微信非图文素材 */
+export async function getWeChatData(params: GetWeChatData) {
+  let { mpId, ...param } = params
+  return request(`${api}/system/mp/media/list/${mpId}`, {
+    method: 'POST',
+    data: param,
+  });
+}
+/**更新公众号下素材*/
+export async function updateMediaCache(params: { mpId: number, mediaType: string }) {
+  let { mpId, mediaType } = params
+  return request(`${api}/system/mp/media/syncMedia/${mpId}/${mediaType}`, {
+    method: 'PUT'
+  })
+}
+/** 删除微信素材*/
+export async function delMediawx(id: number) {
+  return request(`${api}/system/mp/media/${id}`, {
+    method: 'DELETE',
+  });
+}
+/**微信素材详情*/
+export async function getWxMediaInfo(params: { id: number, isCon?: boolean }) {
+  let { id, isCon = true } = params
+  return request(`${api}/system/mp/media/${id}/${isCon}`);
+}
+
+/**导入文章 */
+export async function exportMediaByUrl(params: { url: string }) {
+  return request(`${api}/system/sysMedia/exportMediaByUrl${queryStr(params)}`);
+}
+export async function exportMediaByUrl1(url: string) {
+  return fetch(`${api}/system/mp/media/parseWxNewsByUrl?url=${url}`, {
+    headers: { ['Authorization']: 'Bearer ' + sessionStorage.getItem('Admin-Token') },
+  });
+}
+
+
+
+
+/**本地素材详情*/
+export async function getGraphicDetail(params: { id: string }) {
+  return request(`${api}/system/sysMedia/graphicDetail/${params.id}`);
+}
+/**查询图文同步结果 */
+export async function syncResult(params: any) {
+  return request(api + '/system/sysMedia/syncResult', {
+    method: 'POST',
+    data: params,
+  });
+}
+/**批量上传非图文素材 */
+export async function bantchAdd(params: any) {
+  return request(`${api}/system/sysMedia/bantchAdd`, {
+    method: 'PUT',
+    data: params
+  })
+}
+/**oss删除 */
+export async function delOss(params: any) {
+  return request(`${api}/system/oss/${params}`, {
+    method: 'DELETE',
+    // data: params
+  });
+}
+
+
+
+//=========================新接口===================
+// belongUser 0 公共 1 个人
+/**获取本地素材列表 */
+export async function bdSysMediaList(params: { parentId?: any, belongUser: 0 | 1, pageNum: number, pageSize: number, fileType: "image" | "news" | "voice" | "video" }) {
+  console.log('鹅鹅鹅,曲项向天歌')
+  const { belongUser, ...param } = params
+  return request(api + `/adq/sysMedia/list/${belongUser}`, {
+    method: 'POST',
+    data: param,
+  });
+}
+/**修改本地素材 */
+export async function bdSysMediaEdit(params: { belongUser: 0 | 1, sysMediaId: number, fileType: "image" | "news" | "voice" | "video" }) {
+  const { belongUser, sysMediaId, ...param } = params
+  return request(api + `/system/sysMedia/${belongUser}/${sysMediaId}`, {
+    method: 'PUT',
+    data: param
+  });
+}
+/**新增本地素材 */
+export async function bdSysMediaAdd(params: { belongUser: 0 | 1, parentId: number, fileType: "image" | "news" | "voice" | "video" }) {
+  const { belongUser, ...param } = params
+  return request(api + `/system/sysMedia/${belongUser}`, {
+    method: 'POST',
+    data: param,
+  });
+}
+/**新增微信素材*/
+export async function wxSysMediaAdd(params: { mediaType: "image" | "news" | "voice" | "video", localMediaId: number, mpId: number, news: any }) {
+  return request(api + `/system/mp/media`, {
+    method: 'POST',
+    data: params,
+  });
+}
+/**删除本地素材 */
+export async function delMedia(sysMediaId: any) {
+  return request(`${api}/system/sysMedia/${sysMediaId}`, {
+    method: 'DELETE',
+  });
+}
+/**获取本地素材详情*/
+export async function getMedia(sysMediaId: any) {
+  return request(`${api}/system/sysMedia/${sysMediaId}`, {
+    method: 'GET',
+  });
+}
+/**同步素材 */
+export async function syncMedia(params: { sysMediaId: number, mpIds: number }) {
+  const { mpIds, sysMediaId } = params
+  return request(api + `/system/sysMedia/syncToMp/${sysMediaId}/${mpIds}`, {
+    method: 'PUT',
+  });
+}
+/**返回微信媒体ID的同步接口 */
+export async function syncForSend(params: { sysMediaId: number, mpIds: number }) {
+  const { mpIds, sysMediaId } = params
+  return request(api + `/system/sysMedia/syncForSend/${sysMediaId}/${mpIds}`, {
+    method: 'PUT',
+  });
+}
+
+/**获取素材管理的公众号*/
+export async function getWxlist(sysMediaId: number) {
+  return request(api + `/system/sysMedia/getBindMps/${sysMediaId}`, {
+    method: 'GET'
+  })
+}
+/**编辑素材关联的公众号 */
+export async function editWxlist(params: { sysMediaId: number, mpIds: any }) {
+  const { sysMediaId, mpIds } = params
+  return request(api + `/system/sysMedia/bindMediaMps/${sysMediaId}`, {
+    method: 'POST',
+    data: mpIds
+  })
+}
+/**获取素材文件夹目录树*/
+export async function getFolderTree(props: { belongUser: any, fileType: any }) {
+  let { belongUser, fileType } = props
+  return request(api + `/adq/sysMedia/folderTree/${belongUser}/${fileType}`, {
+    method: 'GET'
+  })
+}
+/*改变文件位置*/
+export async function editMediaFolder(params: { sysMediaId: number, folderId: number }) {
+  const { folderId, sysMediaId } = params
+  return request(api + `/system/sysMedia/configMediaFolder/${folderId}/${sysMediaId}`, {
+    method: 'PUT',
+  });
+}
+/** 排序 */
+export async function configSortApi({ sysMediaId, sort }: { sysMediaId: number, sort: number }) {
+  return request(api + `/system/sysMedia/configMediaSort/${sysMediaId}/${sort}`, {
+    method: 'PUT'
+  });
+}