index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import useCopy from "@/Hook/useCopy"
  2. import React, { useCallback, useEffect, useState } from "react"
  3. import { Carousel, Image, message, Pagination, Popconfirm, Spin } from 'antd'
  4. import style from './index.less'
  5. import FileModal from './components/fileModal'
  6. import { useModel } from "umi"
  7. import ImgModal from "./components/imgModal"
  8. import TreeBox from "./components/tree"
  9. import SortModal from "./components/fileModal/sortModal"
  10. import { EyeOutlined } from "@ant-design/icons"
  11. import ImgsModal from "./components/imgsModal"
  12. interface News {
  13. id: number,
  14. sysMediaId: number,
  15. sortIndex: number,
  16. title: string,
  17. thumbUrl: string,
  18. author: string,
  19. digest: string,
  20. showCoverPic: boolean,
  21. content: string,//内容只在调取详情时获取
  22. contentSourceUrl: string,//文章地址
  23. needOpenComment: boolean,
  24. onlyFansCanComment: boolean
  25. }
  26. interface Item {
  27. id: number,
  28. folder: boolean,//是否是文件夹
  29. mediaType: 'VIDEO' | 'IMG' | 'PAGE',//类型
  30. number: string,//编号
  31. title: string,//名称
  32. url: string,//链接
  33. belongType: string,//所属类型
  34. createTime: string,//创建时间
  35. description: string,//k图文描述
  36. news?: News[]//图文内容
  37. knewsThumbUrl: string, // 图片链接
  38. knewsThumbId: number,//本地图片ID
  39. knewsThumbInfo: any,//k客服详情
  40. videoTitle: string,//视频标题
  41. width: number, // 宽
  42. height: number, // 高
  43. pageSpecsList?: any[],
  44. }
  45. interface Props {
  46. isAll?: boolean,//是否允许全选默认开启
  47. height?: any,//当使用为弹窗组件时设置高度以免太高
  48. noFile?: boolean,//是否禁止选择文件夹
  49. setPage?: (type: 0 | 1 | 2 | 3, data?: any) => void, // 0 创建 1 查看 2 复制
  50. isBack?: boolean, // 是否需要回填
  51. }
  52. function FlieBox(props: Props) {
  53. const { isAll = true, height, noFile = false, setPage, isBack = false } = props
  54. const { state, set, dels, list, get, onFile, allFile, delPupOn, delPupOff, changeClickFile, editFile, fileClick, treeClick, pathClick, getList, edit_media_folder, get_folder_tree } = useModel(isAll ? 'useLaunchAdq.useBdMedia' : 'useLaunchAdq.useBdMediaPup')
  55. const { fileVisible, belongUser, selectFile, selectItem, delPupId, xy, rightClickPup, path, publicPath, parentId, imgVisrible, imgsVisrible, mediaType, sortVisible } = state
  56. const { copy } = useCopy()
  57. const fileImg = require('../../../public/file.png')
  58. const [moveId, setMoveId] = useState<any>('')//移动的素材ID
  59. const [treeEl, item, folderId, setActionId, setHoverId] = TreeBox({ data: get_folder_tree.data, belongUser })
  60. const [listData, setListData] = useState<any>({})
  61. // 获取数据
  62. useEffect(() => {
  63. setListData(list?.data)
  64. }, [list, mediaType])
  65. /**复制编号 */
  66. const copyId = useCallback((e: React.MouseEvent<HTMLSpanElement, MouseEvent>, value: string) => {
  67. e.stopPropagation()//阻止冒泡传递到文件夹被点击事件
  68. copy(value)
  69. }, [])
  70. /**全局右键菜单 */
  71. const Menu = useCallback((props: { isItem?: boolean }) => {
  72. if (props.isItem && isAll) {
  73. return <ul style={{ top: xy?.y, left: xy?.x }} className={style.menu} >
  74. {
  75. isAll && <li onClick={allFile}>全选/反选</li>
  76. }
  77. {mediaType === 'PAGE' ? <>
  78. {/* <li onClick={() => { setPage && setPage(1, rightClickPup.id) }}>查看</li> */}
  79. <li onClick={() => { setPage && setPage(2, rightClickPup.id) }}>复制</li>
  80. <li onClick={() => { setPage && setPage(3, rightClickPup.id) }}>批量复制</li>
  81. </> : <>
  82. <li onClick={() => { editFile() }}>编辑</li>
  83. </>}
  84. <li onClick={() => { set({ actionItem: rightClickPup, sortVisible: true }) }}>编辑排序</li>
  85. <li onClick={() => { set({ fileVisible: true }) }}>新建文件夹</li>
  86. <li onClick={() => { set({ imgVisrible: true }) }}>新建素材</li>
  87. <li onClick={(e) => { delPupOn(rightClickPup.id); onFile(e, rightClickPup, isAll, true) }} style={{ color: 'red' }}> 删除</li>
  88. {
  89. isAll && <li onClick={dels} style={{ color: 'red' }}>删除选中文件</li>
  90. }
  91. </ul>
  92. }
  93. return <ul style={{ top: xy?.y, left: xy?.x }} className={style.menu}>
  94. {
  95. isAll && <li onClick={allFile}>全选/反选</li>
  96. }
  97. {//防止K图文无限嵌套创建判断
  98. (isAll !== false) && <li onClick={() => { set({ fileVisible: true }) }}>新建文件夹</li> //: <li>此处无法新建操作</li>
  99. }
  100. {
  101. mediaType === 'PAGE' ? isAll ? <li onClick={() => { setPage && setPage(0) }}>新建素材</li> : <li>此处无法新建操作</li> : <li onClick={() => { set({ imgVisrible: true }) }}>新建素材</li>
  102. }
  103. {
  104. mediaType === 'IMG' && <li onClick={() => { set({ imgsVisrible: true }) }}>批量新建素材</li>
  105. }
  106. {
  107. isAll && <li onClick={dels} style={{ color: 'red' }}>删除选中文件</li>
  108. }
  109. </ul>
  110. }, [xy, rightClickPup, allFile, mediaType])
  111. /**鼠标右键 */
  112. const rightMenu = useCallback((e: any, isItem?: any) => {
  113. e.stopPropagation()
  114. e.preventDefault()
  115. let x = e.clientX;
  116. let y = e.clientY;
  117. set({ xy: { x, y } })
  118. if (isItem) {
  119. set({ rightClickPup: isItem })
  120. } else {
  121. set({ rightClickPup: { id: 'all' } })
  122. }
  123. return false;
  124. }, [])
  125. /**左键点击页面隐藏菜单 */
  126. useEffect(() => {
  127. function fnc(e: any) {
  128. set({ rightClickPup: { id: '' } })
  129. }
  130. document.addEventListener('click', fnc)
  131. return () => {
  132. document.removeEventListener('click', fnc)
  133. }
  134. }, [])
  135. // /**切换个人|公共|父目录ID变化清空选中的文件*/
  136. useEffect(() => {
  137. if (!isBack) {
  138. set({ selectFile: [], selectItem: [] })
  139. }
  140. if (belongUser == '1' && path) {//处理切换个人|公共时保留路径层级的请求
  141. set({ parentId: path[path?.length - 1]?.id || null })
  142. }
  143. if (belongUser == '0' && publicPath) {
  144. set({ parentId: publicPath[publicPath?.length - 1]?.id || null })
  145. }
  146. }, [belongUser, parentId, path?.length, publicPath?.length])
  147. /**卸载组件处理 */
  148. useEffect(() => {
  149. return () => {
  150. set({ parentId: null })
  151. }
  152. }, [])
  153. /**获取目录树*/
  154. useEffect(() => {
  155. get_folder_tree.run({ belongUser, mediaType })
  156. }, [isAll, mediaType, belongUser])
  157. // 点击目录树进入对应目录
  158. useEffect(() => {
  159. if (item) {
  160. let { icon, children, ...arg } = item
  161. treeClick(arg)
  162. }
  163. }, [item])
  164. // 拖动事件配置
  165. const moveConfig = useCallback((item) => {
  166. return {
  167. draggable: true,
  168. onDragStart: (ev: any) => {
  169. let img = document.createElement('img')
  170. img.src = fileImg;
  171. ev.dataTransfer.setDragImage(img, 0, 0)
  172. setMoveId(item.id)
  173. },
  174. onDragEnd: (e: any) => {
  175. if (folderId !== '' && moveId !== '') {
  176. edit_media_folder.run({ sysMediaId: moveId, folderId, mediaType }).then(res => {
  177. message.success('操作成功')
  178. list.refresh()
  179. })
  180. }
  181. setActionId('')
  182. setHoverId('')
  183. setMoveId('')
  184. },
  185. }
  186. }, [folderId, moveId, mediaType])
  187. return <div style={{ display: 'flex', flexFlow: 'row' }}>
  188. {get_folder_tree?.data?.length > 0 && <div style={{ flexShrink: 0 }}>
  189. {treeEl}
  190. </div>}
  191. <div style={{ flexShrink: 1 }}>
  192. <div className={style.files} onContextMenu={rightMenu} style={height ? { height } : {}}>
  193. {/* 关联公众号筛选 */}
  194. <div className={style.wxSelect}>
  195. </div>
  196. {/* 层级路径 */}
  197. <div className={style.path} >
  198. {//存在渲染个人本地路径不存在执行公共本地
  199. belongUser == '1' && path?.map((item: { title: string }, index: number) => {
  200. return <span key={index} onClick={() => { pathClick({ ...item, index }) }} >
  201. <span className={path?.length !== index + 1 ? style.path_color : ''}>{item?.title}</span>
  202. {path?.length !== index + 1 && <span className={style.rt} >{'>'}</span>}
  203. </span>
  204. })
  205. }
  206. {
  207. belongUser == '0' && publicPath?.map((item: { title: string }, index: number) => {
  208. return <span key={index} onClick={() => { pathClick({ ...item, index }) }} >
  209. <span className={publicPath?.length !== index + 1 ? style.path_color : ''}>{item?.title}</span>
  210. {publicPath?.length !== index + 1 && <span className={style.rt} >{'>'}</span>}
  211. </span>
  212. })
  213. }
  214. </div>
  215. {/* 内容 */}
  216. <Spin spinning={list.loading} style={{ width: '100%' }}>
  217. <div className={style.file_content}>
  218. {
  219. listData?.records?.map((item: Item) => {
  220. if (item.folder) {
  221. {/* 文件夹模板 */ }
  222. return <Popconfirm
  223. title="确定要删除吗?"
  224. onConfirm={() => { dels(item) }}
  225. okText="是"
  226. cancelText="否"
  227. onCancel={delPupOff}
  228. visible={delPupId === item.id}
  229. key={item.id}
  230. >
  231. <Spin tip='正在请求素材详情,请耐心等待...' spinning={get?.loading}>
  232. <div
  233. className={`${style.flex_box} ${selectFile?.some((id: number) => id === item.id) ? style.action : ''}`}
  234. onContextMenu={(e) => { rightMenu(e, item) }}
  235. onClick={(e) => { changeClickFile(e, item, isAll, noFile) }}
  236. onDragStart={(e: any) => {
  237. e.preventDefault()
  238. }}
  239. >
  240. {/* {isAll && <span className={`${style.select}`} onClick={(e) => { changeClickFile(e, item, isAll, noFile) }}>
  241. <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>
  242. </span>} */}
  243. <img src={fileImg}
  244. className={style.flex_box_img}
  245. onClick={(e: any) => { e.stopPropagation(); fileClick(item) }}
  246. onDragOver={(ev) => {
  247. ev.preventDefault()
  248. }}
  249. onDrop={() => {
  250. if (item.id !== folderId) {
  251. setActionId(item.id)
  252. }
  253. }}
  254. />
  255. <span className={style.flex_box_name} >{item?.title}</span>
  256. <span className={style.flex_box_id} onClick={(e) => { copyId(e, item?.number) }} >{item?.number}</span>
  257. </div>
  258. </Spin>
  259. </Popconfirm>
  260. } else {
  261. {/* 图片模板 ,视频模板,音频模板*/ }
  262. let topPageElements: any
  263. let topName: string = ""
  264. if (mediaType === 'PAGE' && item?.pageSpecsList) {
  265. topPageElements = item?.pageSpecsList[0]?.pageElementsSpecList[0]
  266. }
  267. let El = null
  268. if (mediaType === 'IMG') {
  269. El = <Image src={item.url} onClick={(e) => { e.stopPropagation() }} />
  270. } else if (mediaType === 'VIDEO') {
  271. El = <video src={item.url} style={{ width: 130, height: 100 }} controls />
  272. } else if (mediaType === 'PAGE') {
  273. switch (topPageElements?.elementType) {
  274. case 'TOP_IMAGE':
  275. topName = "顶部图片"
  276. El = <Image src={topPageElements?.topImageSpec?.imageUrl} preview={{ visible: false }} onClick={(e) => { e.stopPropagation(); setPage && setPage(1, item.id) }} />
  277. break
  278. case 'TOP_SLIDER':
  279. topName = "顶部轮播图"
  280. El = <>
  281. <Carousel autoplay style={{ width: 150, textAlign: 'center' }}>
  282. {topPageElements?.topSliderSpec?.imageUrlList?.map((url: string, index: number) => <div key={index}>
  283. <Image preview={{ visible: false }} src={url} onClick={(e) => { e.stopPropagation(); /*setImgVisible(true)*/ setPage && setPage(1, item.id) }} />
  284. </div>)}
  285. </Carousel>
  286. {/* <div style={{ display: 'none' }}>
  287. <Image.PreviewGroup preview={{ visible: imgVisible, onVisibleChange: vis => setImgVisible(vis) }}>
  288. {topPageElements?.topSliderSpec?.imageUrlList?.map((item: string, index: number) => <Image src={item} key={index} />)}
  289. </Image.PreviewGroup>
  290. </div> */}
  291. </>
  292. break
  293. case 'TOP_VIDEO':
  294. topName = "顶部视频"
  295. El = <div className={style.pageVideo}>
  296. <span className={style.pagePreview} onClick={(e) => { e.stopPropagation(); setPage && setPage(1, item.id) }}><EyeOutlined /> 预览</span>
  297. <video src={topPageElements?.topVideoSpec?.videoUrl} style={{ width: 130, height: 100 }} controls />
  298. </div>
  299. break
  300. }
  301. }
  302. return <Popconfirm
  303. title="确定要删除吗?"
  304. onConfirm={() => { dels(item.id) }}
  305. okText="是"
  306. cancelText="否"
  307. onCancel={delPupOff}
  308. visible={delPupId === item.id}
  309. key={item.id}
  310. >
  311. <Spin tip='正在请求素材详情,请耐心等待...' spinning={get?.loading}>
  312. <div
  313. className={`${style.image_box} ${!isBack ? (selectFile?.some((id: number) => id === item.id) ? style.action : '') : (selectItem?.some((item1: { url: string }) => item1.url === item.url) ? style.action : '')}`}
  314. onContextMenu={(e) => { rightMenu(e, item) }}
  315. onClick={(e) => { changeClickFile(e, item, isAll) }}
  316. {...moveConfig(item)}
  317. >
  318. <span className={`${style.select}`} onClick={(e) => { changeClickFile(e, item, isAll) }}>
  319. <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>
  320. </span>
  321. {El}
  322. <span className={style.flex_box_name} onClick={(e) => { copyId(e, item?.videoTitle || item?.title) }}>{item?.videoTitle || item?.title}</span>
  323. {mediaType === 'PAGE' ? <span>{topName}</span> : <span>{item?.width}*{item.height}</span>}
  324. </div>
  325. </Spin>
  326. </Popconfirm>
  327. }
  328. })
  329. }
  330. </div>
  331. </Spin>
  332. {/* 鼠标右键菜单 */}
  333. {rightClickPup.id ? rightClickPup.id === 'all' ? <Menu /> : <Menu isItem /> : null}
  334. {/* 新建文件弹窗 */}
  335. {fileVisible && <FileModal isAll={isAll} />}
  336. {/* 编辑排序 */}
  337. {sortVisible && <SortModal isAll={isAll} />}
  338. {/* 新建非图文 */}
  339. {imgVisrible && <ImgModal isAll={isAll} />}
  340. {imgsVisrible && <ImgsModal isAll={isAll} />}
  341. </div>
  342. {
  343. listData?.records?.length > 0 && <div className={style.pagination}>
  344. {/* 分页 */}
  345. <Pagination
  346. showSizeChanger
  347. onChange={(page: number, pageSize?: number) => {
  348. getList({ pageSize, pageNum: page })
  349. }}
  350. onShowSizeChange={(current: number, size: number) => {
  351. getList({ pageSize: size, pageNum: current })
  352. }}
  353. pageSizeOptions={['10', '20', '30', '50', '100']}
  354. current={listData?.current}
  355. defaultPageSize={30}
  356. total={listData?.total}
  357. />
  358. </div>
  359. }
  360. </div>
  361. </div>
  362. }
  363. export default React.memo(FlieBox)