feat(dev/live): ✨️独立展示视频生成页面
独立展示视频生成为主要页面; 去除权限判断; 视频生成页面能够自动播放后续视频
This commit is contained in:
parent
fcf31294b7
commit
9e1a7f521d
@ -13,7 +13,7 @@
|
||||
--navigation-width: 100vw;
|
||||
--navigation-active-color: #ffe0e0;
|
||||
--app-header-header: 70px;
|
||||
--container-width: 1800px;
|
||||
--container-width: 1600px;
|
||||
--header-z-index: 99999;
|
||||
--message-z-index: 100001;
|
||||
}
|
||||
@ -300,7 +300,7 @@
|
||||
|
||||
.data-list-container {
|
||||
@apply list-scroller-container;
|
||||
height: calc(100vh - var(--app-header-header) - 200px);
|
||||
height: calc(100vh - var(--app-header-header) - 100px);
|
||||
|
||||
.data-list-container-inner {
|
||||
|
||||
|
@ -19,4 +19,14 @@
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $name == xxl {
|
||||
@media (max-width: 1799px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $name == xxxl {
|
||||
@media (max-width: 1999px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ type Props = {
|
||||
onEdit?: () => void;
|
||||
onRegenerate?: () => void;
|
||||
hideCheckBox?: boolean;
|
||||
operationRender?:React.ReactNode;
|
||||
onItemClick?: () => void;
|
||||
onRemove?: (action?:'delete' | 'rollback') => void;
|
||||
removeIcon?: React.ReactNode;
|
||||
@ -43,7 +44,8 @@ export const VideoListItem = (
|
||||
id, video, onRemove,removeIcon, checked,playing,
|
||||
onCheckedChange, onEdit, active, editable,
|
||||
className, sortable, type, index,onItemClick,
|
||||
additionOperation,onRegenerate,hideCheckBox
|
||||
additionOperation,onRegenerate,hideCheckBox,
|
||||
operationRender
|
||||
}: Props) => {
|
||||
const {
|
||||
attributes, listeners,
|
||||
@ -111,7 +113,7 @@ export const VideoListItem = (
|
||||
{... (sortable && !generating?listeners:{})}
|
||||
{... (sortable && !generating?attributes:{})}
|
||||
>{video.ctime ? formatTime(video.ctime,'min') : '-'}</div>
|
||||
<div className="col operation">
|
||||
{operationRender ?? <div className="col operation">
|
||||
{/*{sortable && !generating && (!active ?*/}
|
||||
{/* <button className="hover:text-blue-500 cursor-move">*/}
|
||||
{/* <MenuOutlined/>*/}
|
||||
@ -154,7 +156,7 @@ export const VideoListItem = (
|
||||
</>}
|
||||
{additionOperation}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -20,7 +20,7 @@ const AuthContext = createContext<AuthContextType | null>(null)
|
||||
// 权限相关初始化数据
|
||||
const initialState: AuthProps = {
|
||||
isLoggedIn: false,
|
||||
isInitialized: false,
|
||||
isInitialized: true,
|
||||
user: null
|
||||
};
|
||||
|
||||
@ -125,7 +125,7 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
init().then(console.log)
|
||||
//init().then(console.log)
|
||||
}, [])
|
||||
|
||||
// 判断是否已经初始化
|
||||
|
14
src/hooks/useLastState.ts
Normal file
14
src/hooks/useLastState.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
// 通过useRef及useEffect实现 获取最新的state值
|
||||
export function useLastState<T>(value: T){
|
||||
// 创建ref
|
||||
const lastState = React.useRef<T>(value);
|
||||
lastState.current = value;
|
||||
// 通过useEffect监听value的变化
|
||||
const getLastState = React.useCallback(() => lastState.current, []);
|
||||
// 返回最新的state值
|
||||
return {
|
||||
lastState,
|
||||
getLastState
|
||||
};
|
||||
}
|
@ -8,7 +8,7 @@ export default function LivePlayer() {
|
||||
const [liveUrl, setLiveUrl] = useState<string>('http://fm.live.starbitech.com/fm/prod_fm.flv')
|
||||
useMount(async ()=>{
|
||||
getLiveUrl().then((ret)=>{
|
||||
setLiveUrl(ret.flv_url)
|
||||
//setLiveUrl(ret.flv_url)
|
||||
})
|
||||
})
|
||||
return <div className="live-player-wrapper ">
|
||||
|
@ -1,20 +1,15 @@
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {Checkbox, Empty, Modal, Space} from "antd";
|
||||
import {SortableContext, arrayMove} from '@dnd-kit/sortable';
|
||||
import {DndContext} from "@dnd-kit/core";
|
||||
import {Empty, Space} from "antd";
|
||||
|
||||
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||
import {deleteByIds, getList, modifyOrder, playState} from "@/service/api/live.ts";
|
||||
import {getList, 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 InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const cache: { flvPlayer?: FlvJs.Player, timerPlayNext?: any, timerLoadState?: any, prevUrl?: string } = {}
|
||||
@ -23,9 +18,7 @@ export default function LiveIndex() {
|
||||
const player = useRef<PlayerInstance | null>(null)
|
||||
|
||||
const [videoData, setVideoData] = useState<LiveVideoInfo[]>([])
|
||||
const [modal, contextHolder] = Modal.useModal()
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
const [editable, setEditable] = useState<boolean>(false)
|
||||
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
|
||||
|
||||
const [state, setState] = useSetState({
|
||||
@ -40,6 +33,7 @@ export default function LiveIndex() {
|
||||
const activeIndex = useRef(-1)
|
||||
useEffect(() => {
|
||||
activeIndex.current = videoData.findIndex(s=>s.id == state.playId)
|
||||
document.querySelector(`.video-item-${state.playId}`)?.scrollIntoView({behavior: 'smooth'})
|
||||
}, [state.playId,videoData])
|
||||
|
||||
// 显示当前播放视频对应 view item
|
||||
@ -150,52 +144,6 @@ export default function LiveIndex() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 删除视频
|
||||
const processDeleteVideo = async (ids: Id[]) => {
|
||||
deleteByIds(ids).then(() => {
|
||||
showToast(t('delete_success'), 'success')
|
||||
loadList()
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
//
|
||||
const handleConfirm = () => {
|
||||
if (!editable) {
|
||||
setEditable(true)
|
||||
return;
|
||||
}
|
||||
const newSort = videoData.map(s => s.id).join(',')
|
||||
if (newSort == state.originSort) {
|
||||
setEditable(false)
|
||||
return;
|
||||
}
|
||||
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');
|
||||
loadList()
|
||||
setEditable(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleAllCheckedChange = () => {
|
||||
if(editable) return;
|
||||
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
|
||||
setState({
|
||||
checkedAll: !state.checkedAll
|
||||
})
|
||||
}
|
||||
|
||||
// 视频相关时长
|
||||
const totalDuration = useMemo(() => {
|
||||
if (!videoData || videoData.length == 0) return 0;
|
||||
@ -222,21 +170,18 @@ export default function LiveIndex() {
|
||||
// return checkedIdArray.filter(id => currentId.id != id)
|
||||
// }, [checkedIdArray, state.activeIndex])
|
||||
|
||||
const currentSelectedVideoIds = useMemo(()=>{
|
||||
return checkedIdArray.length == 0 ? [] : checkedIdArray.filter(id => id != state.playId)
|
||||
},[checkedIdArray, state.playId])
|
||||
|
||||
return (<div className="container py-5 page-live">
|
||||
<div className="h-[36px]"></div>
|
||||
<div className="flex">
|
||||
<div className="video-player-container mr-16 flex items-center">
|
||||
<div className="video-player-container mr-16 flex ">
|
||||
<div>
|
||||
<div className="text-center text-base text-gray-400">{formatDuration(totalDuration)}</div>
|
||||
<div className="video-player flex justify-center flex-1 mt-1">
|
||||
<div className="live-player relative rounded overflow-hidden w-[360px] h-[636px]"
|
||||
<div className="live-player relative rounded overflow-hidden w-[420px] h-[740px]"
|
||||
style={{backgroundColor: 'hsl(210, 100%, 48%)'}}>
|
||||
<Player
|
||||
ref={player} className="w-[360px] h-[636px] bg-white"
|
||||
ref={player} className="w-[420px] h-[740px] bg-white"
|
||||
muted={true}
|
||||
onProgress={(progress) => {
|
||||
setState({playProgress: progress})
|
||||
@ -259,21 +204,7 @@ 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>
|
||||
<div className="check-all ml-10">
|
||||
<button disabled={editable} className={`${editable?'':'hover:text-blue-300'} text-gray-400`}
|
||||
onClick={handleAllCheckedChange}>
|
||||
<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()}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="list-header">
|
||||
@ -282,7 +213,6 @@ export default function LiveIndex() {
|
||||
<div className="col cover">{t('video.title_thumb')}</div>
|
||||
<div className="col title">{t('video.title')}</div>
|
||||
<div className="col generated-time">{t('video.title_generated_time')}</div>
|
||||
<div className="col operation">{t('video.title_operation')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
@ -296,66 +226,33 @@ export default function LiveIndex() {
|
||||
>
|
||||
{videoData.length == 0 && <div className="m-auto py-16"><Empty/></div>}
|
||||
<div className="sort-list-container flex-1">
|
||||
<DndContext onDragEnd={(e) => {
|
||||
const {active, over} = e;
|
||||
if (over && active.id !== over.id) {
|
||||
let oldIndex = -1, newIndex = -1;
|
||||
setVideoData((items) => {
|
||||
oldIndex = items.findIndex(s => s.id == active.id);
|
||||
newIndex = items.findIndex(s => s.id == over.id);
|
||||
return arrayMove(items, oldIndex, newIndex);
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<SortableContext items={videoData}>
|
||||
{videoData.map((v, index) => (
|
||||
<VideoListItem
|
||||
video={v}
|
||||
index={index + 1}
|
||||
id={v.id}
|
||||
key={index}
|
||||
active={state.playId == v.id}
|
||||
playing={state.playId == v.id}
|
||||
className={`list-index-${index} list-item-${v.id} mt-3 mb-2`}
|
||||
checked={checkedIdArray.includes(v.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newIdArray = checked ? checkedIdArray.concat(v.id) : checkedIdArray.filter(id => id != v.id);
|
||||
setState({checkedAll: newIdArray.length == videoData.length})
|
||||
setCheckedIdArray(newIdArray)
|
||||
// setCheckedIdArray(idArray => {
|
||||
// return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
|
||||
// })
|
||||
}}
|
||||
onRemove={() => processDeleteVideo([v.id])}
|
||||
editable={!editable && state.playId != v.id}
|
||||
sortable={editable && state.playId != v.id}
|
||||
/>))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
{videoData.map((v, index) => (
|
||||
<VideoListItem
|
||||
video={v}
|
||||
index={index + 1}
|
||||
id={v.id}
|
||||
key={index}
|
||||
active={state.playId == v.id}
|
||||
playing={state.playId == v.id}
|
||||
className={`list-index-${index} list-item-${v.id} video-item-${v.id} mt-3 mb-2`}
|
||||
checked={checkedIdArray.includes(v.id)}
|
||||
operationRender={<></>}
|
||||
onCheckedChange={(checked) => {
|
||||
const newIdArray = checked ? checkedIdArray.concat(v.id) : checkedIdArray.filter(id => id != v.id);
|
||||
setState({checkedAll: newIdArray.length == videoData.length})
|
||||
setCheckedIdArray(newIdArray)
|
||||
// setCheckedIdArray(idArray => {
|
||||
// return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
|
||||
// })
|
||||
}}
|
||||
editable={false}
|
||||
sortable={false}
|
||||
/>))}
|
||||
</div>
|
||||
<div className="h-[100px]"></div>
|
||||
</InfiniteScroller>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-action">
|
||||
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
|
||||
{currentSelectedVideoIds.length > 0 && <ButtonBatch
|
||||
className='bg-gray-300 hover:bg-gray-400 text-white'
|
||||
selected={currentSelectedVideoIds}
|
||||
emptyMessage={t('video.delete_empty')}
|
||||
confirmMessage={currentSelectedVideoIds.length > 1?
|
||||
t('video.delete_description_count',{count:currentSelectedVideoIds.length})
|
||||
:
|
||||
t('video.delete_description',{count:currentSelectedVideoIds.length})}
|
||||
onSuccess={loadList}
|
||||
onProcess={processDeleteVideo}
|
||||
>
|
||||
<span className={'text'}>{t('delete_batch')}</span>
|
||||
<IconDelete/>
|
||||
</ButtonBatch>}
|
||||
</div>
|
||||
{contextHolder}
|
||||
</div>)
|
||||
}
|
@ -1,29 +1,31 @@
|
||||
import {Checkbox, Empty, Space} from "antd";
|
||||
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {DndContext} from "@dnd-kit/core";
|
||||
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
|
||||
import {useSetState} from "ahooks";
|
||||
import {Empty} from "antd";
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {useGetState, useSetState} from "ahooks";
|
||||
|
||||
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
||||
import {deleteFromList, getList, modifyOrder, regenerateById, VideoStatus} from "@/service/api/video.ts";
|
||||
import {getList, VideoStatus} from "@/service/api/video.ts";
|
||||
import {formatDuration} from "@/util/strings.ts";
|
||||
import ButtonBatch from "@/components/button-batch.tsx";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {showErrorToast} 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, useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useLastState} from "@/hooks/useLastState.ts";
|
||||
|
||||
function isFullyInViewport(ele: HTMLElement, container: HTMLElement) {
|
||||
return ele.offsetTop >= container.scrollTop && ele.offsetTop + ele.offsetHeight <= container.scrollTop + container.offsetHeight
|
||||
}
|
||||
|
||||
function getNormalVideoList(list: VideoInfo[]) {
|
||||
return list?.filter(s => {
|
||||
return s.status != VideoStatus.Generating && s.oss_video_url
|
||||
}) || []
|
||||
}
|
||||
|
||||
export default function VideoIndex() {
|
||||
const {t} = useTranslation()
|
||||
const [editId, setEditId] = useState(-1)
|
||||
const loc = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const [videoData, setVideoData] = useState<VideoInfo[]>([])
|
||||
|
||||
const [videoData, setVideoData, getLastVideoList] = useGetState<VideoInfo[]>([])
|
||||
const player = useRef<PlayerInstance | null>(null)
|
||||
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
|
||||
const [state, setState] = useSetState({
|
||||
@ -35,16 +37,18 @@ export default function VideoIndex() {
|
||||
current: -1,
|
||||
total: -1
|
||||
},
|
||||
loading:false,
|
||||
loading: false,
|
||||
playVideoUrl: ''
|
||||
})
|
||||
const {lastState} = useLastState(state);
|
||||
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<Id[]>([])
|
||||
const [refreshTimer,setTimer] = useState(0)
|
||||
const [refreshTimer, setTimer] = useState(0)
|
||||
|
||||
// 加载列表
|
||||
const loadList = (needReset = true) => {
|
||||
if(state.loading) return;
|
||||
if(refreshTimer) {
|
||||
const loadList = (needReset = true, onLoad = false) => {
|
||||
if (state.loading) return;
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer)
|
||||
setTimer(0)
|
||||
}
|
||||
@ -59,67 +63,53 @@ export default function VideoIndex() {
|
||||
// 判断是否有生成中的视频
|
||||
if (list.filter(s => s.status == VideoStatus.Generating).length > 0) {
|
||||
// 每5s重新获取一次最新数据
|
||||
setTimer(()=>setTimeout(() => loadList(false), 5000) as any);
|
||||
setTimer(() => setTimeout(() => loadList(false), 5000) as any);
|
||||
}
|
||||
if(onLoad){
|
||||
const _list = getNormalVideoList(list)
|
||||
if(_list.length > 0){
|
||||
playVideo(_list[0])
|
||||
}
|
||||
}
|
||||
}).catch(showErrorToast)
|
||||
.finally(()=>{
|
||||
setState({loading: false})
|
||||
})
|
||||
return ()=>{
|
||||
if(refreshTimer){
|
||||
.finally(() => {
|
||||
setState({loading: false})
|
||||
})
|
||||
return () => {
|
||||
if (refreshTimer) {
|
||||
|
||||
clearTimeout(refreshTimer)
|
||||
}
|
||||
console.log('go out',refreshTimer)
|
||||
console.log('go out', refreshTimer)
|
||||
}
|
||||
}
|
||||
|
||||
// 播放视频
|
||||
const playVideo = (video: VideoInfo) => {
|
||||
console.log('play video',video)
|
||||
if (state.playingId == video.id) {
|
||||
player.current?.pause();
|
||||
setState({playingId: -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.oss_video_url && video.status !== 1) {
|
||||
setState({playingId: video.id})
|
||||
player.current?.play(video.oss_video_url, 0)
|
||||
}
|
||||
}
|
||||
// 处理全选
|
||||
const handleAllCheckedChange = () => {
|
||||
setCheckedIdArray(state.checkedAll ? [] : videoData.filter(s=>s.status == VideoStatus.Generated).map(v => v.id))
|
||||
setState({
|
||||
checkedAll: !state.checkedAll
|
||||
})
|
||||
}
|
||||
const handleModifySort = (items: VideoInfo[]) => {
|
||||
|
||||
modifyOrder(items.map(s => s.id)).then(() => {
|
||||
showToast(t('video.sort_modify_success'), 'success')
|
||||
}).catch(() => {
|
||||
loadList();
|
||||
showToast(t('video.sort_modify_failed'), 'warning')
|
||||
})
|
||||
|
||||
return ()=>{
|
||||
try{
|
||||
Array.from(document.querySelectorAll('video')).forEach(v => v.pause())
|
||||
}catch (e){
|
||||
console.log(e)
|
||||
const videoElement = document.querySelector(`.video-item-${video.id}`) as HTMLElement
|
||||
const scroller = document.querySelector('.data-list-container') as HTMLElement;
|
||||
if(videoElement && isFullyInViewport(videoElement, scroller)) {
|
||||
videoElement.scrollIntoView({behavior: 'smooth'})
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
useEffect(loadList, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadList(true, true)
|
||||
}, [])
|
||||
// const totalDuration = useMemo(() => {
|
||||
// if (!videoData || videoData.length == 0) return 0;
|
||||
// const v = state.playingId == -1 ? null : videoData.find(s=>s.id == state.playingId)
|
||||
// const v = state.playingId == -1 ? null : videoData.find(s => s.id == state.playingId)
|
||||
// if (!v) return 0
|
||||
// //const v = videoData[state.playingIndex] as VideoInfo;
|
||||
// return Math.ceil(v.duration / 1000)
|
||||
@ -127,48 +117,37 @@ export default function VideoIndex() {
|
||||
// //return videoData.reduce((sum, v) => sum + Math.ceil(v.duration / 1000), 0);
|
||||
// }, [videoData, state.playingId])
|
||||
|
||||
useEffect(() => {
|
||||
if (loc.state == 'push-success' && !state.showStatePos && videoData.length && scrollerRef.current) {
|
||||
const generatingItem = document.querySelector(`.list-item-state-${VideoStatus.Generating}`)
|
||||
if (generatingItem) {
|
||||
generatingItem.scrollIntoView({behavior: 'smooth'})
|
||||
setState({showStatePos: true})
|
||||
}
|
||||
|
||||
const handlePlayEnd = () => {
|
||||
const list = getLastVideoList();
|
||||
if (!list?.length) return;
|
||||
const _list = getNormalVideoList(list)
|
||||
if (_list.length > 0) {
|
||||
const _currentIndex = _list.findIndex(s => s.id == lastState.current.playingId)
|
||||
const _next = _currentIndex != -1 && _currentIndex < _list.length - 1 ? _list[_currentIndex + 1] : _list[0];
|
||||
playVideo(_next)
|
||||
}
|
||||
}, [videoData, scrollerRef])
|
||||
const processDeleteVideo = async (ids: Id[],action ?: string) => {
|
||||
deleteFromList(ids).then(() => {
|
||||
showToast(t('delete_success'), 'success')
|
||||
if(action == 'rollback'){
|
||||
navigate('/edit',{
|
||||
state: {action: 'rollback',id: ids[0]},
|
||||
})
|
||||
}else{
|
||||
loadList()
|
||||
}
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
const processGenerateVideo = async (video: VideoInfo) => {
|
||||
regenerateById(video.article_id).then(() => {
|
||||
//showToast(t('delete_success'), 'success')
|
||||
loadList()
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
|
||||
return (<div className="container py-5 page-live">
|
||||
<div className="h-[36px]"></div>
|
||||
<div className="flex">
|
||||
<div className="video-player-container mr-16 w-[360px] flex items-center">
|
||||
<div className="video-player-container mr-16 flex items-center">
|
||||
<div>
|
||||
<div className="text-center text-base text-gray-400">{t("generating.title")}</div>
|
||||
<div className="video-player flex items-center mt-2">
|
||||
<div className=" w-[360px] h-[636px] rounded overflow-hidden">
|
||||
{/*videoData[state.playingIndex]?.oss_video_url*/}
|
||||
<div className=" w-[420px] h-[740px] rounded overflow-hidden">
|
||||
<Player
|
||||
ref={player}
|
||||
url={state.playVideoUrl}
|
||||
onChange={(state) => {
|
||||
if (state.end || state.error) setState({playingId: -1})
|
||||
showControls={true}
|
||||
onChange={(_state) => {
|
||||
console.log('onChange', _state)
|
||||
if (_state.end) {
|
||||
handlePlayEnd();
|
||||
return;
|
||||
}
|
||||
if (_state.error) setState({playingId: -1})
|
||||
}}
|
||||
onProgress={(current, duration) => {
|
||||
setState({
|
||||
@ -178,26 +157,18 @@ export default function VideoIndex() {
|
||||
}
|
||||
})
|
||||
}}
|
||||
className="w-[360px] h-[640px] bg-white"/>
|
||||
className="w-[420px] h-[740px] bg-white"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center text-sm mt-4 text-gray-400">{formatDuration(state.playState.current)} / {formatDuration(state.playState.total)}</div>
|
||||
<div
|
||||
className="text-center text-sm mt-4 text-gray-400">{formatDuration(state.playState.current)} / {formatDuration(state.playState.total)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="video-list-container rounded mt-2 flex flex-col flex-1">
|
||||
<div className="live-control flex justify-between">
|
||||
<div className="pl-[70px]"></div>
|
||||
<div className="flex items-center">
|
||||
<Space size={20}>
|
||||
<span>{t('select.total',{count:videoData.length || 0})}</span>
|
||||
<span className={'text-blue-500'}>{t('select.selected_some',{count:checkedIdArray.length})}</span>
|
||||
</Space>
|
||||
<button className="hover:text-blue-300 text-gray-400 ml-5"
|
||||
onClick={handleAllCheckedChange}>
|
||||
<span className="text-sm mr-2">{t("select.select_all")}</span>
|
||||
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
|
||||
</button>
|
||||
<Checkbox checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className={'video-list-sort-container flex-1 mt-1'}>
|
||||
@ -207,102 +178,47 @@ export default function VideoIndex() {
|
||||
<div className="col cover">{t('video.title_thumb')}</div>
|
||||
<div className="col title">{t('video.title')}</div>
|
||||
<div className="col generated-time">{t('video.title_generated_time')}</div>
|
||||
<div className="col operation">{t('video.title_operation')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<InfiniteScroller loading={state.loading} ref={scrollerRef} onScroll={top => setState({showToTop: top > 30})}>
|
||||
{
|
||||
videoData.length == 0 ? <div className="m-auto py-16"><Empty/></div> :
|
||||
<div className="sort-list-container flex-1">
|
||||
<DndContext onDragEnd={(e) => {
|
||||
const {active, over} = e;
|
||||
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);
|
||||
const newSorts = arrayMove(items, oldIndex, newIndex);
|
||||
handleModifySort(newSorts)
|
||||
return newSorts;
|
||||
});
|
||||
// modal.confirm({
|
||||
// title: '提示',
|
||||
// content: '是否要移动到指定位置',
|
||||
// onOk: handleModifySort,
|
||||
// onCancel: () => {
|
||||
// setVideoData(originArr);
|
||||
// }
|
||||
// })
|
||||
}
|
||||
}}>
|
||||
<SortableContext items={videoData}>
|
||||
{videoData.map((v, index) => (
|
||||
<VideoListItem
|
||||
video={v}
|
||||
index={index + 1}
|
||||
id={v.id}
|
||||
key={index}
|
||||
type={'create'}
|
||||
active={checkedIdArray.includes(v.id)}
|
||||
playing={state.playingId == v.id}
|
||||
checked={checkedIdArray.includes(v.id)}
|
||||
className={`list-item-${index} mt-3 mb-2 list-item-state-${v.status} `}
|
||||
onCheckedChange={(checked) => {
|
||||
setCheckedIdArray(idArray => {
|
||||
const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
|
||||
setState({checkedAll: newArr.length == videoData.length})
|
||||
return newArr;
|
||||
})
|
||||
}}
|
||||
onItemClick={() => playVideo(v)}
|
||||
onRemove={(action) => processDeleteVideo([v.id],action)}
|
||||
onEdit={v.status == VideoStatus.Generated ? () => {
|
||||
setEditId(v.article_id)
|
||||
}:undefined}
|
||||
onRegenerate={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated?()=>{
|
||||
processGenerateVideo(v)
|
||||
}:undefined}
|
||||
hideCheckBox={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated}
|
||||
editable={v.status != VideoStatus.Generating}
|
||||
sortable={v.status == VideoStatus.Generated}
|
||||
/>))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
{videoData.map((v, index) => (
|
||||
<VideoListItem
|
||||
video={v}
|
||||
index={index + 1}
|
||||
id={v.id}
|
||||
key={index}
|
||||
type={'create'}
|
||||
active={checkedIdArray.includes(v.id)}
|
||||
playing={state.playingId == v.id}
|
||||
checked={checkedIdArray.includes(v.id)}
|
||||
className={`list-item-${index} video-item-${v.id} mt-3 mb-2 list-item-state-${v.status} `}
|
||||
onCheckedChange={(checked) => {
|
||||
setCheckedIdArray(idArray => {
|
||||
const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
|
||||
setState({checkedAll: newArr.length == videoData.length})
|
||||
return newArr;
|
||||
})
|
||||
}}
|
||||
onItemClick={() => playVideo(v)}
|
||||
operationRender={<></>}
|
||||
onEdit={undefined}
|
||||
onRegenerate={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated ? () => {
|
||||
} : undefined}
|
||||
hideCheckBox={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated}
|
||||
editable={v.status != VideoStatus.Generating}
|
||||
sortable={v.status == VideoStatus.Generated}
|
||||
/>))}
|
||||
</div>
|
||||
}
|
||||
<div className="h-[130px]"></div>
|
||||
</InfiniteScroller>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-action">
|
||||
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
|
||||
{checkedIdArray.length > 0 && <ButtonBatch
|
||||
onProcess={deleteFromList}
|
||||
selected={checkedIdArray}
|
||||
emptyMessage={t('video.delete_empty')}
|
||||
title={
|
||||
checkedIdArray.length == 1 ? t('video.delete_description',{count:checkedIdArray.length}):
|
||||
t('video.delete_description_count',{count:checkedIdArray.length})
|
||||
}
|
||||
className='bg-gray-300 hover:bg-gray-400 text-white'
|
||||
confirmMessage={<span dangerouslySetInnerHTML={{
|
||||
__html:checkedIdArray.length == 1?
|
||||
t('video.delete_confirm',{count:checkedIdArray.length}):
|
||||
t('video.delete_confirm_count',{count:checkedIdArray.length})
|
||||
}}></span>}
|
||||
onSuccess={() => {
|
||||
showToast(t('delete_success'), 'success')
|
||||
loadList()
|
||||
}}
|
||||
>
|
||||
<span className="text">{t('delete_batch')}</span>
|
||||
<IconDelete/>
|
||||
</ButtonBatch>}
|
||||
<ButtonPush2Room ids={checkedIdArray} list={videoData} onSuccess={loadList}/>
|
||||
</div>
|
||||
</div>
|
||||
<ArticleEditModal type={'video'} id={editId} onClose={() => setEditId(-1)}/>
|
||||
</div>)
|
||||
}
|
@ -5,18 +5,17 @@ import zhCN from 'antd/locale/zh_CN';
|
||||
// for date-picker i18n
|
||||
import dayjs from "dayjs";
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import ErrorBoundary from "./error.tsx";
|
||||
import Loader from "@/components/loader.tsx";
|
||||
import routes from "@/routes/routes.tsx";
|
||||
import {DocumentTitle} from "@/components/document.tsx";
|
||||
import useConfig from "@/hooks/useConfig.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import useGlobalConfig from "@/hooks/useGlobalConfig.ts";
|
||||
import VideoIndex from "@/pages/video";
|
||||
|
||||
|
||||
const router = createBrowserRouter([
|
||||
...routes,
|
||||
{path: '*', element: <ErrorBoundary errorCode={404}/>}
|
||||
{path: '*', element: <VideoIndex />}
|
||||
], {
|
||||
basename: import.meta.env.VITE_APP_BASE_NAME,
|
||||
future: {
|
||||
|
@ -1,81 +1,19 @@
|
||||
import {Outlet, useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import {Button, Divider, Dropdown, MenuProps} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
import {Outlet, useSearchParams} from "react-router-dom";
|
||||
import {Button} from "antd";
|
||||
import React from "react";
|
||||
|
||||
import AuthGuard from "@/routes/layout/auth-guard.tsx";
|
||||
import {LogoText} from "@/components/icons/logo.tsx";
|
||||
|
||||
import {UserAvatar} from "@/components/icons/user-avatar.tsx";
|
||||
import {DashboardNavigation} from "@/routes/layout/dashboard-navigation.tsx";
|
||||
|
||||
import useAuth from "@/hooks/useAuth.ts";
|
||||
import {hidePhone} from "@/util/strings.ts";
|
||||
import {defaultCache} from "@/hooks/useCache.ts";
|
||||
import {IconVideo} from "@/components/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import useConfig from "@/hooks/useConfig.ts";
|
||||
|
||||
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const NavigationUserContainer = () => {
|
||||
const {t } = useTranslation()
|
||||
const {logout, user} = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const handleLogout = ()=>{
|
||||
logout().then(() => navigate('/user'))
|
||||
}
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: 'profile',
|
||||
label: <div className="nav-item" onClick={() => navigate('/history')}>
|
||||
<IconVideo />
|
||||
<span className={"nav-text"}>{t('history.text')}</span>
|
||||
</div>,
|
||||
},
|
||||
// {
|
||||
// key: 'logout',
|
||||
// label: <div onClick={handleLogout}>退出</div>,
|
||||
// },
|
||||
];
|
||||
const UserButton = () => (<div
|
||||
className={`flex items-center rounded-3xl ${user ? 'bg-[#e3eeff]' : 'bg-primary-blue'} p-1 pr-2 cursor-pointer rounded`}>
|
||||
<UserAvatar className="user-avatar size-7"/>
|
||||
{user ? <span className={"username ml-2 text-sm"}>{hidePhone(user.nickname)}</span> : (
|
||||
<span className="text-sm mx-2 text-white">{t('login.title')}</span>
|
||||
)}
|
||||
</div>)
|
||||
return (<div className={"flex items-center justify-between gap-2 ml-10"}>
|
||||
{user ? <Dropdown
|
||||
rootClassName={'z-[999999] userinfo-drop-menu'}
|
||||
menu={{items}} placement="bottomRight"
|
||||
dropdownRender={(menu)=>(
|
||||
<div>
|
||||
<div className="user-profile flex gap-4">
|
||||
<div className="avatar"><UserAvatar className="user-avatar"/></div>
|
||||
<div className="info">
|
||||
<div>{user?.nickname}</div>
|
||||
<div>ID: {user?.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<div className="menu-list-container">
|
||||
{menu}
|
||||
</div>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<div className="logout">
|
||||
<div onClick={handleLogout}>{t('user.logout')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)}
|
||||
>
|
||||
<div><UserButton/></div>
|
||||
</Dropdown> : <UserButton/>}
|
||||
</div>)
|
||||
}
|
||||
export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
|
||||
const {i18n} = useTranslation();
|
||||
const [params] = useSearchParams();
|
||||
@ -88,15 +26,14 @@ export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
|
||||
<DashboardNavigation/>
|
||||
<div className="flex items-center">
|
||||
{(params.get('lang') == 'yes' || AppConfig.APP_LANG == 'multiple') && <div>
|
||||
{
|
||||
i18n.language == 'zh-CN'?(
|
||||
<Button className="ml-2" onClick={()=>i18n.changeLanguage('en-US')}>Change To EN</Button>
|
||||
):(
|
||||
<Button className="ml-2" onClick={()=>i18n.changeLanguage('zh-CN')}>显示中文</Button>
|
||||
)
|
||||
}
|
||||
</div>}
|
||||
<NavigationUserContainer/>
|
||||
{
|
||||
i18n.language == 'zh-CN' ? (
|
||||
<Button className="ml-2" onClick={() => i18n.changeLanguage('en-US')}>Change To EN</Button>
|
||||
) : (
|
||||
<Button className="ml-2" onClick={() => i18n.changeLanguage('zh-CN')}>显示中文</Button>
|
||||
)
|
||||
}
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-content flex-1 box-sizing">
|
||||
@ -110,14 +47,6 @@ export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
|
||||
|
||||
|
||||
const DashboardLayout: React.FC<{ children?: React.ReactNode }> = ({children}) => {
|
||||
const loc = useLocation()
|
||||
const navigate = useNavigate()
|
||||
useEffect(()=>{
|
||||
if(!defaultCache.firstLoadPath && loc.pathname == '/live'){
|
||||
defaultCache.firstLoadPath = loc.pathname;
|
||||
navigate('/')
|
||||
}
|
||||
},[])
|
||||
return <AuthGuard>
|
||||
<div className="fixed">{defaultCache.firstLoadPath}</div>
|
||||
<BaseLayout>
|
||||
|
@ -11,35 +11,11 @@ export function DashboardNavigation() {
|
||||
const {t,i18n} = useTranslation()
|
||||
const {user} = useAuth()
|
||||
const NavItems = useMemo(()=>([
|
||||
{
|
||||
key: 'news',
|
||||
name: t('nav.materials'),
|
||||
icon: 'news',
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
name: t('nav.editing'),
|
||||
icon: 'e',
|
||||
path: '/edit'
|
||||
},
|
||||
{
|
||||
key: 'create',
|
||||
name: t('nav.generating'),
|
||||
icon: 'ai',
|
||||
path: '/create'
|
||||
},
|
||||
// {
|
||||
// key: 'library',
|
||||
// name: '视频库',
|
||||
// icon: '+',
|
||||
// path:'/library'
|
||||
// },
|
||||
{
|
||||
key: 'live',
|
||||
name: t('nav.live'),
|
||||
icon: 'v',
|
||||
path: '/live'
|
||||
path: '/'
|
||||
}
|
||||
]),[i18n.language])
|
||||
return (<div className={'flex app-main-navigation'}>
|
||||
|
@ -1,49 +1,13 @@
|
||||
import {RouteObject} from "react-router-dom";
|
||||
import ErrorBoundary from "@/routes/error.tsx";
|
||||
|
||||
;
|
||||
import DashboardLayout from "@/routes/layout/dashboard-layout.tsx";
|
||||
import React from "react";
|
||||
|
||||
const UserAuth = React.lazy(() => import("@/pages/user"))
|
||||
const CreateVideoIndex = React.lazy(() => import("@/pages/video"))
|
||||
const LibraryIndex = React.lazy(() => import("@/pages/library"))
|
||||
const LiveIndex = React.lazy(() => import("@/pages/live"))
|
||||
const NewsIndex = React.lazy(() => import("@/pages/news"))
|
||||
const NewsEdit = React.lazy(() => import("@/pages/news/edit.tsx"))
|
||||
import VideoIndex from "@/pages/video";
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
|
||||
{
|
||||
path: '/',
|
||||
element: <DashboardLayout/>,
|
||||
element: <VideoIndex/>,
|
||||
errorElement: <ErrorBoundary/>,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <NewsIndex/>
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
element: <UserAuth/>,
|
||||
},
|
||||
{
|
||||
path: 'edit',
|
||||
element: <NewsEdit/>
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
element: <CreateVideoIndex/>
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
element: <LibraryIndex/>
|
||||
},
|
||||
{
|
||||
path: 'live',
|
||||
element: <LiveIndex/>
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
export default routes
|
Loading…
x
Reference in New Issue
Block a user