123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- import React, { useEffect, useRef, useState } from 'react'
- import { Prompt } from 'react-router-dom'
- import {
- DragDropContext,
- Droppable,
- Draggable,
- DroppableProvided,
- DroppableStateSnapshot,
- DraggableProvided,
- DraggableStateSnapshot,
- DropResult,
- ResponderProvided,
- DraggingStyle,
- NotDraggingStyle
- } from 'react-beautiful-dnd'
- import useRequest from '@ahooksjs/use-request/es'
- import { useHistory } from 'react-router-dom'
- import { Modal, Popover, Upload, UploadProps, message } from 'antd'
- import { Input } from 'antd'
- import { SearchOutlined } from '@ant-design/icons'
- import { ReactComponent as File } from 'assets/file.svg'
- import { ReactComponent as Image } from 'assets/image.svg'
- import { ReactComponent as Close } from 'assets/close.svg'
- import { ReactComponent as Close2 } from 'assets/close2.svg'
- import { FilesSvg } from './ColumnEditor'
- import Picture from 'Activity/components/Picture'
- import { ColumnType } from './components/ColumnContent'
- import { MaterialService } from 'Material/Material.service'
- import { ColumnService, IDocsList, ISaveColumnDetailParams, IUploadFileRes } from './Column.service'
- import { ColumnStatus } from './components/ColumnCenter'
- import styles from './css/ColumnEditor.module.scss'
- const FroalaWrapper = React.lazy(() => import('components/FroalaWrapper'))
- interface IColumnModifyProps {
- articleId: number // 文章ID(笔记或观点的ID)
- }
- let timer: NodeJS.Timeout
- /**修改并发布-笔记/观点 */
- const ColumnModify: React.FC<IColumnModifyProps> = props => {
- const history = useHistory()
- const froalaInstanceRef = useRef<any>(null)
- const [froalaValue, setFroalaValue] = useState('') // 正文
- const [selectType, setSelectType] = useState(1) // 类型1:笔记,2:观点
- const [searchCategrayValue, setSearchCategrayValue] = useState<string>('') // 搜索行业标签
- const [searchCompanyValue, setSearchCompanyValue] = useState<string>('') // 搜索公司标签
- const [industryTags, setIndustryTags] = useState<string[]>([]) // 选中的行业标签
- const [companyTags, setCompanyTags] = useState<string[]>([]) // 选中的公司标签
- const [bigImg, setBigImg] = useState<string>() // 展开的图片
- const [title, setTitle] = useState<string>('') // 标题
- const [fileList, setFileList] = useState<IDocsList[]>([]) // 上传文件列表
- const [fileImageList, setFileImageList] = useState<IUploadFileRes[]>([]) // 上传图片列表
- const [clickType, setClickType] = useState<number>() // 记录点击操作
- const [hasModify, setHasModify] = useState(false) // 是否有变动
- useEffect(() => {
- const listener = (e: any) => {
- e.preventDefault()
- e.returnValue = '内容还未保存,确定离开吗?'
- }
- if (hasModify) {
- window.addEventListener('beforeunload', listener)
- }
- return () => {
- clearTimeout(timer)
- window.removeEventListener('beforeunload', listener)
- }
- }, [hasModify])
- useEffect(() => {
- props.articleId && getColumnDetail(props.articleId)
- getSearchIndustry('')
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.articleId])
- // 详情
- const { data: columnDetail, run: getColumnDetail } = useRequest(ColumnService.getColumnDetail, {
- manual: true,
- formatResult: response => response.data.Data,
- onSuccess: res => {
- setSelectType(res.Type)
- setTitle(res.Title)
- setFroalaValue(res.Content)
- setFileList(res.Docs)
- setIndustryTags(res.IndustryTags)
- setCompanyTags(res.CompanyTags)
- const imgList = res?.ImgUrlList?.map((item: string, index: number) => ({ Id: index, ResourceUrl: item }))
- setFileImageList(imgList || [])
- }
- })
- // 保存前的校验
- const { loading: checkLoading, run: applyCheckColumnDetail } = useRequest(ColumnService.postCheckColumnDetail, {
- manual: true,
- onSuccess: res => {
- if (res.data.Success) {
- const previewDetail = formatPreviewDetail(clickType)
- applySaveColumnDetail(previewDetail)
- } else {
- message.error(res.data.Msg || res.data.ErrMsg)
- }
- }
- })
- // 保存
- const { loading: saveLoading, run: applySaveColumnDetail } = useRequest(ColumnService.postSaveColumnDetail, {
- manual: true,
- onSuccess: res => {
- res.data.Success ? message.success(res.data.Msg) : message.error(res.data.Msg || res.data.ErrMsg)
- res.data.Success && setHasModify(false)
- res.data.Success && history.push(`/column`)
- }
- })
- const { data: industryList, run: getSearchIndustry } = useRequest(ColumnService.getSearchIndustry, {
- manual: true,
- formatResult: response => response.data.Data
- })
- const { data: companyList, run: getSearchCompanyTags } = useRequest(ColumnService.getSearchCompanyTags, {
- manual: true,
- formatResult: response => response.data.Data
- })
- const handleFormatFileList = (item: IUploadFileRes) => {
- return {
- DocName: item.ResourceName,
- DocSuffix: computeFileSvgShow(item?.ResourceName || '', 'suffix'),
- DocUrl: item.ResourceUrl
- // DocIcon?: string
- } as IDocsList
- }
- const computeFileSvgShow = (name: string, type: 'suffix' | 'svg') => {
- if (!name) return
- const suffix = name?.split('.')?.pop()
- if (type === 'suffix') return suffix
- return FilesSvg?.find(item => item.suffix === suffix)?.svg
- }
- // 上传文件
- const uploadFileprops: UploadProps = {
- name: 'file',
- accept: '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf',
- headers: {
- authorization: 'authorization-text'
- },
- multiple: true,
- itemRender: () => {
- // 不渲染
- },
- beforeUpload: file => {
- const allowedExtensions = ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf']
- const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
- if (!allowedExtensions.includes(fileExtension)) {
- message.error('只允许上传.doc, .docx, .xls, .xlsx, .ppt, .pptx 和 .pdf 文件')
- return Upload.LIST_IGNORE
- }
- },
- customRequest: async options => {
- try {
- const response = await ColumnService.postUploadFile(options.file)
- if (!response.data.Success) return message.error(response.data.Msg || response.data.ErrMsg)
- const fileItem = handleFormatFileList(response.data?.Data)
- setFileList(prevList => [...(prevList || []), fileItem])
- setHasModify(true)
- } catch (error) {
- console.log(error)
- }
- }
- }
- // 上传图片
- const uploadImageprops: UploadProps = {
- name: 'file',
- accept: 'image/*',
- headers: {
- authorization: 'authorization-text'
- },
- multiple: true,
- maxCount: 6,
- itemRender: () => {
- // 不渲染
- },
- beforeUpload: file => {
- const isPNG = file.type.toLowerCase().includes('image')
- if (!isPNG) {
- message.error(`请上传图片`)
- }
- return isPNG || Upload.LIST_IGNORE
- },
- customRequest: async options => {
- try {
- if (fileImageList.length >= 6) return message.error('最多上传6张图片')
- const response = await MaterialService.postUploadImage(options.file)
- if (!response.data.Success) return message.error(response.data.Msg || response.data.ErrMsg)
- setFileImageList(prevList => {
- if (prevList?.length >= 6) {
- message.error('最多上传6张图片')
- return prevList
- }
- return [...(prevList || []), response.data.Data]
- })
- setHasModify(true)
- } catch (error) {
- console.log(error)
- }
- }
- }
- // 删除文件
- const handleDeleteFile = (file: IDocsList, e: React.MouseEvent) => {
- e.stopPropagation()
- setFileList(prevList => prevList.filter(item => item.DocName !== file.DocName))
- setHasModify(true)
- }
- // 删除图片
- const handleDeleteImg = (img: IUploadFileRes, e: React.MouseEvent) => {
- e.stopPropagation()
- setFileImageList(prevList => prevList.filter(item => item.Id !== img.Id))
- setHasModify(true)
- }
- const handleSelectType = (type: number) => {
- setSelectType(type)
- setHasModify(true)
- }
- // 关闭图片查看
- const handleToCloseBigImg = () => {
- setBigImg(undefined)
- }
- // 图片拖拽 start
- const reorder = (list: IUploadFileRes[], startIndex: number, endIndex: number) => {
- const result = Array.from(list)
- const [removed] = result.splice(startIndex, 1)
- result.splice(endIndex, 0, removed)
- return result
- }
- const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) =>
- ({
- userSelect: 'none',
- margin: `0 20px 20px 0`,
- background: isDragging ? '#ffffff' : '#ffffff',
- ...draggableStyle
- } as React.CSSProperties)
- const getListStyle = (isDraggingOver: boolean) => ({
- background: isDraggingOver ? '#ffffff' : '#ffffff',
- display: 'flex',
- overflow: 'auto'
- })
- const onDragEnd = (result: DropResult, provided: ResponderProvided) => {
- if (!result.destination) {
- return
- }
- const reorderedList = reorder(fileImageList, result.source.index, result.destination.index)
- setFileImageList(reorderedList)
- setHasModify(true)
- }
- // 图片拖拽 end
- // 修改并发布
- const handleToClick = (type: string) => {
- if (!columnDetail?.IsApprovalAdmin) return message.error('您没有权限操作')
- if (columnDetail?.Status !== ColumnStatus.Auditing) return message.error('该文章未提交审批或已审批')
- if (checkLoading || saveLoading) return
- const typeFn: {
- [key: string]: () => void
- } = {
- modify: () => {
- Modal.confirm({
- title: '提示',
- content: '确定修改并通过此内容的审核吗?',
- okText: '确定',
- cancelText: '取消',
- centered: true,
- closable: true,
- onOk: () => {
- handleApplyCheckColumnDetail(2)
- }
- })
- }
- }
- typeFn[type]()
- }
- // 保存/发布前先校验
- const handleApplyCheckColumnDetail = (doType: number) => {
- if (!title) return message.error('请输入标题')
- if (doType === 2 && !froalaValue) return message.error('请输入正文')
- if (doType === 2 && industryTags?.length === 0 && companyTags?.length === 0)
- return message.error('请选至少选择一个标签')
- setClickType(doType)
- const imgList = getImgUrlList(froalaValue)
- const uploadImgList = fileImageList?.map(item => item.ResourceUrl)
- applyCheckColumnDetail({ Content: froalaValue, ImgUrl: imgList.concat(uploadImgList) })
- }
- // 获取正文中添加的img地址
- const getImgUrlList = (str: string) => {
- // 匹配 <img> 标签的 src 属性
- const imgRegex = /<img[^>]+src="([^">]+)"/g
- // 使用正则表达式的 exec 方法来提取匹配的内容
- let match
- const imgSrcList = []
- while ((match = imgRegex.exec(str)) !== null) {
- const src = match[1] // 提取的 src 属性值
- imgSrcList.push(src)
- }
- return imgSrcList
- }
- // 标题
- const handleInputTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
- setTitle(e.target.value)
- setHasModify(true)
- }
- // 正文编辑
- const handleChangeText = (value: string) => {
- setFroalaValue(value)
- setHasModify(true)
- }
- // 组合预览详情
- const formatPreviewDetail = (doType?: number) => {
- // 图片
- const imgList = fileImageList?.map(item => item.ResourceUrl)
- return {
- Id: props.articleId || 0,
- Content: froalaValue,
- IndustryTags: industryTags, // 行业标签
- CompanyTags: companyTags, // 公司标签
- DoType: doType || 0, // 1保存2发布
- ImgUrl: imgList,
- Docs: fileList,
- Title: title,
- Type: selectType, //类型1:笔记,2:观点
- IsApprovalPersonnel: true
- } as ISaveColumnDetailParams
- }
- // 搜索行业标签
- const handleChangeSearchCategrayValue = (e: React.ChangeEvent<HTMLInputElement>) => {
- setSearchCategrayValue(e.target.value.replace(/\s/g, ''))
- clearTimeout(timer)
- timer = setTimeout(() => {
- getSearchIndustry(e.target.value)
- }, 500)
- }
- // 搜索公司标签
- const handleChangeSearchCompanyValue = (e: React.ChangeEvent<HTMLInputElement>) => {
- setSearchCompanyValue(e.target.value.replace(/\s/g, ''))
- clearTimeout(timer)
- timer = setTimeout(() => {
- getSearchCompanyTags(e.target.value)
- }, 500)
- }
- // 删除行业标签
- const handleDeleteIndustry = (tag: string, e: React.MouseEvent) => {
- // 阻止冒泡
- e.stopPropagation()
- setIndustryTags(prevList => prevList.filter(item => item !== tag))
- setHasModify(true)
- }
- // 删除公司标签
- const handleDeleteCompany = (tag: string, e: React.MouseEvent) => {
- e.stopPropagation()
- setCompanyTags(prevList => prevList.filter(item => item !== tag))
- setHasModify(true)
- }
- // 添加公司标签
- const handleAddCompanyTag = () => {
- if (!searchCompanyValue) return message.error('请输入标签')
- if (companyTags?.includes(searchCompanyValue)) return message.error('已存在该标签')
- setCompanyTags(prevList => [...prevList, searchCompanyValue])
- setSearchCompanyValue('')
- setHasModify(true)
- }
- // 选中的标签
- const handleAddTags = (actItem: string, type: 'industry' | 'company') => {
- if (type === 'industry') {
- // 如果存在则删除
- if (industryTags?.includes(actItem)) {
- setIndustryTags(prevList => prevList.filter(item => item !== actItem))
- return
- }
- setIndustryTags(prevList => [...prevList, actItem])
- } else {
- // 如果存在则删除
- if (companyTags?.includes(actItem)) {
- setCompanyTags(prevList => prevList.filter(item => item !== actItem))
- return
- }
- setCompanyTags(prevList => [...prevList, actItem])
- }
- setHasModify(true)
- }
- return (
- <div className={styles['columnedit-page']} style={{ overflow: 'auto', height: '100%' }}>
- <Prompt message="您有未保存的数据,确定离开本页面吗?" when={hasModify} />
- <Picture visible={!!bigImg} imgSrc={bigImg} onClose={handleToCloseBigImg} />
- <div className="columnedit-top">
- <strong>发布内容</strong>
- <div className="columnedit-select">
- <span
- className={`columnedit-select-label ${
- selectType === ColumnType.Note ? 'columnedit-active' : 'columnedit-notselect'
- }`}
- onClick={handleSelectType.bind(this, ColumnType.Note)}
- >
- 笔记
- </span>
- <span
- className={`columnedit-select-label ${
- selectType === ColumnType.Viewpoint ? 'columnedit-active' : 'columnedit-notselect'
- }`}
- onClick={handleSelectType.bind(this, ColumnType.Viewpoint)}
- >
- 观点
- </span>
- </div>
- </div>
- <div className="columnedit-title-wrapper">
- <input
- type="text"
- placeholder="请输入标题"
- className="columnedit-title-input"
- value={title}
- onChange={handleInputTitle}
- />
- </div>
- <div className="columnedit-edit-wrapper">
- <FroalaWrapper
- handleUpstream={editorInstance => {
- froalaInstanceRef.current = editorInstance
- }}
- value={froalaValue}
- onChange={handleChangeText}
- configType="column"
- />
- </div>
- <div className="columnedit-edit-operate-wrapper">
- <div className="operate-file-wrapper">
- {fileList?.map((item, index) => (
- <div className="operate-file-item" key={index}>
- <div className="g-flex g-flex-v-center">
- {computeFileSvgShow(item.DocName, 'svg')}
- <span className="operate-file-text">{item.DocName}</span>
- </div>
- <Close className="delete-file" onClick={handleDeleteFile.bind(this, item)} />
- </div>
- ))}
- </div>
- <div className="operate-file-wrapper">
- <DragDropContext onDragEnd={onDragEnd}>
- <Droppable droppableId="droppable" direction="horizontal">
- {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
- <div ref={provided.innerRef} style={getListStyle(snapshot.isDraggingOver)} {...provided.droppableProps}>
- {fileImageList?.map((item, index) => (
- <Draggable key={item.Id} draggableId={item.Id.toString()} index={index}>
- {(provided2: DraggableProvided, snapshot2: DraggableStateSnapshot) => (
- <div
- ref={provided2.innerRef}
- {...provided2.draggableProps}
- {...provided2.dragHandleProps}
- style={getItemStyle(snapshot2.isDragging, provided2.draggableProps.style)}
- >
- <div className="operate-image-item">
- <img src={item?.ResourceUrl} alt="图片" className="operate-image" />
- <Close2 className="delete2-select" onClick={handleDeleteImg.bind(this, item)} />
- </div>
- </div>
- )}
- </Draggable>
- ))}
- {provided.placeholder}
- </div>
- )}
- </Droppable>
- </DragDropContext>
- </div>
- <div className="operate-btn-wrapper">
- <div className="operate-left-btn">
- <Upload {...uploadFileprops}>
- <div className="operate-btn-item operate-upload-btn">
- <File className="upload-file-icon" />
- <span className="upload-file-text">上传文件</span>
- </div>
- </Upload>
- <Upload {...uploadImageprops}>
- <div className="operate-btn-item operate-upload-btn">
- <Image className="upload-file-icon" />
- <span className="upload-file-text">上传图片</span>
- </div>
- </Upload>
- <Popover
- content={
- <div className={styles['operate-select-label']}>
- <div className="operate-select-search">
- <SearchOutlined className="com-fz16" />
- <Input
- className="operate-search-input"
- placeholder="搜索行业分类"
- size="large"
- variant="borderless"
- value={searchCategrayValue}
- onChange={handleChangeSearchCategrayValue}
- />
- </div>
- <div className="selected-line">
- <div className="select-title">
- <span>已选标签</span>
- <span className="select-tips-gray">
- {industryTags.length === 0 ? '至少添加一个标签' : `已选${industryTags.length}个标签`}
- </span>
- </div>
- <div className={`${styles['select-item-wrapper']} selected-wrapper`}>
- {industryTags?.map(item => (
- <div className="select-item active-select-item" key={item}>
- <span>{item}</span>
- <Close className="delete-select" onClick={handleDeleteIndustry.bind(this, item)} />
- </div>
- ))}
- </div>
- </div>
- <div className="will-select-line">
- <div className="select-title">标签</div>
- <div className={`${styles['select-item-wrapper']} will-select-wrapper`}>
- {industryList?.map(item => (
- <div
- className={`select-item ${industryTags.includes(item) ? 'active-select-item' : ''}`}
- key={item}
- onClick={handleAddTags.bind(this, item, 'industry')}
- >
- <span>{item}</span>
- </div>
- ))}
- </div>
- </div>
- </div>
- }
- trigger="click"
- >
- <div className="operate-btn-item operate-add-label">
- {industryTags?.length ? (
- <div className={`${styles['select-item-wrapper']} operate-add-select`}>
- {industryTags.map(item => (
- <div className="select-item " key={item}>
- <span>{item}</span>
- </div>
- ))}
- </div>
- ) : (
- <span className="add-categray-label-text">添加行业标签</span>
- )}
- </div>
- </Popover>
- <Popover
- content={
- <div className={styles['operate-select-label']}>
- <div className="operate-select-search">
- <SearchOutlined className="com-fz16" />
- <Input
- className="operate-search-input"
- placeholder="搜索公司标签"
- size="large"
- variant="borderless"
- value={searchCompanyValue}
- onChange={handleChangeSearchCompanyValue}
- />
- <div className="create-company-btn" onClick={handleAddCompanyTag}>
- <span>+</span>
- <span className="m-l-xs">创建</span>
- </div>
- </div>
- <div className="selected-line">
- <div className="select-title">
- <span>已选标签</span>
- <span className="select-tips-gray">
- {companyTags.length === 0 ? '至少添加一个标签' : `已选${companyTags.length}个标签`}
- </span>
- </div>
- <div className={`${styles['select-item-wrapper']} selected-wrapper`}>
- {companyTags?.map(item => (
- <div className="select-item active-select-item" key={item}>
- <span>{item}</span>
- <Close className="delete-select" onClick={handleDeleteCompany.bind(this, item)} />
- </div>
- ))}
- </div>
- </div>
- <div className="will-select-line">
- <div className="select-title">标签</div>
- <div className={`${styles['select-item-wrapper']} will-select-wrapper`}>
- {companyList?.map(item => (
- <div
- className={`select-item ${companyTags.includes(item) ? 'active-select-item' : ''}`}
- key={item}
- onClick={handleAddTags.bind(this, item, 'company')}
- >
- <span>{item}</span>
- </div>
- ))}
- </div>
- </div>
- </div>
- }
- trigger="click"
- >
- <div className="operate-btn-item operate-add-label">
- {companyTags?.length ? (
- <div className={`${styles['select-item-wrapper']} operate-add-select`}>
- {companyTags?.map(item => (
- <div className="select-item " key={item}>
- <span>{item}</span>
- </div>
- ))}
- </div>
- ) : (
- <span className="add-company-label-text">添加公司标签</span>
- )}
- </div>
- </Popover>
- </div>
- <div className="operate-right-btn">
- <div className="operate-nomal-btn publish-btn" onClick={handleToClick.bind(this, 'modify')}>
- 修改并发布
- </div>
- </div>
- </div>
- </div>
- </div>
- )
- }
- export default ColumnModify
|