feat: ✨️ 直播间页调整锁定相关状态及逻辑
- 新增直播视频回滚功能 - 优化编辑模式操作流程
This commit is contained in:
parent
3d47964580
commit
4dee84a459
@ -19,7 +19,8 @@ import {saveAs} from "file-saver";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
video: VideoInfo | LiveVideoInfo,
|
video: VideoInfo | LiveVideoInfo,
|
||||||
additionOperation?: React.ReactNode;
|
additionOperationBefore?: React.ReactNode;
|
||||||
|
additionOperationAfter?: React.ReactNode;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
downloadVisible?: boolean;
|
downloadVisible?: boolean;
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
@ -45,7 +46,7 @@ export const VideoListItem = (
|
|||||||
id, video, onRemove,removeIcon, checked,playing,
|
id, video, onRemove,removeIcon, checked,playing,
|
||||||
onCheckedChange, onEdit, active, editable,downloadVisible,
|
onCheckedChange, onEdit, active, editable,downloadVisible,
|
||||||
className, sortable, type, index,onItemClick,
|
className, sortable, type, index,onItemClick,
|
||||||
additionOperation,onRegenerate,hideCheckBox
|
additionOperationAfter,additionOperationBefore,onRegenerate,hideCheckBox
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
attributes, listeners,
|
attributes, listeners,
|
||||||
@ -132,6 +133,7 @@ export const VideoListItem = (
|
|||||||
}} style={{fontSize: '1.1em'}}>
|
}} style={{fontSize: '1.1em'}}>
|
||||||
<IconDownloadOutline/>
|
<IconDownloadOutline/>
|
||||||
</button>}
|
</button>}
|
||||||
|
{additionOperationBefore}
|
||||||
{editable && !generating && <>
|
{editable && !generating && <>
|
||||||
{onEdit &&
|
{onEdit &&
|
||||||
<button className="hover:text-blue-500" onClick={e=>{
|
<button className="hover:text-blue-500" onClick={e=>{
|
||||||
@ -167,7 +169,7 @@ export const VideoListItem = (
|
|||||||
}
|
}
|
||||||
}} />}
|
}} />}
|
||||||
</>}
|
</>}
|
||||||
{additionOperation}
|
{additionOperationAfter}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,6 +48,10 @@
|
|||||||
"username": "Please enter your phone number",
|
"username": "Please enter your phone number",
|
||||||
"welcome": "Welcome"
|
"welcome": "Welcome"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"save_failed": "Save failed",
|
||||||
|
"save_success": "Save success"
|
||||||
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"hot_news": {
|
"hot_news": {
|
||||||
"edit_auto": "Smart Fill",
|
"edit_auto": "Smart Fill",
|
||||||
@ -165,6 +169,7 @@
|
|||||||
"download": "Download",
|
"download": "Download",
|
||||||
"generate_failed": "Generate Failed",
|
"generate_failed": "Generate Failed",
|
||||||
"generating": "Generating",
|
"generating": "Generating",
|
||||||
|
"live_rollback_confirm_title": "Are you sure you want to rollback this video?",
|
||||||
"playing": "Playing",
|
"playing": "Playing",
|
||||||
"push_confirm": "Are you sure you want to streaming these video?",
|
"push_confirm": "Are you sure you want to streaming these video?",
|
||||||
"push_empty": "Select the video you want to streaming",
|
"push_empty": "Select the video you want to streaming",
|
||||||
|
@ -48,6 +48,10 @@
|
|||||||
"username": "请输入账号",
|
"username": "请输入账号",
|
||||||
"welcome": "欢迎登录"
|
"welcome": "欢迎登录"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"save_failed": "保存失败",
|
||||||
|
"save_success": "保存成功"
|
||||||
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"hot_news": {
|
"hot_news": {
|
||||||
"edit_auto": "智能填充",
|
"edit_auto": "智能填充",
|
||||||
@ -165,6 +169,7 @@
|
|||||||
"download": "下载视频",
|
"download": "下载视频",
|
||||||
"generate_failed": "生成失败",
|
"generate_failed": "生成失败",
|
||||||
"generating": "生成中",
|
"generating": "生成中",
|
||||||
|
"live_rollback_confirm_title": "你确定要回退此视频吗 ",
|
||||||
"playing": "播放中",
|
"playing": "播放中",
|
||||||
"push_confirm": "是否确定一键推流选中新闻视频?",
|
"push_confirm": "是否确定一键推流选中新闻视频?",
|
||||||
"push_empty": "请选择要推流的新闻视频",
|
"push_empty": "请选择要推流的新闻视频",
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
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 {SortableContext, arrayMove} from '@dnd-kit/sortable';
|
||||||
import {DndContext} from "@dnd-kit/core";
|
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 {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||||
import {deleteByIds, getList, modifyOrder, playState} from "@/service/api/live.ts";
|
import {deleteByIds, getList, modifyOrder, playState} from "@/service/api/live.ts";
|
||||||
|
|
||||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||||
import ButtonBatch from "@/components/button-batch.tsx";
|
import ButtonBatch from "@/components/button-batch.tsx";
|
||||||
import FlvJs from "flv.js";
|
|
||||||
import {formatDuration} from "@/util/strings.ts";
|
import {formatDuration} from "@/util/strings.ts";
|
||||||
import {useSetState} from "ahooks";
|
|
||||||
import {Player, PlayerInstance} from "@/components/video/player.tsx";
|
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 InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||||
import ButtonToTop from "@/components/scoller/button-to-top.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 } = {}
|
const cache: { flvPlayer?: FlvJs.Player, timerPlayNext?: any, timerLoadState?: any, prevUrl?: string } = {}
|
||||||
|
|
||||||
export default function LiveIndex() {
|
export default function LiveIndex() {
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation()
|
||||||
const player = useRef<PlayerInstance | null>(null)
|
const player = useRef<PlayerInstance | null>(null)
|
||||||
@ -27,6 +29,8 @@ export default function LiveIndex() {
|
|||||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||||
const [editable, setEditable] = useState<boolean>(false)
|
const [editable, setEditable] = useState<boolean>(false)
|
||||||
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
|
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
|
||||||
|
const [rollbackIds,setRollbackIds] = useState<Id[]>([])
|
||||||
|
const [delIds,setDelIds] = useState<Id[]>([])
|
||||||
|
|
||||||
const [state, setState] = useSetState({
|
const [state, setState] = useSetState({
|
||||||
playId:-1,
|
playId:-1,
|
||||||
@ -93,6 +97,7 @@ export default function LiveIndex() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化播放状态
|
// 初始化播放状态
|
||||||
const initPlayingState = () => {
|
const initPlayingState = () => {
|
||||||
player.current?.pause();
|
player.current?.pause();
|
||||||
@ -134,7 +139,7 @@ export default function LiveIndex() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(initPlayingState, [videoData])
|
// useEffect(initPlayingState, [videoData])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadList()
|
loadList()
|
||||||
return ()=>{
|
return ()=>{
|
||||||
@ -152,44 +157,60 @@ export default function LiveIndex() {
|
|||||||
|
|
||||||
// 删除视频
|
// 删除视频
|
||||||
const processDeleteVideo = async (ids: Id[]) => {
|
const processDeleteVideo = async (ids: Id[]) => {
|
||||||
deleteByIds(ids).then(() => {
|
// 临时记录删除的id
|
||||||
showToast(t('delete_success'), 'success')
|
setDelIds(_=>[...ids,..._])
|
||||||
loadList()
|
// deleteByIds(ids).then(() => {
|
||||||
}).catch(showErrorToast)
|
// 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) {
|
if (!editable) {
|
||||||
setEditable(true)
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newSort = videoData.map(s => s.id).join(',')
|
const ids = videoData
|
||||||
if (newSort == state.originSort) {
|
.filter(s=>!(delIds.includes(s.id) || rollbackIds.includes(s.id)))
|
||||||
setEditable(false)
|
.map(s => s.id)
|
||||||
return;
|
try{
|
||||||
}
|
// 删除
|
||||||
modal.confirm({
|
if(delIds.length > 0) {
|
||||||
title: t('confirm.title'),
|
await deleteByIds(delIds)
|
||||||
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');
|
|
||||||
loadList()
|
|
||||||
setEditable(false)
|
|
||||||
}
|
}
|
||||||
})
|
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 = () => {
|
const handleAllCheckedChange = () => {
|
||||||
if(editable) return;
|
if(!editable) return;
|
||||||
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
|
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
|
||||||
setState({
|
setState({
|
||||||
checkedAll: !state.checkedAll
|
checkedAll: !state.checkedAll
|
||||||
@ -250,7 +271,7 @@ export default function LiveIndex() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="video-list-container video-list-sort-container flex flex-col flex-1 mt-2">
|
<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>
|
<div>
|
||||||
<Space>
|
<Space>
|
||||||
{/*<span className={"text-blue-500"}>视频正在播放{state.activeIndex == -1 ? '' : `到 ${state.activeIndex + 1} 条`}</span>*/}
|
{/*<span className={"text-blue-500"}>视频正在播放{state.activeIndex == -1 ? '' : `到 ${state.activeIndex + 1} 条`}</span>*/}
|
||||||
@ -259,12 +280,14 @@ export default function LiveIndex() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className={'flex items-center text-gray-400 cursor-pointer select-none'}
|
<div className={'flex items-center text-gray-400 cursor-pointer select-none'}>
|
||||||
onClick={handleConfirm}>
|
{editable ? (<Space size={15}>
|
||||||
<span>{editable ? t('live.edit_unlock') : t('live.edit_locked')}</span>
|
<button className={styles.btnDefault} onClick={handleCancel}>取消</button>
|
||||||
<span className="ml-2 text-sm">
|
<button className={styles.btn} onClick={handleConfirm}>保存操作</button>
|
||||||
{editable ? <IconUnlock/> : <IconLocked/>}
|
</Space>):(<div className="flex items-center " onClick={handleSetEditable}>
|
||||||
</span>
|
{t('live.edit_locked')}
|
||||||
|
<span className="ml-2 text-sm"><IconLocked/></span>
|
||||||
|
</div>)}
|
||||||
</div>
|
</div>
|
||||||
<div className="check-all ml-10">
|
<div className="check-all ml-10">
|
||||||
<button disabled={editable} className={`${editable?'':'hover:text-blue-300'} text-gray-400`}
|
<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>
|
<span className="text-sm mr-2 whitespace-nowrap">{t('select.select_all')}</span>
|
||||||
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
|
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
|
||||||
</button>
|
</button>
|
||||||
<Checkbox disabled={editable} checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
|
<Checkbox disabled={!editable} checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -308,7 +331,7 @@ export default function LiveIndex() {
|
|||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<SortableContext items={videoData}>
|
<SortableContext items={videoData}>
|
||||||
{videoData.map((v, index) => (
|
{videoData.filter(v=>(!(delIds.includes(v.id) || rollbackIds.includes(v.id)))).map((v, index) => (
|
||||||
<VideoListItem
|
<VideoListItem
|
||||||
video={v}
|
video={v}
|
||||||
index={index + 1}
|
index={index + 1}
|
||||||
@ -327,8 +350,18 @@ export default function LiveIndex() {
|
|||||||
// })
|
// })
|
||||||
}}
|
}}
|
||||||
onRemove={() => processDeleteVideo([v.id])}
|
onRemove={() => processDeleteVideo([v.id])}
|
||||||
editable={!editable && state.playId != v.id}
|
editable={editable && state.playId != v.id}
|
||||||
sortable={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>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
@ -1,3 +1,26 @@
|
|||||||
.videoListContainer{
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -185,7 +185,7 @@ export default function VideoIndex() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="video-list-container rounded mt-2 flex flex-col flex-1">
|
<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="pl-[70px]"></div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Space size={20}>
|
<Space size={20}>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user