Compare commits

...

3 Commits

Author SHA1 Message Date
6603bbf75f update ui 2024-12-24 00:04:48 +08:00
2b2fe09e71 添加播放时间更新 2024-12-24 00:02:40 +08:00
2e5893d3ab update 删除 2024-12-23 23:48:39 +08:00
7 changed files with 116 additions and 71 deletions

View File

@ -21,6 +21,7 @@ type Props = {
url?: string; cover?: string; showControls?: boolean; className?: string;
poster?: string;
onChange?: (state: State) => void;
onProgress?: (current:number,duration:number) => void;
muted?: boolean;
}
export type PlayerInstance = {
@ -84,6 +85,9 @@ export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
player.on('ended', () => {
setState({end: true, playing: false, error: false})
})
player.on('timeupdate', () => {
props.onProgress?.(player.currentTime(), player.duration())
})
player.on('error', () => {
setState({end: false, playing: false, error: true})
})

View File

@ -2,7 +2,7 @@ import {useSortable} from "@dnd-kit/sortable";
import {useSetState} from "ahooks";
import React, {useEffect} from "react";
import {clsx} from "clsx";
import {Checkbox, Popconfirm} from "antd";
import {App, Checkbox, Popconfirm} from "antd";
import {CheckCircleFilled, MenuOutlined, MinusCircleFilled, LoadingOutlined} from "@ant-design/icons";
import ImageCover from '@/assets/images/cover.png'
@ -17,6 +17,7 @@ type Props = {
index?: number;
checked?: boolean;
active?: boolean;
playing?: boolean;
onCheckedChange?: (checked: boolean) => void;
onPlay?: () => void;
onEdit?: () => void;
@ -29,7 +30,7 @@ type Props = {
export const VideoListItem = (
{
id, video, onRemove, checked,
id, video, onRemove, checked,playing,
onCheckedChange, onEdit, active, editable,
className, sortable, type, index,onItemClick
}: Props) => {
@ -45,11 +46,19 @@ export const VideoListItem = (
}, [checked])
const generating = (type == 'create' && video.status == VideoStatus.Generating )
const {modal} = App.useApp()
const handleDelete = () => {
if(!onRemove) return;
modal.confirm({
title: '提示',
centered: true,
content: '是否要删除该视频',
onOk: onRemove,
})
}
return <div
className={`video-item ${className}`}
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}
onClick={onItemClick}
>
<div className={`list-row ${generating ? 'disabled' : ''} ${active?'playing':''}`}>
<div
@ -57,7 +66,7 @@ export const VideoListItem = (
{... (sortable && !generating?listeners:{})}
{... (sortable && !generating?attributes:{})}
>{index}</div>
<div className="col cover cursor-pointer">
<div className="col cover cursor-pointer" onClick={onItemClick}>
<div className="relative">
<img className="w-[100px] h-[56px] object-cover" src={video.cover || ImageCover}/>
{generating &&
@ -66,7 +75,7 @@ export const VideoListItem = (
</div>
}
{/* && active*/}
{!generating && active && <div className={'absolute rounded inset-0 bg-black/30 text-sm text-white flex items-center justify-center'}>
{!generating && playing && <div className={'absolute rounded inset-0 bg-black/30 text-sm text-white flex items-center justify-center'}>
<div className="text-center">
<IconPlaying className="inline-block text-xl" />
<div></div>
@ -104,14 +113,7 @@ export const VideoListItem = (
<IconEdit/>
</button>}
{onRemove && <Popconfirm
title={<div style={{minWidth: 150}}><span>?</span></div>}
onConfirm={onRemove}
okText="删除"
cancelText="取消"
>
<button className="hover:text-blue-500"><IconDelete/></button>
</Popconfirm>}
{onRemove && <button className="hover:text-blue-500" onClick={handleDelete}><IconDelete/></button>}
<Checkbox checked={state.checked} onChange={() => {
if (onCheckedChange) {
onCheckedChange(!state.checked)

View File

@ -32,7 +32,8 @@ export default function LiveIndex() {
muted: true,
showToTop: false,
checkedAll: false,
originSort:''
originSort: '',
playProgress: 0
})
const activeIndex = useRef(state.activeIndex)
useEffect(() => {
@ -114,7 +115,7 @@ export default function LiveIndex() {
// console.log('origin list', res.list.map(s => s.id))
setVideoData(() => (res.list || []))
setState({
originSort: res.list?res.list.map(s => s.id).join(','):''
originSort: res.list ? res.list.map(s => s.id).join(',') : ''
})
setCheckedIdArray([])
});
@ -138,7 +139,7 @@ export default function LiveIndex() {
return;
}
const newSort = videoData.map(s => s.id).join(',')
if(newSort == state.originSort){
if (newSort == state.originSort) {
setEditable(false)
return;
}
@ -149,10 +150,10 @@ export default function LiveIndex() {
onOk: () => {
//showToast('编辑成功!!!', 'info');
modifyOrder(videoData.map(s => s.id)).then(() => {
showToast('已完成直播队列的修改!','success')
showToast('已完成直播队列的修改!', 'success')
setEditable(false)
}).catch(() => {
showToast('调整视频顺序失败,请重试!','warning')
showToast('调整视频顺序失败,请重试!', 'warning')
})
},
onCancel: () => {
@ -174,6 +175,15 @@ export default function LiveIndex() {
// 计算总时长
return videoData.reduce((sum, v) => sum + Math.ceil(v.video_duration / 1000), 0);
}, [videoData])
// 根据当前播放index计算已经播放时长
const currentTotalDuration = useMemo(() => {
if (state.activeIndex == -1 || !videoData || videoData.length == 0) return 0;
// 计算总时长
return videoData
.filter((_, index) => (index < state.activeIndex))
.reduce((sum, v) => sum + Math.ceil(v.video_duration / 1000), 0) + state.playProgress
;
}, [videoData, state.playProgress])
const currentSelectedId = useMemo(() => {
if (state.activeIndex < 0 || state.activeIndex >= videoData.length) return [];
@ -185,16 +195,21 @@ export default function LiveIndex() {
{contextHolder}
<div className="flex">
<div className="video-player-container mr-8 flex flex-col">
<div className="text-center text-base"></div>
<div className="text-center text-base">
<span>: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)}</span>
</div>
<div className="video-player flex justify-center flex-1 mt-5">
<div className="live-player relative rounded overflow-hidden w-[360px] h-[636px]"
style={{backgroundColor: 'hsl(210, 100%, 48%)'}}>
<Player ref={player} className="w-[360px] h-[636px] bg-white" muted={true}/>
<Player
ref={player} className="w-[360px] h-[636px] bg-white"
muted={true}
onProgress={(progress) => {
setState({playProgress: progress})
}}
/>
</div>
</div>
<div className="mt-4 text-center text-sm">
<span>: {formatDuration(totalDuration)}</span>
</div>
</div>
<div className="video-list-container video-list-sort-container flex-1">
@ -236,7 +251,8 @@ export default function LiveIndex() {
<InfiniteScroller
ref={scrollerRef}
onScroll={top => setState({showToTop: top > 30})}
onCallback={()=>{}}
onCallback={() => {
}}
>
<div className="sort-list-container flex-1">
<DndContext onDragEnd={(e) => {
@ -282,17 +298,17 @@ export default function LiveIndex() {
</div>
<div className="page-action">
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
<ButtonBatch
className='bg-gray-300 hover:bg-gray-400 text-white'
selected={currentSelectedId}
emptyMessage={`请选择要删除的视频`}
confirmMessage={`是否删除当前的${currentSelectedId.length}条视频?`}
onSuccess={loadList}
onProcess={processDeleteVideo}
>
<span className={'text'}></span>
<IconDelete/>
</ButtonBatch>
{checkedIdArray.length > 0 && <ButtonBatch
className='bg-gray-300 hover:bg-gray-400 text-white'
selected={checkedIdArray}
emptyMessage={`请选择要删除的视频`}
confirmMessage={`是否删除当前的${checkedIdArray.length}条视频?`}
onSuccess={loadList}
onProcess={processDeleteVideo}
>
<span className={'text'}></span>
<IconDelete/>
</ButtonBatch>}
</div>
</div>)
}

View File

@ -112,7 +112,7 @@ export default function SearchPanel({onSearch}: SearchPanelProps) {
const setFalse = ()=>togglePinnedManagePanel(false)
useClickAway(() => setFalse(), pinnedManagePanel)
return (<div className={`${styles.searchPanel} pt-8 pb-2`}>
return (<div className={`${styles.searchPanel} pt-6 pb-2`}>
<div className="flex justify-between items-center">
<div className="search-form flex items-center gap-4">
<Input

View File

@ -68,7 +68,7 @@ export default function NewEdit() {
return (<div className="container pb-5 news-edit">
<div className="search-panel-container my-5">
<div className="search-form flex gap-5 justify-between">
<div className="search-form flex pt-1 gap-5 justify-between">
<EditSearchForm onSubmit={setParams}/>
{/*<Button type="primary" onClick={() => setEditId(0)}>手动新增</Button>*/}
</div>

View File

@ -1,4 +1,4 @@
import {Checkbox, Empty, Modal} from "antd";
import {Checkbox, Empty} from "antd";
import React, {useEffect, useMemo, useRef, useState} from "react";
import {DndContext} from "@dnd-kit/core";
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
@ -9,26 +9,30 @@ import ArticleEditModal from "@/components/article/edit-modal.tsx";
import {deleteByIds, getList, modifyOrder, VideoStatus} from "@/service/api/video.ts";
import {formatDuration} from "@/util/strings.ts";
import ButtonBatch from "@/components/button-batch.tsx";
import {showToast} from "@/components/message.ts";
import {showErrorToast, showToast} from "@/components/message.ts";
import {Player, PlayerInstance} from "@/components/video/player.tsx";
import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx";
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
import {IconDelete} from "@/components/icons";
import {useLocation} from "react-router-dom";
import {playState} from "@/service/api/live.ts";
export default function VideoIndex() {
const [editId, setEditId] = useState(-1)
const loc = useLocation()
const [videoData, setVideoData] = useState<VideoInfo[]>([])
const [modal, contextHolder] = Modal.useModal()
const player = useRef<PlayerInstance | null>(null)
const scrollerRef = useRef<InfiniteScrollerRef|null>(null)
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
const [state, setState] = useSetState({
checkedAll: false,
playingIndex: -1,
showToTop: false,
showStatePos: false
showStatePos: false,
playState: {
current: -1,
total: -1
}
})
const [checkedIdArray, setCheckedIdArray] = useState<Id[]>([])
@ -51,19 +55,19 @@ export default function VideoIndex() {
// 播放视频
const playVideo = (video: VideoInfo, playingIndex: number) => {
if(state.playingIndex == playingIndex){
if (state.playingIndex == playingIndex) {
player.current?.pause();
setState({playingIndex: -1})
return;
}
if(video.status == VideoStatus.Generating ) return;
setState({playingIndex})
player.current?.play('https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-17/1186196465916190720.flv',30)
if (video.status == VideoStatus.Generating) return;
// setState({playingIndex})
// player.current?.play('https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-17/1186196465916190720.flv', 30)
//
// if (video.oss_video_url && video.status !== 1) {
// setState({playingIndex})
// player.current?.play(video.oss_video_url, 0)
// }
if (video.oss_video_url && video.status !== 1) {
setState({playingIndex})
player.current?.play(video.oss_video_url, 0)
}
}
// 处理全选
const handleAllCheckedChange = () => {
@ -72,38 +76,43 @@ export default function VideoIndex() {
checkedAll: !state.checkedAll
})
}
const handleModifySort = (items:VideoInfo[]) => {
const handleModifySort = (items: VideoInfo[]) => {
modifyOrder(items.map(s => s.id)).then(() => {
showToast('调整视频顺序成功!','success')
}).catch(()=>{
showToast('调整视频顺序成功!', 'success')
}).catch(() => {
loadList();
showToast('调整视频顺序失败,请重试!','warning')
showToast('调整视频顺序失败,请重试!', 'warning')
})
}
//
useEffect(loadList, [])
const totalDuration = useMemo(() => {
if (!videoData || videoData.length == 0) return 0;
if(state.playingIndex == -1 || state.playingIndex >= videoData.length) return 0
const v= videoData[state.playingIndex] as VideoInfo;
if (state.playingIndex == -1 || state.playingIndex >= videoData.length) return 0
const v = videoData[state.playingIndex] as VideoInfo;
return Math.ceil(v.duration / 1000)
// 计算总时长
//return videoData.reduce((sum, v) => sum + Math.ceil(v.duration / 1000), 0);
}, [videoData,state.playingIndex])
}, [videoData, state.playingIndex])
useEffect(()=>{
if(loc.state == 'push-success' && !state.showStatePos && videoData.length && scrollerRef.current){
useEffect(() => {
if (loc.state == 'push-success' && !state.showStatePos && videoData.length && scrollerRef.current) {
const generatingItem = document.querySelector(`.list-item-state-${VideoStatus.Generating}`)
if(generatingItem){
if (generatingItem) {
generatingItem.scrollIntoView({behavior: 'smooth'})
setState({showStatePos: true})
}
}
},[videoData,scrollerRef])
}, [videoData, scrollerRef])
const processDeleteVideo = async (ids: Id[]) => {
deleteByIds(ids).then(() => {
showToast('删除成功!', 'success')
loadList()
}).catch(showErrorToast)
}
return (<div className="container py-10 page-live">
{contextHolder}
<div className="flex">
<div className="video-player-container mr-16 w-[360px] flex flex-col">
<div className="text-center text-base text-gray-400"> - </div>
@ -112,12 +121,21 @@ export default function VideoIndex() {
<Player
ref={player} url={videoData[state.playingIndex]?.oss_video_url}
onChange={(state) => {
console.log(state)
if (state.end || state.error) setState({playingIndex: -1})
}}
onProgress={(current, duration) => {
setState({
playState: {
current: current,
total: duration
}
})
}}
className="w-[360px] h-[640px] bg-white"/>
</div>
</div>
<div className="text-center text-sm mt-4 text-gray-400">: {formatDuration(totalDuration)}</div>
<div className="text-center text-sm mt-4 text-gray-400">{formatDuration(state.playState.current)} / {formatDuration(state.playState.total)}</div>
</div>
<div className="video-list-container rounded flex flex-col flex-1">
<div className="live-control flex justify-between">
@ -128,7 +146,7 @@ export default function VideoIndex() {
<span className="text-sm mr-2"></span>
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
</button>
<Checkbox checked={state.checkedAll} onChange={()=>handleAllCheckedChange()} />
<Checkbox checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
</div>
</div>
<div className={'video-list-sort-container flex-1'}>
@ -141,7 +159,7 @@ export default function VideoIndex() {
<div className="col operation"></div>
</div>
</div>
<InfiniteScroller ref={scrollerRef} onScroll={top=> setState({showToTop: top > 30})}>
<InfiniteScroller ref={scrollerRef} onScroll={top => setState({showToTop: top > 30})}>
{
videoData.length == 0 ? <div className="m-auto"><Empty/></div> :
<div className="sort-list-container flex-1">
@ -176,7 +194,8 @@ export default function VideoIndex() {
id={v.id}
key={index}
type={'create'}
active={state.playingIndex == index}
active={checkedIdArray.includes(v.id)}
playing={state.playingIndex == index}
checked={checkedIdArray.includes(v.id)}
className={`list-item-${index} mt-3 mb-2 list-item-state-${v.status}`}
onCheckedChange={(checked) => {
@ -186,8 +205,8 @@ export default function VideoIndex() {
return newArr;
})
}}
onItemClick={ () => playVideo(v, index)}
onRemove={()=>{}}
onItemClick={() => playVideo(v, index)}
onRemove={() => processDeleteVideo([v.id])}
onEdit={v.status == VideoStatus.Generating ? undefined : () => {
setEditId(v.article_id)
}}
@ -203,7 +222,7 @@ export default function VideoIndex() {
</div>
</div>
<div className="page-action">
<ButtonToTop visible={state.showToTop} onClick={()=>scrollerRef.current?.scrollToPosition(0)}/>
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
{checkedIdArray.length > 0 && <ButtonBatch
onProcess={deleteByIds}
selected={checkedIdArray}
@ -217,7 +236,7 @@ export default function VideoIndex() {
}}
>
<span className="text"></span>
<IconDelete />
<IconDelete/>
</ButtonBatch>}
<ButtonPush2Room ids={checkedIdArray} list={videoData} onSuccess={loadList}/>
</div>

View File

@ -90,6 +90,10 @@ export function calcContentLengthLikeWord(str:string) {
// 将时长转换成 时:分:秒
export function formatDuration(duration: number) {
if(duration < 0 || isNaN(duration)){
return '00:00:00';
}
duration = Math.ceil(duration);
const hour = Math.floor(duration / 3600);
const minute = Math.floor((duration - hour * 3600) / 60);
const second = duration - hour * 3600 - minute * 60;