Compare commits

...

1 Commits

Author SHA1 Message Date
9e1a7f521d feat(dev/live): ️独立展示视频生成页面
独立展示视频生成为主要页面;
去除权限判断;
视频生成页面能够自动播放后续视频
2025-03-17 17:12:25 +08:00
12 changed files with 177 additions and 470 deletions

View File

@ -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 {

View File

@ -19,4 +19,14 @@
@content;
}
}
@if $name == xxl {
@media (max-width: 1799px) {
@content;
}
}
@if $name == xxxl {
@media (max-width: 1999px) {
@content;
}
}
}

View File

@ -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>
}

View File

@ -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
View 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
};
}

View File

@ -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 ">

View File

@ -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>)
}

View File

@ -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>)
}

View File

@ -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: {

View File

@ -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>

View File

@ -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'}>

View File

@ -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