From 97d92002174773cbf5d61331d20124d4286504d1 Mon Sep 17 00:00:00 2001 From: callmeyan Date: Sat, 14 Dec 2024 16:28:01 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=E7=BC=96=E8=BE=91=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E8=B0=83=E6=95=B4=E4=B8=BA=E6=96=B0=E7=9A=84=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/article/article.module.scss | 72 +++++++---- src/components/article/block.tsx | 131 ++++++++++----------- src/components/article/edit-modal.tsx | 88 ++++++++++---- src/components/article/group.tsx | 38 +++++- src/components/article/item.tsx | 34 ++++-- src/components/article/list.tsx | 42 +++++++ src/pages/news/edit.tsx | 18 +-- src/service/api/article.ts | 4 +- src/service/api/video.ts | 2 +- 9 files changed, 291 insertions(+), 138 deletions(-) create mode 100644 src/components/article/list.tsx diff --git a/src/components/article/article.module.scss b/src/components/article/article.module.scss index 349a72a..1a06d04 100644 --- a/src/components/article/article.module.scss +++ b/src/components/article/article.module.scss @@ -1,5 +1,5 @@ .blockContainer { - @apply flex mb-5; + @apply flex mb-5; } .block { @@ -14,6 +14,10 @@ } } +.blockFist { + @apply p-0 border-0 !important; +} + .blockItem { } @@ -21,40 +25,61 @@ .group { } -.image { - @apply border border-blue-200 p-2 flex-1 rounded focus:border-blue-200; - min-height: 100px; - &:hover{ - @apply border-blue-500; - } - :global{ - .ant-upload-wrapper{ +.imageList { + @apply grid grid-cols-4 gap-4 p-3 border border-blue-200; + :global { + .ant-upload-wrapper { display: block; border: none; padding: 0; } - .ant-upload{ + + .ant-upload { display: block; } + + img { + @apply block m-0; + max-width: 100%; + height: 100px; + object-fit: contain; + padding: 2px; + } } } +.image { + @apply rounded bg-gray-100; + height: 100px; + + &:hover { + @apply border-blue-500; + } +} +.imageDelete{ + @apply absolute flex items-center justify-center p-0.5 w-[22px] h-[22px] rounded-full border border-red-500 text-red-500 cursor-pointer z-10; + right:-10px; + top:-10px; + font-size: 14px; + &:hover{ + @apply text-white bg-red-500; + } +} .uploadImage { - @apply flex justify-center items-center relative; - img { - display: block; - max-width: 100%; - max-height: 200px; + @apply flex justify-center items-center relative h-[100px] text-gray-400; + + .uploadTips { + @apply absolute inset-0 cursor-pointer opacity-0 transition rounded flex items-center justify-center bg-black/20 text-white; } - .uploadTips{ - @apply absolute inset-0 cursor-pointer opacity-0 rounded flex items-center justify-center bg-black/50 text-white; - } - .imagePlaceholder{ + + .imagePlaceholder { @apply flex items-center justify-center; height: 100px; } - &:hover{ - .uploadTips{ + + &:hover { + @apply bg-gray-100 cursor-pointer rounded text-blue-500; + .uploadTips { @apply opacity-100; } } @@ -62,10 +87,11 @@ .text { @apply border border-blue-200 overflow-hidden flex-1 rounded focus:border-blue-200 transition; - &:hover{ + &:hover { @apply border-blue-500; } - &:focus-within{ + + &:focus-within { @apply border-blue-500 shadow-md; } } diff --git a/src/components/article/block.tsx b/src/components/article/block.tsx index 65882e6..1553d3e 100644 --- a/src/components/article/block.tsx +++ b/src/components/article/block.tsx @@ -1,24 +1,56 @@ import React from "react"; import clsx from "clsx"; +import {Popconfirm, Space} from "antd"; -import {IconAdd, IconAddImage, IconAddText, IconDelete} from "@/components/icons"; -import {BlockImage, BlockText} from "./item.tsx"; +import {IconAdd, IconDelete} from "@/components/icons"; +import ImageList from "@/components/article/list.tsx"; +import { BlockText} from "./item.tsx"; import styles from './article.module.scss' -import {Button, Popconfirm} from "antd"; type Props = { children?: React.ReactNode; - index?:number; + index?: number; className?: string; blocks: BlockContent[]; editable?: boolean; onChange?: (blocks: BlockContent[]) => void; onRemove?: () => void; onAdd?: () => void; + errorMessage?: string; } -export default function ArticleBlock({className, blocks, editable, onRemove, onAdd, onChange,index}: Props) { +function rebuildBlockArray(blocks: BlockContent[]) { + const textBlock: BlockContent = { + type: 'text', + content: '' + } + const _blocks: BlockContent[] = [textBlock]; + const textArray: string[] = [] + blocks.forEach(it => { + if (it.type == 'text') { + textArray.push(it.content) + } else { + _blocks.push(it) + } + }) + textBlock.content = textArray.join('\n') + return _blocks +} + + +export default function ArticleBlock( + { + className, + blocks: defaultBlocks, + editable, + onRemove, + onAdd, + onChange, + index, + errorMessage + }: Props) { + const blocks = rebuildBlockArray(defaultBlocks) const handleBlockRemove = (index: number) => { // 删除当前项 onChange?.(blocks.filter((_, idx) => index !== idx)) @@ -43,72 +75,39 @@ export default function ArticleBlock({className, blocks, editable, onRemove, onA } return
-
+
- {blocks.map((it, idx) => { - const isFirstTextBlock = index == 0 && it.type ==='text' && firstTextBlockIndex == idx - return (
-
- { - it.type === 'text' - ? handleBlockChange(idx, block)} data={it} - editable={editable}/> - : - } - {editable &&
- {isFirstTextBlock?: - 请确认删除此{it.type === 'text' ? '文本' : '图片'}? -
} - onConfirm={() => handleBlockRemove(idx)} - okText="删除" - cancelText="取消" - > - - - - } -
- handleAddBlock('text', idx + 1)} - className="article-action-icon" title="新增文本"> - handleAddBlock('image', idx + 1)} - className="article-action-icon mt-1" title="新增图片"> -
-
} -
- {isFirstTextBlock &&
该编辑框内容由数字人播报
} -
) - } - )} - {editable && blocks.length == 0 && -
-
- - -
-
- } +
+
+ handleBlockChange(0, block)} + data={blocks[0]} + isFirstBlock={index == 0} + editable={editable}/> +
+ {index == 0 &&
+
{errorMessage}
+
该编辑框内容由数字人播报
+
} +
+ {index > 0 && }
{editable &&
- - 请确认删除此删除此分组? -
} - onConfirm={onRemove} - okText="删除" - cancelText="取消" - > - - + { + index > 0 ? 请确认删除此删除此分组?
} + onConfirm={onRemove} + okText="删除" + cancelText="取消" + > + + + + : + } } diff --git a/src/components/article/edit-modal.tsx b/src/components/article/edit-modal.tsx index 6a731b6..c658da7 100644 --- a/src/components/article/edit-modal.tsx +++ b/src/components/article/edit-modal.tsx @@ -2,11 +2,13 @@ import {Input, Modal} from "antd"; import ArticleGroup from "@/components/article/group.tsx"; import {useEffect, useState} from "react"; import {useSetState} from "ahooks"; -import {getById} from "@/service/api/article.ts"; +import * as article from "@/service/api/article.ts"; +import {regenerate} from "@/service/api/video.ts"; type Props = { id?: number; - onClose?: () => void; + type: 'news' | 'video'; + onClose?: (saved?: boolean) => void; } export default function ArticleEditModal(props: Props) { @@ -16,29 +18,59 @@ export default function ArticleEditModal(props: Props) { const [state, setState] = useSetState({ loading: false, - open: false + open: false, + msgTitle: '', + msgGroup: '', }) + // 保存数据 const handleSave = () => { + console.log(groups, title) + if (!title) { + // setState({msgTitle: '请输入标题内容'}); + return; + } + if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) { + // setState({msgGroup: '请输入正文文本内容'}); + return; + } + const save = props.type == 'news' ? article.save : regenerate + setState({loading: true}) + save(title, groups, props.id > 0 ? props.id : undefined).then(() => { + props.onClose?.(true) + }).finally(() => { + setState({loading: false}) + }); props.onClose?.() - // if (props.onSave) { - // setState({loading: true}) - // props.onSave?.().then(() => { - // setState({loading: false, open: false}) - // }) - // } else { - // console.log(groups) - // } } useEffect(() => { - if(props.id){ - if(props.id > 0){ - getById(props.id).then(res => { - setGroups(res.content_group) - setTitle(res.title) - }) - }else{ - setGroups([]) - setTitle('') + if (props.id) { + if (props.id > 0) { + if (props.type == 'news') { + article.getById(props.id).then(res => { + setGroups(res.content_group) + setTitle(res.title) + }) + } + } else { + // 新增 + setGroups([ + [{ + type: 'text', + content: '韩国国会当地时间14日16时举行全体会议,就在野党阵营第二次提出的尹锡悦总统弹劾案进行表决。根据投票结果,有204票赞成,85票反对,3票弃权,8票无效,弹劾案最终获得通过,尹锡悦的总统职务立即停止。' + }], + [ + { + type: 'text', + content: '韩国宪法法院将在180天内完成弹劾案审判程序。如果宪法法院作出弹劾案不成立的裁决,尹锡悦将立即恢复总统职务;如果宪法法院认可弹劾案成立,尹锡悦将立即被罢免,预计韩国将在明年4月至6月间举行大选。' + }, + { + type: 'image', + content: 'https://zverse-on.oss-cn-shanghai.aliyuncs.com/metahuman/workbench/20241214/193c442df75.jpeg' + }, + + ], + ]) + setTitle('韩国国会通过总统弹劾案 尹锡悦职务立即停止') } } }, [props.id]) @@ -49,7 +81,7 @@ export default function ArticleEditModal(props: Props) { maskClosable={false} keyboard={false} width={800} - onCancel={props.onClose} + onCancel={()=>props.onClose?.()} okButtonProps={{loading: state.loading}} onOk={handleSave} > @@ -59,18 +91,26 @@ export default function ArticleEditModal(props: Props) { *
- { + { setTitle(e.target.value) + setState({msgTitle: e.target.value ? '' : '请输入标题内容'}) }} placeholder={'请输入文章标题'}/>
+
{state.msgTitle}
-
+
正文 *
- setGroups(() => list)}/> + { + setGroups(() => list) + setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? '请输入正文文本内容' : ''}); + }} + />
); diff --git a/src/components/article/group.tsx b/src/components/article/group.tsx index 20963f8..cf2158d 100644 --- a/src/components/article/group.tsx +++ b/src/components/article/group.tsx @@ -1,22 +1,46 @@ - import {message} from "antd" import ArticleBlock from "@/components/article/block.tsx"; import styles from './article.module.scss' +import {showToast} from "@/components/message.ts"; type Props = { groups: BlockContent[][]; editable?: boolean; onChange?: (groups: BlockContent[][]) => void; + errorMessage?: string; } -export default function ArticleGroup({groups, editable, onChange}: Props) { + +function rebuildGroups(groups: BlockContent[][]) { + if (groups.length < 2) { + Array(2 - groups.length).fill([{type: 'text', content: ''}]).forEach((it) => { + groups.push(it) + }) + } + + return groups; + + +} + +export default function ArticleGroup({groups: _groups, editable, onChange,errorMessage}: Props) { + const groups = rebuildGroups(_groups) /** * 添加一个组 * @param insertIndex 插入的位置,-1表示插入到末尾 */ - const handleAddGroup = ( insertIndex: number = -1) => { - const newGroup: BlockContent[] = [] - const _groups = [...groups] + const handleAddGroup = (insertIndex: number = -1) => { + if (insertIndex !== -1 && insertIndex !== 1) { + const triggerGroup = insertIndex == -1 || insertIndex >= groups.length ? groups[groups.length - 1] : groups[insertIndex - 1]; + // 判断 + if (triggerGroup.length == 0 || triggerGroup.some(s => !s.content)) { + showToast('请先添加内容') + return; + } + } + + const newGroup: BlockContent[] = [{type: 'text', content: ''}] + const _groups = [...groups]; if (insertIndex == -1 || insertIndex >= groups.length) { // -1或者越界表示新增 _groups.push(newGroup) } else { @@ -34,6 +58,7 @@ export default function ArticleGroup({groups, editable, onChange}: Props) { groups[index] = blocks onChange?.([...groups]) }} + errorMessage={errorMessage} index={index} onAdd={() => { handleAddGroup?.(index + 1) @@ -48,6 +73,7 @@ export default function ArticleGroup({groups, editable, onChange}: Props) { /> ))} {groups.length == 0 && editable && - onChange?.([blocks])} index={0} blocks={[{type:'text',content:''}]}/>} + onChange?.([blocks])} index={0} + blocks={[{type: 'text', content: ''}]}/>}
} \ No newline at end of file diff --git a/src/components/article/item.tsx b/src/components/article/item.tsx index 963cfbf..a1f367d 100644 --- a/src/components/article/item.tsx +++ b/src/components/article/item.tsx @@ -1,10 +1,12 @@ import React, {useState} from "react"; -import {Button, Input, Spin, Upload, UploadProps} from "antd"; +import {Input, Popconfirm, Spin, Upload, UploadProps} from "antd"; +import {CloseOutlined,CloudUploadOutlined} from "@ant-design/icons"; +import {clsx} from "clsx"; import styles from './article.module.scss' import {getOssPolicy} from "@/service/api/common.ts"; import {showToast} from "@/components/message.ts"; -import {clsx} from "clsx"; +import {IconAddImage} from "@/components/icons"; type Props = { children?: React.ReactNode; @@ -14,11 +16,15 @@ type Props = { onChange?: (data: BlockContent) => void; isFirstBlock?: boolean; } +type ImageProps = { + onRemove?: () => void; + onlyUpload?: boolean; +} & Props; const MimeTypes = ['image/jpeg', 'image/png', 'image/jpg'] const Data: { uploadConfig?: TOSSPolicy } = {} -export function BlockImage({data, editable, onChange}: Props) { +export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: ImageProps) { const [loading, setLoading] = useState(-1) // oss上传文件所需的数据 @@ -48,7 +54,7 @@ export function BlockImage({data, editable, onChange}: Props) { console.log('onChange', file); if (file.status == 'done') { setLoading(-1) - onChange?.({type: 'image', content: Data.uploadConfig?.host + file.url}) + onChange?.({type: 'image', content: Data.uploadConfig?.host + '/' + file.url}) } else if (file.status == 'error') { setLoading(-1) showToast('上传图片失败,请重试', 'warning') @@ -58,7 +64,15 @@ export function BlockImage({data, editable, onChange}: Props) { } // return
- {editable ?
+ {editable ?
+ {!onlyUpload && 请确认删除此删除此图片?
} + onConfirm={onRemove} + okText="删除" + cancelText="取消" + > + + } = 0} percent={loading == 0 ? 'auto' : loading}> 更换图片
:
- +
+ +
上传图片
+
}
@@ -84,13 +101,14 @@ export function BlockImage({data, editable, onChange}: Props) { export function BlockText({data, editable, onChange, isFirstBlock}: Props) { return
-
+
{editable ?
{ onChange?.({type: 'text', content: e.target.value}) }} - placeholder={'请输入文本'} value={data.content} autoSize={{minRows: 3, maxRows: 8}} + placeholder={'请输入文本内容'} value={data.content} autoSize={{minRows: 3, maxRows: 8}} variant={"borderless"}/>
:

{data.content}

}
diff --git a/src/components/article/list.tsx b/src/components/article/list.tsx new file mode 100644 index 0000000..2a0bb6f --- /dev/null +++ b/src/components/article/list.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +import {BlockImage} from "@/components/article/item.tsx"; + +import styles from './article.module.scss' + +export default function ImageList(props: { + blocks: BlockContent[]; + editable?: boolean; + onChange?: (blocks: BlockContent[]) => void; +}) { + + // 处理删除 + const handleRemove = (index: number) => { + props.onChange?.(props.blocks.filter((_, idx) => index !== idx)) + const newBlocks = [...props.blocks] + newBlocks.splice(index, 1) + props.onChange?.(newBlocks) + } + // 处理新增 + const handleAdd = (data: BlockContent) => { + props.onChange?.([...props.blocks, data]) + } + // 处理更新 + const handleUpdate = (index: number, data: BlockContent) => { + props.onChange?.(props.blocks.map((it, idx) => idx === index ? data : it)) + } + + + return (
+ {props.blocks.map((it, index) => ( + it.type === 'image' ? handleUpdate(index, data)} + onRemove={() => handleRemove(index)} + /> : null + ))} + {props.editable && + } +
) +} + diff --git a/src/pages/news/edit.tsx b/src/pages/news/edit.tsx index 4baf032..924ff8b 100644 --- a/src/pages/news/edit.tsx +++ b/src/pages/news/edit.tsx @@ -14,12 +14,9 @@ export default function NewEdit() { const [editId, setEditId] = useState(-1) const [selectedRowKeys, setSelectedRowKeys] = useState([]) const [params, setParams] = useState({ - pagination: { - page: 1, - limit: 10 - } + pagination: {page: 1, limit: 10} }) - const {data} = useRequest(() => getList(params), {refreshDeps: [params]}) + const {data, refresh} = useRequest(() => getList(params), {refreshDeps: [params]}) const columns: TableColumnsType = [ { @@ -86,15 +83,20 @@ export default function NewEdit() { showSizeChanger={false} simple={true} rootClassName={'simple-pagination'} - onChange={(page) => setParams(prev=>({ + onChange={(page) => setParams(prev => ({ ...prev, pagination: {page, limit: 10} }))} /> - +
}
- setEditId(-1)}/> + { + setEditId(-1) + if (saved) refresh() + }}/> ) } \ No newline at end of file diff --git a/src/service/api/article.ts b/src/service/api/article.ts index 7fb948c..22d963a 100644 --- a/src/service/api/article.ts +++ b/src/service/api/article.ts @@ -21,9 +21,9 @@ export function getById(id: Id) { return post({url: '/article/detail/' + id}) } -export function save(title: string, content_group: BlockContent[][], id: number) { +export function save(title: string, content_group: BlockContent[][], id?: number) { return post<{ content: string }>({ - url: '/spider/article', + url: '/article/save', data: { title, content_group, diff --git a/src/service/api/video.ts b/src/service/api/video.ts index f8273a7..2069047 100644 --- a/src/service/api/video.ts +++ b/src/service/api/video.ts @@ -13,7 +13,7 @@ export function getList(data: { * @param content_group * @param article_id */ -export function regenerate(title: string, content_group: BlockContent[][], article_id: number) { +export function regenerate(title: string, content_group: BlockContent[][], article_id?: Id) { return post<{ content: string }>({ url: '/video/regenerate', data: { From c63b0c088eeba7dd21efa3039458f18ef7f88bcb Mon Sep 17 00:00:00 2001 From: callmeyan Date: Sat, 14 Dec 2024 22:10:11 +0800 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E7=9B=B8=E5=85=B3=E6=95=B0=E5=AD=97=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/message.ts | 5 + src/components/video/video-list-item.tsx | 10 +- src/pages/library/components/search-form.tsx | 15 ++- src/pages/live/index.tsx | 102 ++++++++++++++++-- src/pages/live/style.module.scss | 3 + .../news/components/button-push2video.tsx | 18 ++-- src/pages/news/index.tsx | 2 +- .../video/components/button-push2room.tsx | 31 ++++++ src/pages/{create => video}/index.tsx | 38 ++++--- src/routes/routes.tsx | 2 +- src/service/api/article.ts | 15 +-- src/service/api/live.ts | 20 ++++ src/service/api/video.ts | 15 ++- src/service/request.ts | 14 ++- src/types/api.d.ts | 12 +++ src/util/strings.ts | 11 ++ 16 files changed, 243 insertions(+), 70 deletions(-) create mode 100644 src/pages/live/style.module.scss create mode 100644 src/pages/video/components/button-push2room.tsx rename src/pages/{create => video}/index.tsx (81%) create mode 100644 src/service/api/live.ts diff --git a/src/components/message.ts b/src/components/message.ts index 00d304f..80b258c 100644 --- a/src/components/message.ts +++ b/src/components/message.ts @@ -1,4 +1,5 @@ import {message} from "antd"; +import {BizError} from "@/service/types.ts"; export function showToast(content: string, type?: 'success' | 'info' | 'warning' | 'error') { @@ -8,6 +9,10 @@ export function showToast(content: string, type?: 'success' | 'info' | 'warning' className: 'aui-toast' }).then(); } +export function showErrorToast(e:Error|BizError) { + showToast(String(((e instanceof BizError)?e.data:'') || e.message),'error') +} + export function showLoading(content = 'Loading...') { const key = 'globalLoading_' + (new Date().getTime()); diff --git a/src/components/video/video-list-item.tsx b/src/components/video/video-list-item.tsx index 3a4a9fa..cb13bbd 100644 --- a/src/components/video/video-list-item.tsx +++ b/src/components/video/video-list-item.tsx @@ -8,7 +8,7 @@ import {IconEdit, IconPlay} from "@/components/icons"; import {Popconfirm} from "antd"; type Props = { - video: VideoInfo, + video: VideoInfo | LiveVideoInfo, editable?: boolean; sortable?: boolean; index?: number; @@ -25,7 +25,6 @@ export const VideoListItem = ( { index, id, video, onPlay, onRemove, checked, onCheckedChange, onEdit, active, editable, - }: Props) => { const { attributes, listeners, @@ -42,14 +41,13 @@ export const VideoListItem = ( className={'video-item flex items-center gap-3 mb-5'} ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}> {index && index > 0 &&
-
{id}
+
{index}
}
-
{video.title}
+
{video.title || video.video_title}
- {video.title}/ + {video.video_title}/
{editable && diff --git a/src/pages/library/components/search-form.tsx b/src/pages/library/components/search-form.tsx index c3ddf40..5835daf 100644 --- a/src/pages/library/components/search-form.tsx +++ b/src/pages/library/components/search-form.tsx @@ -17,9 +17,9 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) { timeRange: string; keywords: string; searching: boolean; - time: string; + time: number; }>({ - keywords: "", searching: false, timeRange: "", time: '-1' + keywords: "", searching: false, timeRange: "", time: 0 }) const onFinish = (values: any) => { setState({searching: true}) @@ -52,12 +52,11 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) { {/**/} {/* */} {/**/} - {/**/} - {/* */} - {/* */} - {/* */} - {/* */} - {/**/} + + + + + diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index 9b0504e..a67c09d 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -1,15 +1,99 @@ -import React, {useState} from "react"; +import React, {useEffect, useState} from "react"; import {Button, message, Modal} from "antd"; import {SortableContext, arrayMove} from '@dnd-kit/sortable'; import {DndContext} from "@dnd-kit/core"; import {VideoListItem} from "@/components/video/video-list-item.tsx"; +import {getList} from "@/service/api/live.ts"; + +import styles from './style.module.scss' export default function LiveIndex() { - const [videoData, setVideoData] = useState() + const [videoData, setVideoData] = useState([]) const [modal, contextHolder] = Modal.useModal() const [checkedIdArray, setCheckedIdArray] = useState([]) - const [editable,setEditable] = useState(false) + const [editable, setEditable] = useState(false) + + const [state, setState] = useState({ + activeId: -1, + }) + + useEffect(() => { + getList().then(res => { + setVideoData([ + { + id: 1, + video_id: 1, + video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', + cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', + video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', + video_duration: 100, + status: 1, + order_no: '1' + }, + { + id: 2, + video_id: 1, + video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', + cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', + video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', + video_duration: 100, + status: 1, + order_no: '1' + }, + { + id: 3, + video_id: 1, + video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', + cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', + video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', + video_duration: 100, + status: 1, + order_no: '1' + }, + { + id: 4, + video_id: 1, + video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', + cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', + video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', + video_duration: 100, + status: 1, + order_no: '1' + }, + { + id: 5, + video_id: 1, + video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', + cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', + video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', + video_duration: 100, + status: 1, + order_no: '1' + }, + { + id: 6, + video_id: 1, + video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', + cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', + video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', + video_duration: 100, + status: 1, + order_no: '1' + }, + { + id: 7, + video_id: 1, + video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', + cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', + video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', + video_duration: 100, + status: 1, + order_no: '1' + } + ]) + }) + }, []) const processDeleteVideo = async (_idArray: number[]) => { message.info('删除成功!!!' + _idArray.join('')); } @@ -42,19 +126,19 @@ export default function LiveIndex() { -
+
- {editable ?<> + {editable ? <>
- +
批量删除
- :
- + :
+
}
@@ -86,7 +170,7 @@ export default function LiveIndex() { video={v} index={index + 1} id={v.id} - active={index == 0} + active={state.activeId == v.id} key={index} onCheckedChange={(checked) => { setCheckedIdArray(idArray => { diff --git a/src/pages/live/style.module.scss b/src/pages/live/style.module.scss new file mode 100644 index 0000000..5ba3fb7 --- /dev/null +++ b/src/pages/live/style.module.scss @@ -0,0 +1,3 @@ +.videoListContainer{ + +} \ No newline at end of file diff --git a/src/pages/news/components/button-push2video.tsx b/src/pages/news/components/button-push2video.tsx index c391851..42904e8 100644 --- a/src/pages/news/components/button-push2video.tsx +++ b/src/pages/news/components/button-push2video.tsx @@ -1,26 +1,26 @@ import {Button, Modal} from "antd"; import React, {useState} from "react"; -import {showToast} from "@/components/message.ts"; -import {push2article} from "@/service/api/news.ts"; +import {showErrorToast, showToast} from "@/components/message.ts"; +import {push2video} from "@/service/api/article.ts"; -export default function ButtonPush2Video(props: { ids: Id[]}){ - const [loading,setLoading] = useState(false) - const handlePush = ()=>{ +export default function ButtonPush2Video(props: { ids: Id[] }) { + const [loading, setLoading] = useState(false) + const handlePush = () => { setLoading(true) - push2article(props.ids).then(()=>{ + push2video(props.ids).then(() => { showToast('一键推流成功,已成功推入数字人视频生成,请前往数字人视频生成页面查看!', 'success') - }).finally(()=>{ + }).catch(showErrorToast).finally(() => { setLoading(false) }) } - const onPushClick = ()=>{ + const onPushClick = () => { if (props.ids.length === 0) { showToast('请选择要开播的新闻', 'warning') return } Modal.confirm({ - title:'操作提示', + title: '操作提示', content: '是否确定一键开播选中新闻?', onOk: handlePush }) diff --git a/src/pages/news/index.tsx b/src/pages/news/index.tsx index 8a8720c..19a2d2c 100644 --- a/src/pages/news/index.tsx +++ b/src/pages/news/index.tsx @@ -95,7 +95,7 @@ export default function NewsIndex() {
{ handleViewNewsDetail(item.id) - }}>{item.id}{item.title}
+ }}>{item.title}
{item.internal_article_id > 0 &&
已加入编辑界面
}
diff --git a/src/pages/video/components/button-push2room.tsx b/src/pages/video/components/button-push2room.tsx new file mode 100644 index 0000000..ff069fe --- /dev/null +++ b/src/pages/video/components/button-push2room.tsx @@ -0,0 +1,31 @@ +import {Button, Modal} from "antd"; +import React, {useState} from "react"; +import {showErrorToast, showToast} from "@/components/message.ts"; +import {push2room} from "@/service/api/video.ts"; + + +export default function ButtonPush2Room(props: { ids: Id[]}){ + const [loading,setLoading] = useState(false) + const handlePush = ()=>{ + setLoading(true) + push2room(props.ids).then(()=>{ + showToast('一键推流成功,已推流至数字人直播间,请前往数字人直播间页面查看!', 'success') + }).catch(showErrorToast).finally(()=>{ + setLoading(false) + }) + } + const onPushClick = ()=>{ + if (props.ids.length === 0) { + showToast('请选择要推流的新闻', 'warning') + return + } + Modal.confirm({ + title:'操作提示', + content: '是否确定一键推流选中新闻视频??', + onOk: handlePush + }) + } + return ( + + ) +} \ No newline at end of file diff --git a/src/pages/create/index.tsx b/src/pages/video/index.tsx similarity index 81% rename from src/pages/create/index.tsx rename to src/pages/video/index.tsx index bb2f43b..985e2f7 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/video/index.tsx @@ -1,7 +1,5 @@ -import {Button, message, Modal} from "antd"; -import React, {useEffect, useRef, useState} from "react"; - -import {ArticleGroupList, MockVideoDataList} from "@/_local/mock-data"; +import {message, Modal} from "antd"; +import React, {useEffect, useMemo, useRef, useState} from "react"; import {DndContext} from "@dnd-kit/core"; import {arrayMove, SortableContext} from "@dnd-kit/sortable"; import {VideoListItem} from "@/components/video/video-list-item.tsx"; @@ -10,19 +8,18 @@ import {useSetState} from "ahooks"; import {CheckCircleFilled} from "@ant-design/icons"; import {clsx} from "clsx"; import {getList} from "@/service/api/video.ts"; +import {formatDuration} from "@/util/strings.ts"; +import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx"; -export default function CreateIndex() { - const [editNews, setEditNews] = useSetState<{ - title?: string; - groups?: ArticleContentGroup[]; - }>({}) +export default function VideoIndex() { + const [editId, setEditId] = useState(-1) const [videoData, setVideoData] = useState([]) useEffect(() => { - getList({}).then((ret) => { - setVideoData(ret.list) + getList().then((ret) => { + setVideoData(ret.list || []) }) }, []) @@ -63,13 +60,19 @@ export default function CreateIndex() { }) } + const totalDuration = useMemo(() => { + if(!videoData || videoData.length == 0) return 0; + // 计算总时长 + return videoData.reduce((sum, v) => sum + v.duration, 0); + }, [videoData]) + return (
{contextHolder}
- 视频时长: 00:00:29 + 视频时长: {formatDuration(totalDuration)}
批量删除 @@ -99,6 +102,7 @@ export default function CreateIndex() { } }}> + {videoData.map((v, index) => ( playVideo(v)} onEdit={() => { - setEditNews({title: v.title, groups: [...ArticleGroupList]}) + setEditId(v.article_id) }} editable />))}
- +
-
+
预览视频
-
+
- + setEditId(-1)}/>
) } \ No newline at end of file diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx index d2a2dc2..9ae6fdd 100644 --- a/src/routes/routes.tsx +++ b/src/routes/routes.tsx @@ -1,7 +1,7 @@ import {RouteObject} from "react-router-dom"; import ErrorBoundary from "@/routes/error.tsx"; import UserAuth from "@/pages/user"; -import CreateIndex from "@/pages/create"; +import CreateIndex from "../pages/video"; import LibraryIndex from "@/pages/library"; import LiveIndex from "@/pages/live"; import NewsIndex from "@/pages/news"; diff --git a/src/service/api/article.ts b/src/service/api/article.ts index 7fb948c..cf3b9ef 100644 --- a/src/service/api/article.ts +++ b/src/service/api/article.ts @@ -22,12 +22,13 @@ export function getById(id: Id) { } export function save(title: string, content_group: BlockContent[][], id: number) { - return post<{ content: string }>({ - url: '/spider/article', - data: { - title, - content_group, - id - } + return post<{ content: string }>(id && id > 0 ? '/article/modify' : '/article/create/new', { + title, + content_group, + id }) +} + +export function push2video(article_ids: Id[]) { + return post('/article/push2video', {article_ids}) } \ No newline at end of file diff --git a/src/service/api/live.ts b/src/service/api/live.ts new file mode 100644 index 0000000..36b58d2 --- /dev/null +++ b/src/service/api/live.ts @@ -0,0 +1,20 @@ +import {post} from "@/service/request.ts"; + +export function playState() { + return post<{ + id: number; + start_time?: number; + }>({url: '/room/playing'}) +} + +export function getList() { + return post>('/room/list') +} + +export function modifyOrder(ids: Id[]) { + return post('/video/order', {ids}) +} + +export function deleteById(ids: Id[]) { + return post('/video/remove', {ids}) +} \ No newline at end of file diff --git a/src/service/api/video.ts b/src/service/api/video.ts index f8273a7..c9100c1 100644 --- a/src/service/api/video.ts +++ b/src/service/api/video.ts @@ -1,10 +1,7 @@ import {post} from "@/service/request.ts"; -export function getList(data: { - title?: string, - time_flag?: number; -}) { - return post>({url: '/video/list', data}) +export function getList() { + return post>('/video/list') } /** @@ -31,9 +28,11 @@ export function getById(id: Id) { export function deleteById(id: Id) { return post({url: '/video/detail/' + id}) } + export function modifyOrder(ids: Id[]) { - return post({url: ' /video/modifyorder',data:{ids}}) + return post('/video/modifyorder', {ids}) } -export function push2room(ids: Id[]) { - return post({url: ' /video/push2room',data:{ids}}) + +export function push2room(video_ids: Id[]) { + return post('/video/push2room', {video_ids}) } \ No newline at end of file diff --git a/src/service/request.ts b/src/service/request.ts index 676248f..77bba4a 100644 --- a/src/service/request.ts +++ b/src/service/request.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import {stringify} from 'qs' import {BizError} from './types'; import {getAuthToken} from "@/hooks/useAuth.ts"; +import {showToast} from "@/components/message.ts"; const JSON_FORMAT: string = 'application/json'; const REQUEST_TIMEOUT = 300000; // 超时时长5min @@ -23,6 +24,7 @@ Axios.interceptors.request.use(config => { } return config }, err => { + console.log('请求拦截器报错',err) return Promise.reject(err) }) @@ -46,11 +48,11 @@ export function request(options: RequestOption) { return; } // const - const {code, message, data, request_id} = res.data + const {code, msg, data, trace_id} = res.data if (code == 0) { resolve(data as unknown as T) } else { - reject(new BizError(message, code, request_id, data as unknown as AllType)) + reject(new BizError(msg, code, trace_id, data as unknown as AllType)) } }).catch(e => { reject(new BizError(e.message, 500)) @@ -59,9 +61,13 @@ export function request(options: RequestOption) { } -export function post(params: RequestOption) { +export function post(params: RequestOption | string, _data?: AllType) { + const options = typeof params === 'string' ? {url: params} : params; + if (_data) { + options.data = _data + } return request({ - ...params, + ...options, method: 'post' }) } diff --git a/src/types/api.d.ts b/src/types/api.d.ts index ce4ca4b..1f7968b 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -89,3 +89,15 @@ declare interface VideoInfo { article_id: number; status: number; } +// room live +declare interface LiveVideoInfo { + id: number; + video_id: number; + video_title: string; + cover_url: string; + video_duration: number; + video_oss_url: string; + status: number; + order_no: string; +} + diff --git a/src/util/strings.ts b/src/util/strings.ts index 508d563..69cc62a 100644 --- a/src/util/strings.ts +++ b/src/util/strings.ts @@ -1,5 +1,6 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime" +import {padStart} from "lodash"; dayjs.extend(relativeTime) @@ -79,4 +80,14 @@ export function calcContentLengthLikeWord(str:string) { } catch (e) { return str.length } +} + +// 将时长转换成 时:分:秒 +export function formatDuration(duration: number) { + const hour = Math.floor(duration / 3600); + const minute = Math.floor((duration - hour * 3600) / 60); + const second = duration - hour * 3600 - minute * 60; + // 需要补0 + return padStart(hour.toString(), 2, '0') + ':' + padStart(minute.toString(), 2, '0') + ':' + padStart(second.toString(), 2, '0') + // return `${hour}:${minute}:${second}` } \ No newline at end of file From 950bc59847920aa0c695db98958dc09466e828e1 Mon Sep 17 00:00:00 2001 From: callmeyan Date: Sat, 14 Dec 2024 22:38:22 +0800 Subject: [PATCH 03/12] :lipstick: update ui --- src/assets/core.scss | 4 + src/assets/images/cover.png | Bin 0 -> 65818 bytes src/components/video/video-list-item.tsx | 16 +-- src/pages/library/index.tsx | 2 +- src/pages/video/index.tsx | 126 +++++++++++++---------- 5 files changed, 85 insertions(+), 63 deletions(-) create mode 100644 src/assets/images/cover.png diff --git a/src/assets/core.scss b/src/assets/core.scss index d19f631..0ea47fb 100644 --- a/src/assets/core.scss +++ b/src/assets/core.scss @@ -109,4 +109,8 @@ .video-item-shadow { box-shadow: 0 0 6px 0 var(--tw-shadow-color); //filter: drop-shadow(0 0 6px var(--tw-shadow-color)); +} +.video-list-sort-container{ + height: calc(100vh - var(--app-header-header) - 300px); + overflow: auto; } \ No newline at end of file diff --git a/src/assets/images/cover.png b/src/assets/images/cover.png new file mode 100644 index 0000000000000000000000000000000000000000..c80d38d3144d07d66bd14626a6a70ec31f78bcf6 GIT binary patch literal 65818 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 { const { @@ -40,14 +42,14 @@ export const VideoListItem = ( return
- {index && index > 0 &&
-
{index}
-
} + {/*{index && index > 0 &&
*/} + {/*
{index}
*/} + {/*
}*/}
+ className={`video-item-info flex gap-2 flex-1 bg-gray-100 h-[80px] overflow-hidden rounded-lg p-3 shadow-blue-500 ${active ? 'video-item-shadow' : ''}`}>
{video.title || video.video_title}
- {video.video_title}/ + {video.video_title}/
{editable && diff --git a/src/pages/library/index.tsx b/src/pages/library/index.tsx index 71f202c..f2515c2 100644 --- a/src/pages/library/index.tsx +++ b/src/pages/library/index.tsx @@ -10,7 +10,7 @@ import {getList} from "@/service/api/video.ts"; export default function LibraryIndex() { const [modal, contextHolder] = Modal.useModal(); const [checkedIdArray, setCheckedIdArray] = useState([]) - const {data} = useRequest(()=>getList({}),{ + const {data} = useRequest(()=>getList(),{ }) const handleRemove = (video: VideoInfo) => { diff --git a/src/pages/video/index.tsx b/src/pages/video/index.tsx index 985e2f7..3c749f2 100644 --- a/src/pages/video/index.tsx +++ b/src/pages/video/index.tsx @@ -2,11 +2,12 @@ import {message, Modal} from "antd"; import React, {useEffect, useMemo, useRef, useState} from "react"; import {DndContext} from "@dnd-kit/core"; import {arrayMove, SortableContext} from "@dnd-kit/sortable"; -import {VideoListItem} from "@/components/video/video-list-item.tsx"; -import ArticleEditModal from "@/components/article/edit-modal.tsx"; import {useSetState} from "ahooks"; import {CheckCircleFilled} from "@ant-design/icons"; import {clsx} from "clsx"; + +import {VideoListItem} from "@/components/video/video-list-item.tsx"; +import ArticleEditModal from "@/components/article/edit-modal.tsx"; import {getList} from "@/service/api/video.ts"; import {formatDuration} from "@/util/strings.ts"; import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx"; @@ -17,21 +18,22 @@ export default function VideoIndex() { const [videoData, setVideoData] = useState([]) - useEffect(() => { - getList().then((ret) => { - setVideoData(ret.list || []) - }) - }, []) const [modal, contextHolder] = Modal.useModal() const videoRef = useRef(null) const [state, setState] = useSetState({ - checkedAll: false + checkedAll: false, + playingIndex: -1, }) const [checkedIdArray, setCheckedIdArray] = useState([]) const processDeleteVideo = async (_idArray: number[]) => { message.info('删除成功!!!' + _idArray.join('')); } + useEffect(() => { + getList().then((ret) => { + setVideoData(ret.list || []) + }) + }, []) const handleDeleteBatch = () => { modal.confirm({ @@ -41,11 +43,14 @@ export default function VideoIndex() { }) } - const playVideo = (video: VideoInfo) => { + const playVideo = (video: VideoInfo,playingIndex:number) => { + setState({ + playingIndex + }) console.log('play', video) - if (videoRef.current) { - videoRef.current!.src = video.play_url - } + // if (videoRef.current) { + // videoRef.current!.src = video.play_url + // } } const handleAllCheckedChange = () => { // setVideoData(list=>{ @@ -61,7 +66,7 @@ export default function VideoIndex() { } const totalDuration = useMemo(() => { - if(!videoData || videoData.length == 0) return 0; + if (!videoData || videoData.length == 0) return 0; // 计算总时长 return videoData.reduce((sum, v) => sum + v.duration, 0); }, [videoData]) @@ -82,49 +87,60 @@ export default function VideoIndex() {
- { - const {active, over} = e; - if (over && active.id !== over.id) { - let oldIndex = -1, newIndex = -1; - const originArr = [...videoData] - setVideoData((items) => { - oldIndex = items.findIndex(s => s.id == active.id); - newIndex = items.findIndex(s => s.id == over.id); - return arrayMove(items, oldIndex, newIndex); - }); - modal.confirm({ - title: '提示', - content: '是否要移动到指定位置', - onCancel: () => { - setVideoData(originArr); - } - }) - } - }}> - - +
+
{videoData.map((v, index) => ( - { - setCheckedIdArray(idArray => { - const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); - setState({checkedAll: newArr.length == videoData.length}) - return newArr; - }) - }} - onPlay={() => playVideo(v)} - onEdit={() => { - setEditId(v.article_id) - }} - editable - />))} - - +
+
{index}
+
+ ))} +
+
+ { + const {active, over} = e; + if (over && active.id !== over.id) { + let oldIndex = -1, newIndex = -1; + const originArr = [...videoData] + setVideoData((items) => { + oldIndex = items.findIndex(s => s.id == active.id); + newIndex = items.findIndex(s => s.id == over.id); + return arrayMove(items, oldIndex, newIndex); + }); + modal.confirm({ + title: '提示', + content: '是否要移动到指定位置', + onCancel: () => { + setVideoData(originArr); + } + }) + } + }}> + + {videoData.map((v, index) => ( + { + setCheckedIdArray(idArray => { + const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); + setState({checkedAll: newArr.length == videoData.length}) + return newArr; + }) + }} + onPlay={() => playVideo(v,index)} + onEdit={() => { + setEditId(v.article_id) + }} + editable + />))} + + +
+
From 2525358eb912b447c4dd38b75c4ad4a5359456b3 Mon Sep 17 00:00:00 2001 From: callmeyan Date: Sun, 15 Dec 2024 13:33:01 +0800 Subject: [PATCH 04/12] :lipstick: update video item active --- src/assets/core.scss | 24 +++- src/components/video/video-list-item.tsx | 4 +- src/pages/live/index.tsx | 171 ++++++++++++++++------- src/pages/video/index.tsx | 111 ++++++++------- 4 files changed, 198 insertions(+), 112 deletions(-) diff --git a/src/assets/core.scss b/src/assets/core.scss index 0ea47fb..de064aa 100644 --- a/src/assets/core.scss +++ b/src/assets/core.scss @@ -105,12 +105,30 @@ } } } - +.page-live{ + .live-player{ + max-height: calc(100vh - var(--app-header-header) - 130px); + overflow: hidden; + iframe{ + width: 100%; + height: 100%; + overflow: hidden; + } + } +} .video-item-shadow { box-shadow: 0 0 6px 0 var(--tw-shadow-color); //filter: drop-shadow(0 0 6px var(--tw-shadow-color)); } .video-list-sort-container{ - height: calc(100vh - var(--app-header-header) - 300px); + min-height: 300px; + max-height: calc(100vh - var(--app-header-header) - 300px); overflow: auto; -} \ No newline at end of file +} + +.live-video-list-sort-container{ + min-height: 300px; + padding-right: 10px; + max-height: calc(100vh - var(--app-header-header) - 200px); + overflow: auto; +} diff --git a/src/components/video/video-list-item.tsx b/src/components/video/video-list-item.tsx index f415b00..2a45281 100644 --- a/src/components/video/video-list-item.tsx +++ b/src/components/video/video-list-item.tsx @@ -20,6 +20,7 @@ type Props = { onEdit?: () => void; onRemove?: () => void; id: number; + className?: string; } export const VideoListItem = ( @@ -27,6 +28,7 @@ export const VideoListItem = ( // index, id, video, onPlay, onRemove, checked, onCheckedChange, onEdit, active, editable, + className, }: Props) => { const { attributes, listeners, @@ -40,7 +42,7 @@ export const VideoListItem = ( }, [checked]) return
{/*{index && index > 0 &&
*/} {/*
{index}
*/} diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index a67c09d..3b1df0c 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -4,9 +4,11 @@ import {SortableContext, arrayMove} from '@dnd-kit/sortable'; import {DndContext} from "@dnd-kit/core"; import {VideoListItem} from "@/components/video/video-list-item.tsx"; -import {getList} from "@/service/api/live.ts"; +import {getList, playState} from "@/service/api/live.ts"; import styles from './style.module.scss' +import {set} from "lodash"; +import {showToast} from "@/components/message.ts"; export default function LiveIndex() { const [videoData, setVideoData] = useState([]) @@ -15,14 +17,55 @@ export default function LiveIndex() { const [editable, setEditable] = useState(false) const [state, setState] = useState({ - activeId: -1, + activeIndex: -1, }) + const showVideoItem = (index: number) => { + // 找到对应video item 并显示在视图可见区域 + const container = document.querySelector('.live-video-list-sort-container') + const item = document.querySelector(`.list-item-${index}`) + if (item && container) { + // 获取容器数据 + const containerRect = container.getBoundingClientRect() + // 获取对应item的数据 + const rect = item.getBoundingClientRect() + // 计算对应item需要在容器中滚动的距离 + const scrollDistance = rect.top - containerRect.top + // 设置滚动高度 + container.scrollTo({ + top: index == 0 ? 0 : container.scrollTop + scrollDistance - 10, + behavior: 'smooth' + }) + } + } + const activeToNext = () => { + const endToFirst = state.activeIndex >= videoData.length - 1 + const activeIndex = endToFirst ? 0 : state.activeIndex + 1 + setState(() => { + return { + activeIndex + } + }) + if (endToFirst) { + showToast('即将播放第一条视频'); + } + // 找到对应video item 并显示在视图可见区域 + showVideoItem(activeIndex) + } + const initPlayingState = (videoList?: LiveVideoInfo[] | null) => { + const list = videoList || videoData || [] + playState().then(ret => { + setState({ + activeIndex: 0 //list.findIndex(v => v.id === ret.id) + }) + }); + } + useEffect(() => { getList().then(res => { - setVideoData([ + res.list = [ { - id: 1, + id: 11, video_id: 1, video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', @@ -32,7 +75,7 @@ export default function LiveIndex() { order_no: '1' }, { - id: 2, + id: 0, video_id: 1, video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', @@ -42,7 +85,7 @@ export default function LiveIndex() { order_no: '1' }, { - id: 3, + id: 10, video_id: 1, video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', @@ -91,7 +134,10 @@ export default function LiveIndex() { status: 1, order_no: '1' } - ]) + + ] + setVideoData(res.list || []) + initPlayingState(res.list || []) }) }, []) const processDeleteVideo = async (_idArray: number[]) => { @@ -109,26 +155,25 @@ export default function LiveIndex() { title: '提示', content: '是否采纳全部编辑操作?', onOk: () => { - message.info('编辑成功!!!'); + showToast('编辑成功!!!', 'info'); } }) } - return (
{contextHolder}
数字人直播间
-
- +
+ {/**/}
-
-
-
+
+
+
{editable ? <>
@@ -140,48 +185,66 @@ export default function LiveIndex() { :
} - +
+ + +
- { - const {active, over} = e; - if (over && active.id !== over.id) { - let oldIndex = -1, newIndex = -1; - const originArr = [...videoData] - setVideoData((items) => { - oldIndex = items.findIndex(s => s.id == active.id); - newIndex = items.findIndex(s => s.id == over.id); - return arrayMove(items, oldIndex, newIndex); - }); - modal.confirm({ - title: '提示', - content: '是否要移动到指定位置', - onCancel: () => { - setVideoData(originArr); - }, - onOk: () => { - setVideoData([...videoData]) - } - }) - } - }}> - - {videoData.map((v, index) => ( - { - setCheckedIdArray(idArray => { - return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); +
+
+
+ {videoData.map((v, index) => ( +
+
{index + 1}
+
+ ))} +
+
+ { + const {active, over} = e; + if (over && active.id !== over.id) { + let oldIndex = -1, newIndex = -1; + const originArr = [...videoData] + setVideoData((items) => { + oldIndex = items.findIndex(s => s.id == active.id); + newIndex = items.findIndex(s => s.id == over.id); + return arrayMove(items, oldIndex, newIndex); + }); + modal.confirm({ + title: '提示', + content: '是否要移动到指定位置', + onCancel: () => { + setVideoData(originArr); + }, + onOk: () => { + setVideoData([...videoData]) + } }) - }} - onRemove={() => processDeleteVideo([v.id])} - editable={editable} - />))} - - + } + }}> + + {videoData.map((v, index) => ( + { + setCheckedIdArray(idArray => { + return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); + }) + }} + onRemove={() => processDeleteVideo([v.id])} + editable={editable} + />))} + + +
+
+
diff --git a/src/pages/video/index.tsx b/src/pages/video/index.tsx index 3c749f2..0ba080a 100644 --- a/src/pages/video/index.tsx +++ b/src/pages/video/index.tsx @@ -1,4 +1,4 @@ -import {message, Modal} from "antd"; +import {Empty, message, Modal} from "antd"; import React, {useEffect, useMemo, useRef, useState} from "react"; import {DndContext} from "@dnd-kit/core"; import {arrayMove, SortableContext} from "@dnd-kit/sortable"; @@ -43,7 +43,7 @@ export default function VideoIndex() { }) } - const playVideo = (video: VideoInfo,playingIndex:number) => { + const playVideo = (video: VideoInfo, playingIndex: number) => { setState({ playingIndex }) @@ -88,58 +88,61 @@ export default function VideoIndex() {
-
- {videoData.map((v, index) => ( -
-
{index}
-
- ))} -
-
- { - const {active, over} = e; - if (over && active.id !== over.id) { - let oldIndex = -1, newIndex = -1; - const originArr = [...videoData] - setVideoData((items) => { - oldIndex = items.findIndex(s => s.id == active.id); - newIndex = items.findIndex(s => s.id == over.id); - return arrayMove(items, oldIndex, newIndex); - }); - modal.confirm({ - title: '提示', - content: '是否要移动到指定位置', - onCancel: () => { - setVideoData(originArr); - } - }) - } - }}> - - {videoData.map((v, index) => ( - { - setCheckedIdArray(idArray => { - const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); - setState({checkedAll: newArr.length == videoData.length}) - return newArr; - }) - }} - onPlay={() => playVideo(v,index)} - onEdit={() => { - setEditId(v.article_id) - }} - editable - />))} - - -
+ {videoData.length == 0 ?
: <> +
+ {videoData.map((v, index) => ( +
+
{index + 1}
+
+ ))} +
+
+ { + const {active, over} = e; + if (over && active.id !== over.id) { + let oldIndex = -1, newIndex = -1; + const originArr = [...videoData] + setVideoData((items) => { + oldIndex = items.findIndex(s => s.id == active.id); + newIndex = items.findIndex(s => s.id == over.id); + return arrayMove(items, oldIndex, newIndex); + }); + modal.confirm({ + title: '提示', + content: '是否要移动到指定位置', + onCancel: () => { + setVideoData(originArr); + } + }) + } + }}> + + {videoData.map((v, index) => ( + { + setCheckedIdArray(idArray => { + const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); + setState({checkedAll: newArr.length == videoData.length}) + return newArr; + }) + }} + onPlay={() => playVideo(v, index)} + onEdit={() => { + setEditId(v.article_id) + }} + editable + />))} + + +
+ }
From b07f336bd58740ea5bf252c4dba13642611b1051 Mon Sep 17 00:00:00 2001 From: callmeyan Date: Sun, 15 Dec 2024 16:37:13 +0800 Subject: [PATCH 05/12] =?UTF-8?q?:sparkles:=20=E8=B0=83=E6=95=B4=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=B0=E9=97=BB=E6=89=B9=E9=87=8F=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/article/edit-modal.tsx | 1 - src/pages/live/index.tsx | 10 +-- .../news/components/button-news-download.tsx | 90 ++++++++++++++----- src/service/api/article.ts | 2 +- 4 files changed, 76 insertions(+), 27 deletions(-) diff --git a/src/components/article/edit-modal.tsx b/src/components/article/edit-modal.tsx index c658da7..4896d71 100644 --- a/src/components/article/edit-modal.tsx +++ b/src/components/article/edit-modal.tsx @@ -40,7 +40,6 @@ export default function ArticleEditModal(props: Props) { }).finally(() => { setState({loading: false}) }); - props.onClose?.() } useEffect(() => { if (props.id) { diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index 3b1df0c..ee20e2a 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -33,14 +33,14 @@ export default function LiveIndex() { const scrollDistance = rect.top - containerRect.top // 设置滚动高度 container.scrollTo({ - top: index == 0 ? 0 : container.scrollTop + scrollDistance - 10, + top: index == 0 ? 0 : container.scrollTop + scrollDistance - 10, behavior: 'smooth' }) } } - const activeToNext = () => { - const endToFirst = state.activeIndex >= videoData.length - 1 - const activeIndex = endToFirst ? 0 : state.activeIndex + 1 + const activeToNext = (index?: number) => { + const endToFirst = index != undefined && index > -1 ? false : state.activeIndex >= videoData.length - 1 + const activeIndex = index != undefined && index > -1 ? index : (endToFirst ? 0 : state.activeIndex + 1) setState(() => { return { activeIndex @@ -56,7 +56,7 @@ export default function LiveIndex() { const list = videoList || videoData || [] playState().then(ret => { setState({ - activeIndex: 0 //list.findIndex(v => v.id === ret.id) + activeIndex: list.findIndex(v => v.id === ret.id) }) }); } diff --git a/src/pages/news/components/button-news-download.tsx b/src/pages/news/components/button-news-download.tsx index 72f7d87..bbfa8ac 100644 --- a/src/pages/news/components/button-news-download.tsx +++ b/src/pages/news/components/button-news-download.tsx @@ -2,36 +2,86 @@ import {Button} from "antd"; import JSZip from "jszip" import {saveAs} from "file-saver"; import {useState} from "react"; + +import {getById} from "@/service/api/news.ts"; + import {showToast} from "@/components/message.ts"; +/** + * 批量获取新闻内容 + * @param ids + */ +function getAllNewsContent(ids: Id[]) { + return new Promise((resolve, reject) => { + const request = ids.map((id) => getById(id)) + Promise.all(request).then(res => { + resolve(res) + }).catch(err => { + reject(err) + }) + }) +} + +/** + * 获取新闻html + * @param news + */ +function getNewsHtml(news: NewsInfo) { + return ` + +${news.title} + + + +
+

${news.title}

+
+ ${news.media_name} + ${news.publish_time} +
+
${news.content}
+
+ +` +} + +/** + * 将新闻数据包装成html并打包下载 + * @param list + */ +async function downloadAsZip(list: NewsInfo[]) { + const zip = new JSZip(); + + list.forEach(news => { + zip.file(`${news.title}.html`, getNewsHtml(news)) + }) + const content = await zip.generateAsync({type: "blob"}); + saveAs(content, "news.zip"); + // .then(function (content) { + // + // }).finally(() => { + // setLoading(false) + // }); +} + export default function ButtonNewsDownload(props: { ids: Id[] }) { const [loading, setLoading] = useState(false) - const onDownloadClick = (ids: Id[]) => { + const onDownloadClick = async (ids: Id[]) => { if (props.ids.length === 0) { - showToast('请选择要推送的新闻', 'warning') + showToast('请选择要下载的新闻', 'warning') return } setLoading(true) - const zip = new JSZip(); - ids.forEach(id => { - zip.file(`${id}.html`, ` - -${id} - - -
-

title ${id}

-

content ${id}

-
- -`) - }) - zip.generateAsync({type: "blob"}).then(function (content) { - saveAs(content, "news.zip"); - }).finally(() => { + try { + const list = await getAllNewsContent(ids) + await downloadAsZip(list) + } catch (e) { + showToast('下载新闻失败,请重试!', 'error') + } finally { setLoading(false) - }); + } + } return ( diff --git a/src/service/api/article.ts b/src/service/api/article.ts index cf3b9ef..3baff95 100644 --- a/src/service/api/article.ts +++ b/src/service/api/article.ts @@ -21,7 +21,7 @@ export function getById(id: Id) { return post({url: '/article/detail/' + id}) } -export function save(title: string, content_group: BlockContent[][], id: number) { +export function save(title: string, content_group: BlockContent[][], id?: number) { return post<{ content: string }>(id && id > 0 ? '/article/modify' : '/article/create/new', { title, content_group, From be22fc387afb1f2a8e932ab88cf9ecfa594cbefa Mon Sep 17 00:00:00 2001 From: callmeyan Date: Sun, 15 Dec 2024 18:00:48 +0800 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=93=8D=E4=BD=9C=E6=8C=89=E9=92=AE;=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=A7=86=E9=A2=91=E5=B1=95=E7=A4=BA;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/core.scss | 17 +++ src/components/article/edit-modal.tsx | 49 +++---- src/components/button-batch.tsx | 56 ++++++++ src/components/message.ts | 12 +- src/pages/live/index.tsx | 4 - src/pages/video/index.tsx | 178 +++++++++++++------------- src/service/api/video.ts | 5 +- 7 files changed, 189 insertions(+), 132 deletions(-) create mode 100644 src/components/button-batch.tsx diff --git a/src/assets/core.scss b/src/assets/core.scss index de064aa..47b4bef 100644 --- a/src/assets/core.scss +++ b/src/assets/core.scss @@ -19,6 +19,22 @@ @tailwind utilities; +::-webkit-scrollbar { + width: 4px; + border-radius: 5px; +} + +::-webkit-scrollbar-thumb { + background: #999; + height: 10px; + border-radius: 5px; + + &:hover { + background: #666; + cursor: pointer; + } +} + .btn { @apply px-5 py-2 rounded-md bg-white border text-sm; &:hover { @@ -124,6 +140,7 @@ min-height: 300px; max-height: calc(100vh - var(--app-header-header) - 300px); overflow: auto; + padding-right: 10px; } .live-video-list-sort-container{ diff --git a/src/components/article/edit-modal.tsx b/src/components/article/edit-modal.tsx index 4896d71..f607075 100644 --- a/src/components/article/edit-modal.tsx +++ b/src/components/article/edit-modal.tsx @@ -11,20 +11,24 @@ type Props = { onClose?: (saved?: boolean) => void; } +const DEFAULT_STATE = { + loading: false, + open: false, + msgTitle: '', + msgGroup: '', + error:'' +} export default function ArticleEditModal(props: Props) { const [groups, setGroups] = useState([]); const [title, setTitle] = useState('') const [state, setState] = useSetState({ - loading: false, - open: false, - msgTitle: '', - msgGroup: '', + ...DEFAULT_STATE }) // 保存数据 const handleSave = () => { - console.log(groups, title) + setState({error: ''}) if (!title) { // setState({msgTitle: '请输入标题内容'}); return; @@ -37,39 +41,24 @@ export default function ArticleEditModal(props: Props) { setState({loading: true}) save(title, groups, props.id > 0 ? props.id : undefined).then(() => { props.onClose?.(true) + }).catch(e=>{ + setState({error: e.data || '保存失败,请重试!'}) }).finally(() => { setState({loading: false}) }); } useEffect(() => { + setState({...DEFAULT_STATE}) if (props.id) { if (props.id > 0) { - if (props.type == 'news') { - article.getById(props.id).then(res => { - setGroups(res.content_group) - setTitle(res.title) - }) - } + article.getById(props.id).then(res => { + setGroups(res.content_group) + setTitle(res.title) + }) } else { // 新增 - setGroups([ - [{ - type: 'text', - content: '韩国国会当地时间14日16时举行全体会议,就在野党阵营第二次提出的尹锡悦总统弹劾案进行表决。根据投票结果,有204票赞成,85票反对,3票弃权,8票无效,弹劾案最终获得通过,尹锡悦的总统职务立即停止。' - }], - [ - { - type: 'text', - content: '韩国宪法法院将在180天内完成弹劾案审判程序。如果宪法法院作出弹劾案不成立的裁决,尹锡悦将立即恢复总统职务;如果宪法法院认可弹劾案成立,尹锡悦将立即被罢免,预计韩国将在明年4月至6月间举行大选。' - }, - { - type: 'image', - content: 'https://zverse-on.oss-cn-shanghai.aliyuncs.com/metahuman/workbench/20241214/193c442df75.jpeg' - }, - - ], - ]) - setTitle('韩国国会通过总统弹劾案 尹锡悦职务立即停止') + setGroups([]) + setTitle('') } } }, [props.id]) @@ -83,6 +72,7 @@ export default function ArticleEditModal(props: Props) { onCancel={()=>props.onClose?.()} okButtonProps={{loading: state.loading}} onOk={handleSave} + okText={props.type == 'news' ? '确定' : '重新生成'} >
@@ -111,6 +101,7 @@ export default function ArticleEditModal(props: Props) { }} />
+ {state.error &&
{state.error}
}
); } \ No newline at end of file diff --git a/src/components/button-batch.tsx b/src/components/button-batch.tsx new file mode 100644 index 0000000..2f55bfb --- /dev/null +++ b/src/components/button-batch.tsx @@ -0,0 +1,56 @@ +import React, {useState} from "react"; +import {Button, Modal} from "antd"; +import {ButtonType} from "antd/es/button"; +import {showErrorToast, showToast} from "@/components/message.ts"; + +type Props = { + selected: any[], + type?: ButtonType; + emptyMessage: string, + confirmMessage: React.ReactNode, + onProcess: (ids: Id[]) => Promise + successMessage?: string; + onSuccess?: () => void; + children?: React.ReactNode + +} +/** + * 统一批量操作按钮 + */ +export default function ButtonBatch( + { + selected, emptyMessage, successMessage, children, + type, confirmMessage, onProcess,onSuccess + }: Props) { + const [loading, setLoading] = useState(false) + const onBatchProcess = async () => { + setLoading(true) + try { + await onProcess(selected) + if (successMessage) showToast(successMessage, 'success') + if (onSuccess) { + onSuccess() + } + } catch (e) { + showErrorToast(e) + } finally { + setLoading(false) + } + } + const handleBtnClick = () => { + if (selected.length == 0) { + showToast(emptyMessage, 'warning') + return; + } + Modal.confirm({ + title: '操作提示', + centered: true, + content: confirmMessage, + onOk: onBatchProcess + }) + } + + return ( + + ) +} \ No newline at end of file diff --git a/src/components/message.ts b/src/components/message.ts index 80b258c..e37404b 100644 --- a/src/components/message.ts +++ b/src/components/message.ts @@ -1,16 +1,18 @@ import {message} from "antd"; import {BizError} from "@/service/types.ts"; -export function showToast(content: string, type?: 'success' | 'info' | 'warning' | 'error') { +export function showToast(content: string, type?: 'success' | 'info' | 'warning' | 'error', duration?: number) { message.open({ type, content, + duration, className: 'aui-toast' }).then(); } -export function showErrorToast(e:Error|BizError) { - showToast(String(((e instanceof BizError)?e.data:'') || e.message),'error') + +export function showErrorToast(e: Error | BizError) { + showToast(String(((e instanceof BizError) ? e.data : '') || e.message), 'error') } @@ -22,14 +24,14 @@ export function showLoading(content = 'Loading...') { content, }).then(); return { - update(content: string,type?: 'success' | 'info' | 'warning' | 'error'){ + update(content: string, type?: 'success' | 'info' | 'warning' | 'error') { message.open({ key, content, type }).then(); }, - close(){ + close() { message.destroy(key); } } diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index ee20e2a..ce03999 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -185,10 +185,6 @@ export default function LiveIndex() { :
} -
- - -
diff --git a/src/pages/video/index.tsx b/src/pages/video/index.tsx index 0ba080a..59a9c0e 100644 --- a/src/pages/video/index.tsx +++ b/src/pages/video/index.tsx @@ -1,4 +1,4 @@ -import {Empty, message, Modal} from "antd"; +import {Empty, Modal} from "antd"; import React, {useEffect, useMemo, useRef, useState} from "react"; import {DndContext} from "@dnd-kit/core"; import {arrayMove, SortableContext} from "@dnd-kit/sortable"; @@ -8,17 +8,15 @@ import {clsx} from "clsx"; import {VideoListItem} from "@/components/video/video-list-item.tsx"; import ArticleEditModal from "@/components/article/edit-modal.tsx"; -import {getList} from "@/service/api/video.ts"; +import {deleteByIds, getList} from "@/service/api/video.ts"; import {formatDuration} from "@/util/strings.ts"; import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx"; +import ButtonBatch from "@/components/button-batch.tsx"; export default function VideoIndex() { const [editId, setEditId] = useState(-1) - const [videoData, setVideoData] = useState([]) - - const [modal, contextHolder] = Modal.useModal() const videoRef = useRef(null) const [state, setState] = useSetState({ @@ -26,45 +24,31 @@ export default function VideoIndex() { playingIndex: -1, }) const [checkedIdArray, setCheckedIdArray] = useState([]) - const processDeleteVideo = async (_idArray: number[]) => { - message.info('删除成功!!!' + _idArray.join('')); - } - useEffect(() => { + + // 加载列表 + const loadList = () => { getList().then((ret) => { setVideoData(ret.list || []) - }) - }, []) - - const handleDeleteBatch = () => { - modal.confirm({ - title: '提示', - content: '是否要删除选择的视频?', - onOk: () => processDeleteVideo(checkedIdArray) + setState({checkedAll: false, playingIndex: -1}) }) } + // 播放视频 const playVideo = (video: VideoInfo, playingIndex: number) => { - setState({ - playingIndex - }) - console.log('play', video) - // if (videoRef.current) { - // videoRef.current!.src = video.play_url - // } + setState({playingIndex}) + if (videoRef.current && video.oss_video_url) { + videoRef.current!.src = video.oss_video_url + } } + // 处理全选 const handleAllCheckedChange = () => { - // setVideoData(list=>{ - // list.map(s=>{ - // s.checked = !state.checkedAll - // }) - // return list - // }) setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id)) setState({ checkedAll: !state.checkedAll }) } - + // + useEffect(loadList, []) const totalDuration = useMemo(() => { if (!videoData || videoData.length == 0) return 0; // 计算总时长 @@ -75,76 +59,86 @@ export default function VideoIndex() { {contextHolder}
-
+
视频时长: {formatDuration(totalDuration)}
-
- 批量删除 +
+ 批量删除 +
-
- {videoData.length == 0 ?
: <> -
- {videoData.map((v, index) => ( -
-
{index + 1}
-
- ))} -
-
- { - const {active, over} = e; - if (over && active.id !== over.id) { - let oldIndex = -1, newIndex = -1; - const originArr = [...videoData] - setVideoData((items) => { - oldIndex = items.findIndex(s => s.id == active.id); - newIndex = items.findIndex(s => s.id == over.id); - return arrayMove(items, oldIndex, newIndex); - }); - modal.confirm({ - title: '提示', - content: '是否要移动到指定位置', - onCancel: () => { - setVideoData(originArr); - } - }) - } - }}> - - {videoData.map((v, index) => ( - { - setCheckedIdArray(idArray => { - const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); - setState({checkedAll: newArr.length == videoData.length}) - return newArr; - }) - }} - onPlay={() => playVideo(v, index)} - onEdit={() => { - setEditId(v.article_id) - }} - editable - />))} - - -
- } +
+
+ {videoData.length == 0 ?
: <> +
+ {videoData.map((v, index) => ( +
+
{index + 1}
+
+ ))} +
+
+ { + const {active, over} = e; + if (over && active.id !== over.id) { + let oldIndex = -1, newIndex = -1; + const originArr = [...videoData] + setVideoData((items) => { + oldIndex = items.findIndex(s => s.id == active.id); + newIndex = items.findIndex(s => s.id == over.id); + return arrayMove(items, oldIndex, newIndex); + }); + modal.confirm({ + title: '提示', + content: '是否要移动到指定位置', + onCancel: () => { + setVideoData(originArr); + } + }) + } + }}> + + {videoData.map((v, index) => ( + { + setCheckedIdArray(idArray => { + const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); + setState({checkedAll: newArr.length == videoData.length}) + return newArr; + }) + }} + onPlay={() => playVideo(v, index)} + onEdit={() => { + setEditId(v.article_id) + }} + editable + />))} + + +
+ } +
-
+
@@ -157,6 +151,6 @@ export default function VideoIndex() {
- setEditId(-1)}/> + setEditId(-1)}/>
) } \ No newline at end of file diff --git a/src/service/api/video.ts b/src/service/api/video.ts index 9d514ba..d85dad3 100644 --- a/src/service/api/video.ts +++ b/src/service/api/video.ts @@ -25,10 +25,11 @@ export function getById(id: Id) { return post({url: '/video/detail/' + id}) } -export function deleteById(id: Id) { - return post({url: '/video/detail/' + id}) +export function deleteByIds(ids: Id[]) { + return post('/video/remove', {ids}) } + export function modifyOrder(ids: Id[]) { return post('/video/modifyorder', {ids}) } From 39f254c99b77e855e9bea4215d014ed478ab5b8a Mon Sep 17 00:00:00 2001 From: callmeyan Date: Sun, 15 Dec 2024 18:45:28 +0800 Subject: [PATCH 07/12] feat: update --- src/pages/video/index.tsx | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/pages/video/index.tsx b/src/pages/video/index.tsx index 59a9c0e..c8adcad 100644 --- a/src/pages/video/index.tsx +++ b/src/pages/video/index.tsx @@ -8,10 +8,11 @@ import {clsx} from "clsx"; import {VideoListItem} from "@/components/video/video-list-item.tsx"; import ArticleEditModal from "@/components/article/edit-modal.tsx"; -import {deleteByIds, getList} from "@/service/api/video.ts"; +import {deleteByIds, getList, modifyOrder, push2room} from "@/service/api/video.ts"; import {formatDuration} from "@/util/strings.ts"; import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx"; import ButtonBatch from "@/components/button-batch.tsx"; +import {showErrorToast, showToast} from "@/components/message.ts"; export default function VideoIndex() { @@ -28,6 +29,8 @@ export default function VideoIndex() { // 加载列表 const loadList = () => { getList().then((ret) => { + console.log('origin list', ret.list.map(s => s.id)) + setCheckedIdArray([]) setVideoData(ret.list || []) setState({checkedAll: false, playingIndex: -1}) }) @@ -35,8 +38,8 @@ export default function VideoIndex() { // 播放视频 const playVideo = (video: VideoInfo, playingIndex: number) => { - setState({playingIndex}) if (videoRef.current && video.oss_video_url) { + setState({playingIndex}) videoRef.current!.src = video.oss_video_url } } @@ -47,6 +50,15 @@ export default function VideoIndex() { checkedAll: !state.checkedAll }) } + const handleModifySort = () => { + + setVideoData((items) => { + modifyOrder(items.map(s => s.id)).catch(() => { + showToast('调整视频顺序失败,请重试!') + }).finally(loadList) + return items; + }) + } // useEffect(loadList, []) const totalDuration = useMemo(() => { @@ -95,6 +107,7 @@ export default function VideoIndex() { if (over && active.id !== over.id) { let oldIndex = -1, newIndex = -1; const originArr = [...videoData] + console.log(originArr.map(s => s.id)) setVideoData((items) => { oldIndex = items.findIndex(s => s.id == active.id); newIndex = items.findIndex(s => s.id == over.id); @@ -103,6 +116,7 @@ export default function VideoIndex() { modal.confirm({ title: '提示', content: '是否要移动到指定位置', + onOk: handleModifySort, onCancel: () => { setVideoData(originArr); } @@ -139,12 +153,20 @@ export default function VideoIndex() {
- + 一键推流 + {/**/}
-
+
预览视频
-
+
From 0592d97e3992c4c8587866da2983e4a536678fbf Mon Sep 17 00:00:00 2001 From: callmeyan Date: Mon, 16 Dec 2024 16:54:06 +0800 Subject: [PATCH 08/12] feat: update --- package.json | 1 + src/assets/core.scss | 49 ++++-- src/assets/libs.scss | 22 +++ src/components/article/block.tsx | 3 +- src/components/video/video-list-item.tsx | 29 ++-- src/pages/live/index.tsx | 172 ++++++++------------- src/pages/test.tsx | 90 +++++++++++ src/pages/video/index.tsx | 31 +++- src/routes/layout/dashboard-navigation.tsx | 2 +- src/routes/routes.tsx | 5 + src/service/api/live.ts | 6 +- yarn.lock | 18 +++ 12 files changed, 275 insertions(+), 153 deletions(-) create mode 100644 src/assets/libs.scss create mode 100644 src/pages/test.tsx diff --git a/package.json b/package.json index 040a9a4..c2a8f7f 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.11", "file-saver": "^2.0.5", + "flv.js": "^1.6.2", "jszip": "^3.10.1", "qs": "^6.12.1", "react": "^18.3.1", diff --git a/src/assets/core.scss b/src/assets/core.scss index 47b4bef..08985b6 100644 --- a/src/assets/core.scss +++ b/src/assets/core.scss @@ -1,3 +1,5 @@ +@use "./libs" as *; + :root { font-family: -apple-system, "PingFang SC", 'Microsoft YaHei', sans-serif; line-height: 1.5; @@ -35,22 +37,25 @@ } } -.btn { - @apply px-5 py-2 rounded-md bg-white border text-sm; - &:hover { - @apply bg-gray-100; - } - - &.btn-primary { - @apply bg-blue-500 text-white border-blue-500; +@layer base { + .btn { + @apply px-5 py-2 rounded-md bg-white border text-sm; &:hover { - @apply bg-blue-600; + @apply bg-gray-100; + } + + &.btn-primary { + @apply bg-blue-500 text-white border-blue-500; + &:hover { + @apply bg-blue-600; + } } } -} -.card { - @apply bg-white rounded-lg p-5 my-10; + .card { + @apply bg-white rounded-lg p-5 my-10; + } + } .radio-icon, .checkbox-icon { @@ -121,31 +126,41 @@ } } } -.page-live{ - .live-player{ + +.page-live { + .live-player { max-height: calc(100vh - var(--app-header-header) - 130px); overflow: hidden; - iframe{ + + iframe { width: 100%; height: 100%; overflow: hidden; } } } + .video-item-shadow { box-shadow: 0 0 6px 0 var(--tw-shadow-color); //filter: drop-shadow(0 0 6px var(--tw-shadow-color)); } -.video-list-sort-container{ + +.video-list-sort-container { min-height: 300px; max-height: calc(100vh - var(--app-header-header) - 300px); overflow: auto; padding-right: 10px; } -.live-video-list-sort-container{ +.live-video-list-sort-container { min-height: 300px; padding-right: 10px; max-height: calc(100vh - var(--app-header-header) - 200px); overflow: auto; } + +.app-main-navigation { + @include media-breakpoint-down(md) { + display: none; + } +} \ No newline at end of file diff --git a/src/assets/libs.scss b/src/assets/libs.scss new file mode 100644 index 0000000..891a092 --- /dev/null +++ b/src/assets/libs.scss @@ -0,0 +1,22 @@ +@mixin media-breakpoint-down($name) { + @if $name == sm { + @media (max-width: 767px) { + @content; + } + } + @if $name == md { + @media (max-width: 991px) { + @content; + } + } + @if $name == lg { + @media (max-width: 1199px) { + @content; + } + } + @if $name == xl { + @media (max-width: 1399px) { + @content; + } + } +} \ No newline at end of file diff --git a/src/components/article/block.tsx b/src/components/article/block.tsx index 1553d3e..32db078 100644 --- a/src/components/article/block.tsx +++ b/src/components/article/block.tsx @@ -80,7 +80,6 @@ export default function ArticleBlock(
handleBlockChange(0, block)} data={blocks[0]} isFirstBlock={index == 0} @@ -98,7 +97,7 @@ export default function ArticleBlock( {editable &&
{ index > 0 ? 请确认删除此删除此分组?
} + title={
请确认删除此分组?
} onConfirm={onRemove} okText="删除" cancelText="取消" diff --git a/src/components/video/video-list-item.tsx b/src/components/video/video-list-item.tsx index 2a45281..b85f009 100644 --- a/src/components/video/video-list-item.tsx +++ b/src/components/video/video-list-item.tsx @@ -28,7 +28,7 @@ export const VideoListItem = ( // index, id, video, onPlay, onRemove, checked, onCheckedChange, onEdit, active, editable, - className, + className, sortable }: Props) => { const { attributes, listeners, @@ -45,8 +45,8 @@ export const VideoListItem = ( className={`video-item flex items-center gap-3 ${className}`} ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}> {/*{index && index > 0 &&
*/} - {/*
{index}
*/} - {/*
}*/} + {/*
{index}
*/} + {/*
}*/}
{video.title || video.video_title}
@@ -54,14 +54,14 @@ export const VideoListItem = ( {video.video_title}/
- {editable && -
- {!active ? : } - {onPlay && - } +
+ {sortable && (!active ? : )} + {onPlay && + } + {editable && <> {onEdit && } @@ -73,15 +73,14 @@ export const VideoListItem = ( } }}> {onRemove && 请确认删除此视频?
} + title={
请确认删除此视频?
} onConfirm={onRemove} okText="删除" cancelText="取消" > } -
- } + } +
} \ No newline at end of file diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index ce03999..af456fa 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -4,11 +4,12 @@ import {SortableContext, arrayMove} from '@dnd-kit/sortable'; import {DndContext} from "@dnd-kit/core"; import {VideoListItem} from "@/components/video/video-list-item.tsx"; -import {getList, playState} from "@/service/api/live.ts"; +import {deleteByIds, getList, modifyOrder, playState} from "@/service/api/live.ts"; import styles from './style.module.scss' import {set} from "lodash"; -import {showToast} from "@/components/message.ts"; +import {showErrorToast, showToast} from "@/components/message.ts"; +import ButtonBatch from "@/components/button-batch.tsx"; export default function LiveIndex() { const [videoData, setVideoData] = useState([]) @@ -38,6 +39,7 @@ export default function LiveIndex() { }) } } + const activeToNext = (index?: number) => { const endToFirst = index != undefined && index > -1 ? false : state.activeIndex >= videoData.length - 1 const activeIndex = index != undefined && index > -1 ? index : (endToFirst ? 0 : state.activeIndex + 1) @@ -60,105 +62,51 @@ export default function LiveIndex() { }) }); } - - useEffect(() => { + const loadList = () => { getList().then(res => { - res.list = [ - { - id: 11, - video_id: 1, - video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', - cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', - video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', - video_duration: 100, - status: 1, - order_no: '1' - }, - { - id: 0, - video_id: 1, - video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', - cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', - video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', - video_duration: 100, - status: 1, - order_no: '1' - }, - { - id: 10, - video_id: 1, - video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', - cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', - video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', - video_duration: 100, - status: 1, - order_no: '1' - }, - { - id: 4, - video_id: 1, - video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', - cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', - video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', - video_duration: 100, - status: 1, - order_no: '1' - }, - { - id: 5, - video_id: 1, - video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', - cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', - video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', - video_duration: 100, - status: 1, - order_no: '1' - }, - { - id: 6, - video_id: 1, - video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', - cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', - video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', - video_duration: 100, - status: 1, - order_no: '1' - }, - { - id: 7, - video_id: 1, - video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要?', - cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg', - video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4', - video_duration: 100, - status: 1, - order_no: '1' - } - - ] + console.log('origin list', res.list.map(s => s.id)) setVideoData(res.list || []) - initPlayingState(res.list || []) - }) - }, []) - const processDeleteVideo = async (_idArray: number[]) => { - message.info('删除成功!!!' + _idArray.join('')); + setCheckedIdArray([]) + initPlayingState(res.list) + }); } - const handleDeleteBatch = () => { - modal.confirm({ - title: '提示', - content: '是否要删除选择的视频?', - onOk: () => processDeleteVideo(checkedIdArray) - }) + + useEffect(loadList, []) + + const processDeleteVideo = async (ids: number[]) => { + deleteByIds(ids).then(()=>{ + showToast('删除成功!','success') + loadList() + }).catch(showErrorToast) } const handleConfirm = () => { modal.confirm({ title: '提示', - content: '是否采纳全部编辑操作?', + content: '是否采纳移动视频位置操作?', onOk: () => { - showToast('编辑成功!!!', 'info'); + //showToast('编辑成功!!!', 'info'); + modifyOrder(videoData.map(s => s.id)).then(() => { + setEditable(false) + loadList() + }).catch(() => { + showToast('调整视频顺序失败,请重试!') + }) + // showToast('编辑成功!!!', 'info'); + // console.log('origin list', videoData.map(s => s.id)) } }) } + const handleCancelConfirm = () => { + modal.confirm({ + title: '提示', + content: '是否取消移动视频位置操作?', + onOk: () => { + showToast('退出并清除移动视频位置操作!', 'info'); + loadList() + setEditable(false) + }, + }) + } return (
{contextHolder} @@ -177,14 +125,20 @@ export default function LiveIndex() { {editable ? <>
- -
-
- 批量删除 +
:
- +
} + {!editable &&
+ 批量删除 +
}
@@ -201,22 +155,22 @@ export default function LiveIndex() { const {active, over} = e; if (over && active.id !== over.id) { let oldIndex = -1, newIndex = -1; - const originArr = [...videoData] + // const originArr = [...videoData] setVideoData((items) => { oldIndex = items.findIndex(s => s.id == active.id); newIndex = items.findIndex(s => s.id == over.id); return arrayMove(items, oldIndex, newIndex); }); - modal.confirm({ - title: '提示', - content: '是否要移动到指定位置', - onCancel: () => { - setVideoData(originArr); - }, - onOk: () => { - setVideoData([...videoData]) - } - }) + // modal.confirm({ + // title: '提示', + // content: '是否要移动到指定位置', + // onCancel: () => { + // setVideoData(originArr); + // }, + // onOk: () => { + // setVideoData([...videoData]) + // } + // }) } }}> @@ -225,16 +179,18 @@ export default function LiveIndex() { video={v} index={index + 1} id={v.id} - active={state.activeIndex == index} key={index} + active={state.activeIndex == index} className={`list-item-${index} mt-3 mb-2`} + checked={checkedIdArray.includes(v.id)} onCheckedChange={(checked) => { setCheckedIdArray(idArray => { return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); }) }} onRemove={() => processDeleteVideo([v.id])} - editable={editable} + editable={!editable} + sortable={editable} />))} diff --git a/src/pages/test.tsx b/src/pages/test.tsx new file mode 100644 index 0000000..80d3c0a --- /dev/null +++ b/src/pages/test.tsx @@ -0,0 +1,90 @@ +import {useRef, useState} from "react"; +import {Button} from "antd"; +import FlvJs from "flv.js"; + +const list = [ + { + "id": 10, + "cover_url": "", + "video_id": 51, + "video_title": "以军称在加沙地带打死一名哈马斯高级官员", + "video_duration": 31910, + "video_oss_url": "https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185251497659736064.flv", + "status": 4, + "order_no": "" + }, + { + "id": 8, + "cover_url": "", + "video_id": 43, + "video_title": "历时12天史上第三人 尹锡悦总统弹劾案获通过 一文梳理韩国政坛众生相", + "video_duration": 728840, + "video_oss_url": "https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185229869001351168.flv", + "status": 4, + "order_no": "" + }, + { + "id": 9, + "cover_url": "", + "video_id": 44, + "video_title": "推动房地产市场止跌回稳,发力重点在哪里?", + "video_duration": 57500, + "video_oss_url": "https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185229857764810752.flv", + "status": 4, + "order_no": "" + }, + { + "id": 11, + "cover_url": "", + "video_id": 52, + "video_title": "以军称在加沙地带打死一名哈马斯高级官员", + "video_duration": 37980, + "video_oss_url": "https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185251495390617600.flv", + "status": 4, + "order_no": "" + } +] + +const cache:{ + flvPlayer?: FlvJs.Player +} = { + +} +export default function Test() { + const videoRef = useRef(null) + const [index, setIndex] = useState(-1) + const load = (url: string) => { + if (FlvJs.isSupported()) { + if(cache.flvPlayer){ + cache.flvPlayer.pause() + cache.flvPlayer.unload() + } + cache.flvPlayer = FlvJs.createPlayer({ + type: 'flv', + url: url + }) + + cache.flvPlayer.attachMediaElement(videoRef.current!) + cache.flvPlayer.load() + cache.flvPlayer.play() + } + // const url = 'https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185229869001351168.flv' + // if (videoRef.current) { + // videoRef.current!.src = url + // videoRef.current?.play() + // } + } + const play = () => { + const next = index >= list.length - 1 ? 0 : index + 1 + load(list[next].video_oss_url) + setIndex(next) + } + return (
+
+ +
+ +
) +} \ No newline at end of file diff --git a/src/pages/video/index.tsx b/src/pages/video/index.tsx index c8adcad..cdbbda4 100644 --- a/src/pages/video/index.tsx +++ b/src/pages/video/index.tsx @@ -10,11 +10,11 @@ import {VideoListItem} from "@/components/video/video-list-item.tsx"; import ArticleEditModal from "@/components/article/edit-modal.tsx"; import {deleteByIds, getList, modifyOrder, push2room} from "@/service/api/video.ts"; import {formatDuration} from "@/util/strings.ts"; -import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx"; import ButtonBatch from "@/components/button-batch.tsx"; -import {showErrorToast, showToast} from "@/components/message.ts"; - +import {showToast} from "@/components/message.ts"; +import FlvJs from "flv.js"; +const cache:{flvPlayer?: FlvJs.Player} = {} export default function VideoIndex() { const [editId, setEditId] = useState(-1) const [videoData, setVideoData] = useState([]) @@ -29,7 +29,6 @@ export default function VideoIndex() { // 加载列表 const loadList = () => { getList().then((ret) => { - console.log('origin list', ret.list.map(s => s.id)) setCheckedIdArray([]) setVideoData(ret.list || []) setState({checkedAll: false, playingIndex: -1}) @@ -40,6 +39,21 @@ export default function VideoIndex() { const playVideo = (video: VideoInfo, playingIndex: number) => { if (videoRef.current && video.oss_video_url) { setState({playingIndex}) + if (FlvJs.isSupported()) { + // 已经有播放实例 则销毁 + if(cache.flvPlayer){ + cache.flvPlayer.pause() + cache.flvPlayer.unload() + } + cache.flvPlayer = FlvJs.createPlayer({ + type: 'flv', + url: video.oss_video_url + }) + + cache.flvPlayer.attachMediaElement(videoRef.current!) + cache.flvPlayer.load() + cache.flvPlayer.play() + } videoRef.current!.src = video.oss_video_url } } @@ -51,7 +65,6 @@ export default function VideoIndex() { }) } const handleModifySort = () => { - setVideoData((items) => { modifyOrder(items.map(s => s.id)).catch(() => { showToast('调整视频顺序失败,请重试!') @@ -81,7 +94,10 @@ export default function VideoIndex() { selected={checkedIdArray} emptyMessage={`请选择要删除的新闻视频`} confirmMessage={`是否删除当前的${checkedIdArray.length}个新闻视频?`} - onSuccess={loadList} + onSuccess={()=>{ + showToast('删除成功!','success') + loadList() + }} >批量删除 +
}
+
+ 视频时长: {formatDuration(totalDuration)} +
diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 3a53c41..3658646 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -103,5 +103,5 @@ declare interface LiveVideoInfo { declare interface LiveState{ id: number; - start_time?: number; + live_start_time?: number; } From 99d7787b04fe9a471e19a2d350f4a8dfda402d1b Mon Sep 17 00:00:00 2001 From: callmeyan Date: Mon, 16 Dec 2024 21:56:57 +0800 Subject: [PATCH 12/12] =?UTF-8?q?fixed:=20=E7=9B=B4=E6=92=AD=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/live/index.tsx | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index aac671d..4521939 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -11,9 +11,8 @@ import ButtonBatch from "@/components/button-batch.tsx"; import FlvJs from "flv.js"; import {formatDuration} from "@/util/strings.ts"; import {useSetState} from "ahooks"; -import {set} from "lodash"; -const cache: { flvPlayer?: FlvJs.Player,timerPlayNext?:any,timerLoadState?:any } = {} +const cache: { flvPlayer?: FlvJs.Player,timerPlayNext?:any,timerLoadState?:any,prevUrl?:string } = {} export default function LiveIndex() { const videoRef = useRef(null) const [videoData, setVideoData] = useState([]) @@ -76,23 +75,26 @@ export default function LiveIndex() { return; } if (FlvJs.isSupported()) { + if(cache.prevUrl !== video.video_oss_url) { + // 已经有播放实例 则销毁 + if (cache.flvPlayer) { + cache.flvPlayer.pause() + cache.flvPlayer.unload() + } + cache.prevUrl = video.video_oss_url + cache.flvPlayer = FlvJs.createPlayer({ + type: 'flv', + url: video.video_oss_url + }) - // 已经有播放实例 则销毁 - if (cache.flvPlayer) { - cache.flvPlayer.pause() - cache.flvPlayer.unload() + cache.flvPlayer.attachMediaElement(videoRef.current!) + cache.flvPlayer.load() } - cache.flvPlayer = FlvJs.createPlayer({ - type: 'flv', - url: video.video_oss_url - }) - - cache.flvPlayer.attachMediaElement(videoRef.current!) - cache.flvPlayer.load() if(liveState.live_start_time > 0 && playedTime > 0) videoRef.current!.currentTime = playedTime - cache.flvPlayer.play() + cache.flvPlayer!.play() + cache.timerPlayNext = setTimeout(()=>{ const index = activeToNext(),nextVideo = videoData[index] playVideo(nextVideo,{live_start_time:(Date.now() / 1000 >> 0),id:nextVideo.id}) @@ -118,7 +120,6 @@ export default function LiveIndex() { }); } const clearAllTimer = ()=>{ - if(cache.timerPlayNext) clearTimeout(cache.timerPlayNext) if(cache.timerLoadState) clearTimeout(cache.timerLoadState) } @@ -132,9 +133,9 @@ export default function LiveIndex() { }); } + useEffect(initPlayingState,[videoData]) useEffect(()=>{ loadList() - initPlayingState(); return clearAllTimer; }, [])