feat: ️ 直播间页调整锁定相关状态及逻辑

- 新增直播视频回滚功能
- 优化编辑模式操作流程
This commit is contained in:
LittleBoy 2025-04-08 22:39:00 +08:00
parent 3d47964580
commit 4dee84a459
6 changed files with 119 additions and 51 deletions

View File

@ -19,7 +19,8 @@ import {saveAs} from "file-saver";
type Props = {
video: VideoInfo | LiveVideoInfo,
additionOperation?: React.ReactNode;
additionOperationBefore?: React.ReactNode;
additionOperationAfter?: React.ReactNode;
editable?: boolean;
downloadVisible?: boolean;
sortable?: boolean;
@ -45,7 +46,7 @@ export const VideoListItem = (
id, video, onRemove,removeIcon, checked,playing,
onCheckedChange, onEdit, active, editable,downloadVisible,
className, sortable, type, index,onItemClick,
additionOperation,onRegenerate,hideCheckBox
additionOperationAfter,additionOperationBefore,onRegenerate,hideCheckBox
}: Props) => {
const {
attributes, listeners,
@ -132,6 +133,7 @@ export const VideoListItem = (
}} style={{fontSize: '1.1em'}}>
<IconDownloadOutline/>
</button>}
{additionOperationBefore}
{editable && !generating && <>
{onEdit &&
<button className="hover:text-blue-500" onClick={e=>{
@ -167,7 +169,7 @@ export const VideoListItem = (
}
}} />}
</>}
{additionOperation}
{additionOperationAfter}
</div>
</div>
</div>

View File

@ -48,6 +48,10 @@
"username": "Please enter your phone number",
"welcome": "Welcome"
},
"message": {
"save_failed": "Save failed",
"save_success": "Save success"
},
"modal": {
"hot_news": {
"edit_auto": "Smart Fill",
@ -165,6 +169,7 @@
"download": "Download",
"generate_failed": "Generate Failed",
"generating": "Generating",
"live_rollback_confirm_title": "Are you sure you want to rollback this video?",
"playing": "Playing",
"push_confirm": "Are you sure you want to streaming these video?",
"push_empty": "Select the video you want to streaming",

View File

@ -48,6 +48,10 @@
"username": "请输入账号",
"welcome": "欢迎登录"
},
"message": {
"save_failed": "保存失败",
"save_success": "保存成功"
},
"modal": {
"hot_news": {
"edit_auto": "智能填充",
@ -165,6 +169,7 @@
"download": "下载视频",
"generate_failed": "生成失败",
"generating": "生成中",
"live_rollback_confirm_title": "你确定要回退此视频吗 ",
"playing": "播放中",
"push_confirm": "是否确定一键推流选中新闻视频?",
"push_empty": "请选择要推流的新闻视频",

View File

@ -1,23 +1,25 @@
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import {Checkbox, Empty, Modal, Space} from "antd";
import {Button, Checkbox, Empty, Modal, Popconfirm, Space} from "antd";
import {SortableContext, arrayMove} from '@dnd-kit/sortable';
import {DndContext} from "@dnd-kit/core";
import FlvJs from "flv.js";
import {useTranslation} from "react-i18next";
import {useSetState} from "ahooks";
import {VideoListItem} from "@/components/video/video-list-item.tsx";
import {deleteByIds, getList, modifyOrder, playState} from "@/service/api/live.ts";
import {showErrorToast, showToast} from "@/components/message.ts";
import ButtonBatch from "@/components/button-batch.tsx";
import FlvJs from "flv.js";
import {formatDuration} from "@/util/strings.ts";
import {useSetState} from "ahooks";
import {Player, PlayerInstance} from "@/components/video/player.tsx";
import {IconDelete, IconLocked, IconUnlock} from "@/components/icons";
import {IconDelete, IconLocked, IconRollbackCircle, IconWarningCircle} from "@/components/icons";
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
import {useTranslation} from "react-i18next";
import styles from "./style.module.scss"
const cache: { flvPlayer?: FlvJs.Player, timerPlayNext?: any, timerLoadState?: any, prevUrl?: string } = {}
export default function LiveIndex() {
const {t} = useTranslation()
const player = useRef<PlayerInstance | null>(null)
@ -27,6 +29,8 @@ export default function LiveIndex() {
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
const [editable, setEditable] = useState<boolean>(false)
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
const [rollbackIds,setRollbackIds] = useState<Id[]>([])
const [delIds,setDelIds] = useState<Id[]>([])
const [state, setState] = useSetState({
playId:-1,
@ -93,6 +97,7 @@ export default function LiveIndex() {
}
}
// 初始化播放状态
const initPlayingState = () => {
player.current?.pause();
@ -134,7 +139,7 @@ export default function LiveIndex() {
});
}
useEffect(initPlayingState, [videoData])
// useEffect(initPlayingState, [videoData])
useEffect(() => {
loadList()
return ()=>{
@ -152,44 +157,60 @@ export default function LiveIndex() {
// 删除视频
const processDeleteVideo = async (ids: Id[]) => {
deleteByIds(ids).then(() => {
showToast(t('delete_success'), 'success')
loadList()
}).catch(showErrorToast)
// 临时记录删除的id
setDelIds(_=>[...ids,..._])
// deleteByIds(ids).then(() => {
// showToast(t('delete_success'), 'success')
// loadList()
// }).catch(showErrorToast)
}
// 状态:锁定->解锁
const handleSetEditable = ()=>{
setEditable(true)
setCheckedIdArray([])
setState({checkedAll: false})
}
//
const handleConfirm = () => {
const handleCancel = ()=>{
setEditable(false)
setRollbackIds(()=>[])
setDelIds(()=>[])
}
const handleRollback = (v:LiveVideoInfo)=>{
setRollbackIds(_=>[v.id,..._])
}
const handleConfirm = async () => {
if (!editable) {
setEditable(true)
return;
}
const newSort = videoData.map(s => s.id).join(',')
if (newSort == state.originSort) {
setEditable(false)
return;
const ids = videoData
.filter(s=>!(delIds.includes(s.id) || rollbackIds.includes(s.id)))
.map(s => s.id)
try{
// 删除
if(delIds.length > 0) {
await deleteByIds(delIds)
}
modal.confirm({
title: t('confirm.title'),
content: t('video.sort_modify_confirm'),
centered: true,
onOk: () => {
//showToast('编辑成功!!!', 'info');
modifyOrder(videoData.map(s => s.id)).then(() => {
showToast(t('video.sort_modify_live_success'), 'success')
setEditable(false)
}).catch(() => {
showToast(t('video.sort_modify_failed'), 'warning')
})
},
onCancel: () => {
showToast(t('video.sort_modify_rollback'), 'info');
if(rollbackIds.length > 0) {
showToast('回退暂未实现')
}
// 调整排序
await modifyOrder(ids);
showToast(t('message.save_success'), 'success')
}catch (e){
console.log(e)
showToast(t('message.save_failed'), 'error')
}finally {
setCheckedIdArray([])
setState({checkedAll: false})
setRollbackIds(()=>[])
setDelIds(()=>[])
loadList()
setEditable(false)
}
})
}
const handleAllCheckedChange = () => {
if(editable) return;
if(!editable) return;
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
setState({
checkedAll: !state.checkedAll
@ -250,7 +271,7 @@ export default function LiveIndex() {
</div>
</div>
<div className="video-list-container video-list-sort-container flex flex-col flex-1 mt-2">
<div className="live-control flex justify-between mb-1">
<div className="live-control flex justify-between mb-1 h-[30px]">
<div>
<Space>
{/*<span className={"text-blue-500"}>视频正在播放{state.activeIndex == -1 ? '' : `到 ${state.activeIndex + 1} 条`}</span>*/}
@ -259,12 +280,14 @@ export default function LiveIndex() {
</div>
<div className="flex items-center">
<div className={'flex items-center text-gray-400 cursor-pointer select-none'}
onClick={handleConfirm}>
<span>{editable ? t('live.edit_unlock') : t('live.edit_locked')}</span>
<span className="ml-2 text-sm">
{editable ? <IconUnlock/> : <IconLocked/>}
</span>
<div className={'flex items-center text-gray-400 cursor-pointer select-none'}>
{editable ? (<Space size={15}>
<button className={styles.btnDefault} onClick={handleCancel}></button>
<button className={styles.btn} onClick={handleConfirm}></button>
</Space>):(<div className="flex items-center " onClick={handleSetEditable}>
{t('live.edit_locked')}
<span className="ml-2 text-sm"><IconLocked/></span>
</div>)}
</div>
<div className="check-all ml-10">
<button disabled={editable} className={`${editable?'':'hover:text-blue-300'} text-gray-400`}
@ -272,7 +295,7 @@ export default function LiveIndex() {
<span className="text-sm mr-2 whitespace-nowrap">{t('select.select_all')}</span>
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
</button>
<Checkbox disabled={editable} checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
<Checkbox disabled={!editable} checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
</div>
</div>
</div>
@ -308,7 +331,7 @@ export default function LiveIndex() {
}
}}>
<SortableContext items={videoData}>
{videoData.map((v, index) => (
{videoData.filter(v=>(!(delIds.includes(v.id) || rollbackIds.includes(v.id)))).map((v, index) => (
<VideoListItem
video={v}
index={index + 1}
@ -327,8 +350,18 @@ export default function LiveIndex() {
// })
}}
onRemove={() => processDeleteVideo([v.id])}
editable={!editable && state.playId != v.id}
editable={editable && state.playId != v.id}
sortable={editable && state.playId != v.id}
additionOperationBefore={<>
{editable && state.playId != v.id && <Popconfirm
rootClassName={'popconfirm-main'}
placement={'left'}
arrow={false}
icon={<IconWarningCircle/>}
title={t('video.live_rollback_confirm_title')}
onConfirm={() => handleRollback(v)}
><button className="hover:text-blue-500"><IconRollbackCircle /></button></Popconfirm>}
</>}
/>))}
</SortableContext>
</DndContext>

View File

@ -1,3 +1,26 @@
.videoListContainer{
}
@mixin btnDefault{
border-radius: 20px;
padding: 2px 16px;
height: auto;
}
.btn{
@include btnDefault;
background: #4096FF;
color:#fff;
border: 1px solid #4096FF;
&:hover{
background: #337acc;
}
}
.btnDefault{
@include btnDefault;
color:#00000099;
border: 1px solid #00000099;
&:hover{
background: #00000011;
}
}

View File

@ -185,7 +185,7 @@ export default function VideoIndex() {
</div>
</div>
<div className="video-list-container rounded mt-2 flex flex-col flex-1">
<div className="live-control flex justify-between">
<div className="live-control flex justify-between h-[30px]">
<div className="pl-[70px]"></div>
<div className="flex items-center">
<Space size={20}>