Compare commits

...

10 Commits

Author SHA1 Message Date
9e1a7f521d feat(dev/live): ️独立展示视频生成页面
独立展示视频生成为主要页面;
去除权限判断;
视频生成页面能够自动播放后续视频
2025-03-17 17:12:25 +08:00
fcf31294b7 feat(dev/main): ️更新错误页面展示内容 2025-03-10 13:45:18 +08:00
45b0912d48 🐛 fixed : 跳过异常新闻和选中新闻一致时直接关闭 2025-03-08 22:15:54 +08:00
0bf20343d0 feat: 不能删除最后一组素材 2025-03-08 21:43:01 +08:00
d782801420 🐛 feat: 更新英文推送double check notice文案 2025-03-07 19:10:31 +08:00
0dec5aa1f2 🐛 feat: 直播界面解锁排序时禁止全选操作 2025-03-02 22:51:48 +08:00
51e133b273 💄 feat: 更新回滚图标 2025-03-02 22:50:42 +08:00
54056aec3a 🐛 feat: 新增 disableRemoveMessage 属性以控制删除提示信息 2025-03-02 12:24:50 +08:00
496192061f 📝 update: update toast message 2025-03-02 12:18:04 +08:00
b65631ad9c 🐛 fixed: deselect in empty data 2025-03-02 12:17:00 +08:00
22 changed files with 283 additions and 518 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

@ -16,6 +16,7 @@ type Props = {
blocks: BlockContent[];
editable?: boolean;
onChange?: (blocks: BlockContent[]) => void;
disableRemoveMessage?:string;
onRemove?: () => void;
onAdd?: (index:number,checkIndex:number) => void;
errorMessage?: string;
@ -46,6 +47,7 @@ export default function ArticleBlock(
blocks: defaultBlocks,
editable,
onRemove,
disableRemoveMessage,
onAdd,
onChange,
index,
@ -80,7 +82,7 @@ export default function ArticleBlock(
</div>
{editable && <div className="ml-2 flex items-center">
{
index > 0 ? <Popconfirm
disableRemoveMessage? <span></span> : <Popconfirm
rootClassName={'popconfirm-main'}
placement={'left'}
arrow={false}
@ -93,13 +95,13 @@ export default function ArticleBlock(
<span className="article-action-icon hidden group-hover:block ml-1" title={t('news.edit_delete_group')}>
<IconDelete style={{fontSize: 24}}/>
</span>
</Popconfirm> : <span></span>
</Popconfirm>
}
</div>}
</div>
{editable && <div className={'divider-container after'}><Divider>
<span onClick={()=>onAdd?.(index + 1,index)} className="article-action-add" title="新增分组"><IconAdd style={{fontSize: 24}}/></span>
<span onClick={()=>onAdd?.(index + 1,index)} className="article-action-add" title={t('news.materials.add_group')}><IconAdd style={{fontSize: 24}}/></span>
</Divider></div> }
</div>
}

View File

@ -1,10 +1,11 @@
import {Input, message} from "antd"
import {Divider, Input, message} from "antd"
import ArticleBlock from "@/components/article/block.tsx";
import styles from './article.module.scss'
import {showToast} from "@/components/message.ts";
import React from "react";
import {useTranslation} from "react-i18next";
import {IconAdd} from "@/components/icons";
type Props = {
groups: BlockContent[][];
@ -79,6 +80,11 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
<div className="panel-body py-3">
<div className="max-h-[485px] overflow-auto py-4">
{editable && groups.length == 1 && <div className={`${styles.blockContainer} group`}><div className={'divider-container before'}><Divider>
<span onClick={()=>handleAddGroup?.(1,1)} className="article-action-add" title={t('news.materials.add_group')}><IconAdd style={{fontSize: 24}}/></span>
</Divider></div></div> }
{groups.map((g, index) => (
index == 0 ? null : <ArticleBlock
editable={editable}
@ -93,8 +99,9 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
onAdd={(_index,checkIndex) => {
handleAddGroup?.(_index ? _index :index + 1,checkIndex)
}}
disableRemoveMessage={groups.length <= 1?t('news.edit_notice_keep_1'):''}
onRemove={async () => {
if (groups.length == 1) {
if (groups.length <= 1) {
message.warning(t('news.edit_notice_keep_1'))
return;
}

View File

@ -20,7 +20,7 @@ type Props = {
children?: React.ReactNode;
title?: React.ReactNode;
className?: string;
onError?: (e: Error | BizError) => void;
}
/**
*
@ -28,7 +28,7 @@ type Props = {
export default function ButtonBatch(
{
selected, emptyMessage, successMessage, children, icon,
title, confirmMessage, onProcess, onSuccess, className
title, confirmMessage, onProcess, onSuccess, className, onError
}: Props) {
const {t} = useTranslation()
const [loading, setLoading] = useState(false)
@ -42,7 +42,8 @@ export default function ButtonBatch(
onSuccess()
}
} catch (e) {
showErrorToast(e as unknown as BizError)
const _e = e as unknown as BizError
onError?onError(_e) : showErrorToast(_e)
} finally {
setLoading(false)
}
@ -63,7 +64,7 @@ export default function ButtonBatch(
onOk: onBatchProcess
})
}else{
onBatchProcess().catch(showErrorToast);
onBatchProcess().catch(onError || showErrorToast);
}
}

View File

@ -118,7 +118,15 @@ export const IconAddCircle = ({style, className}: IconProps) => (
fill="currentColor"/>
</svg>
)
export const IconRollbackCircle = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-warning`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 21 21" version="1.1">
<g>
<path d="M6.35192 0.404738C6.35236 0.404738 6.35272 0.4051 6.35272 0.405547V3.78207H12.3726C14.6365 3.78177 16.8099 4.66937 18.4238 6.25343C20.0378 7.8375 20.963 9.99107 21 12.2496V12.3918C20.9999 14.6502 20.1103 16.818 18.5231 18.428C16.9359 20.0379 14.7782 20.9611 12.5151 20.9984L12.3726 21H4.16146C4.05408 21 3.9511 20.9574 3.87516 20.8817C3.79923 20.8059 3.75657 20.7032 3.75657 20.596V18.9801C3.75657 18.8729 3.79923 18.7702 3.87516 18.6944C3.9511 18.6186 4.05408 18.5761 4.16146 18.5761H12.3726C14.0164 18.5932 15.5998 17.9581 16.7743 16.8105C17.9488 15.6628 18.6183 14.0966 18.6354 12.4565C18.6526 10.8163 18.0161 9.23653 16.8659 8.06464C15.7157 6.89274 14.146 6.22475 12.5022 6.20761H0.405711C0.324927 6.20777 0.24594 6.18382 0.178909 6.13883C0.111878 6.09384 0.0598667 6.02988 0.0295641 5.95516C-0.000738426 5.88044 -0.00794696 5.79839 0.00886551 5.71955C0.025678 5.64071 0.0657426 5.56869 0.123908 5.51275L5.66279 0.115483C5.71978 0.0597486 5.792 0.0220586 5.87039 0.00714763C5.94878 -0.00776331 6.02983 0.000769782 6.10338 0.031675C6.17692 0.0625803 6.23967 0.114479 6.28374 0.180854C6.32767 0.247003 6.35109 0.324588 6.35111 0.403927C6.35111 0.404375 6.35147 0.404738 6.35192 0.404738Z" fill="#B6A8AB"/>
</g>
</svg>
)
export const IconWarningCircle = ({style, className}: IconProps) => (
<svg className={`svg-icon ${className || ''} icon-warning`} style={style} xmlns="http://www.w3.org/2000/svg"
width="1em" height="1em" viewBox="0 0 22 22" version="1.1">

View File

@ -9,7 +9,7 @@ import {
IconEdit,
IconGenerateFailed,
IconGenerating,
IconPlaying, IconRegenerate,
IconPlaying, IconRegenerate, IconRollbackCircle,
IconWarningCircle
} from "@/components/icons";
import {VideoStatus} from "@/service/api/video.ts";
@ -30,8 +30,10 @@ type Props = {
onEdit?: () => void;
onRegenerate?: () => void;
hideCheckBox?: boolean;
operationRender?:React.ReactNode;
onItemClick?: () => void;
onRemove?: () => void;
onRemove?: (action?:'delete' | 'rollback') => void;
removeIcon?: React.ReactNode;
id: number;
className?: string;
type?: 'live' | 'create'
@ -39,10 +41,11 @@ type Props = {
export const VideoListItem = (
{
id, video, onRemove, checked,playing,
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,
@ -110,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/>*/}
@ -141,8 +144,8 @@ export const VideoListItem = (
icon={<IconWarningCircle/>}
title={t('video.delete_confirm_title')}
// description={`删除后需从重新${type == 'create' ? '生成' : '推流'}`}
onConfirm={onRemove}
><button className="hover:text-blue-500"><IconDelete/></button></Popconfirm>}
onConfirm={() => onRemove(failed ? 'rollback' : 'delete')}
><button className="hover:text-blue-500">{removeIcon?removeIcon:(failed?<IconRollbackCircle />:<IconDelete/>)}</button></Popconfirm>}
{hideCheckBox ? <span className={"inline-block w-[18px] h-1"}></span> : <Checkbox checked={state.checked} onChange={() => {
if (onCheckedChange) {
onCheckedChange(!state.checked)
@ -153,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

@ -1,8 +1,14 @@
{
"AppTitle": "AI Livesteam",
"go_to_home": "Go to Homepage" ,
"Hello": "Hello",
"cancel": "Cancel",
"close": "Close",
"service_error": "Service exception, please contact customer support.",
"error_401": "You do not have permission to access this page",
"error_403": "You do not have permission to access this page",
"error_404": "Page not found",
"error_500": "Service exception, please contact customer support.",
"confirm": {
"push_title": "Push Notice",
"push_video": "Are you sure editing selected news?",
@ -83,7 +89,8 @@
"get_detail_error": "Get new details failed",
"image_count": "Images",
"materials": {
"title": "News Materials"
"title": "News Materials",
"add_group": "Add Group"
},
"news_all_source": "All",
"push_empty": "please select the news to edit",
@ -156,9 +163,9 @@
"modal": {
"warning": "Warning",
"push_article": {
"content_normal": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, do you want to transfer them into videos?",
"content_normal": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, Do you want to transfer them into videos?",
"content_normal_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected. Do you want to transfer it to a video?",
"content_error": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, and metahuman contents are too short in the news below. Do you want to transfer them to videos?",
"content_error": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, and <span class=\"modal-count-warning\">{{error_count}}</span> metahuman contents are too short in these news below. Do you want to transfer them to videos?",
"content_error_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected, and the metahuman content is too short in this news. Do you want to transfer it to a video?",
"error_title": "Abnormal news",
"action_cancel": "Cancel",

View File

@ -1,8 +1,14 @@
{
"AppTitle": "数字人直播",
"go_to_home": "返回首页" ,
"Hello": "你好",
"cancel": "取消",
"close": "关闭",
"service_error": "新闻异常,无法生成,请咨询客服",
"error_401": "您没有权限访问本页面",
"error_403": "您没有权限访问本页面",
"error_404": "访问的页面不存在",
"error_500": "服务异常,请咨询客服.",
"confirm": {
"push_title": "推流提示",
"push_video": "是否确定一键推流选中新闻视频?",
@ -83,7 +89,8 @@
"get_detail_error": "获取新闻详情失败",
"image_count": "图片数",
"materials": {
"title": "新闻素材"
"title": "新闻素材",
"add_group": "新增分组"
},
"news_all_source": "全部来源",
"push_empty": "请选择要推入编辑的新闻",

View File

@ -12,6 +12,8 @@ import ButtonBatch from "@/components/button-batch.tsx";
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
import {IconArrowRight, IconDelete} from "@/components/icons";
import {useTranslation} from "react-i18next";
import {showToast} from "@/components/message.ts";
import {BizError} from "@/service/types.ts";
const DEFAULT_PAGE_LIMIT = {
page: 1,
@ -179,6 +181,9 @@ export default function LibraryIndex() {
icon={<IconArrowRight className={'text-white'}/>}
onProcess={push2room}
emptyMessage={t('video.push_empty')}
onError={e=>{
showToast(String((e as BizError).data || e.message),'error')
}}
>{t('video.push_to_live')}</ButtonBatch>}
</div>
</>)

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,51 +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 = () => {
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
setState({
checkedAll: !state.checkedAll
})
}
// 视频相关时长
const totalDuration = useMemo(() => {
if (!videoData || videoData.length == 0) return 0;
@ -221,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})
@ -258,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 className="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 checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
</div>
</div>
</div>
<div className="list-header">
@ -281,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="">
@ -295,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

@ -31,9 +31,13 @@ export default function ButtonPush2Video(props: PushVideoProps) {
const {t} = useTranslation()
const navigate = useNavigate()
const handlePush = (action: ProcessResult) => {
setLoading(true)
const skip = action === ProcessResult.Skip && state.errorIds.length > 0
const ids = !skip ? props.ids : props.ids.filter(id => !state.errorIds.includes(id));
if(skip && (state.errorIds.length == props.ids.length || ids.length == 0)){
setState({modalVisible: false})
return;
}
setLoading(true)
push2video(ids).then(() => {
setState({modalVisible: false})
if (skip) {
@ -45,7 +49,10 @@ export default function ButtonPush2Video(props: PushVideoProps) {
state: 'push-success'
})
// props.onSuccess?.()
}).catch(showErrorToast).finally(() => {
}).catch(()=>{
showToast(t('service_error'), 'error')
//showErrorToast
}).finally(() => {
setLoading(false)
})
}

View File

@ -106,7 +106,7 @@ export default function NewsIndex() {
<div className="overflow-auto leading-7 text-base news-detail-content-container"
style={{maxHeight: 500}} dangerouslySetInnerHTML={{__html: activeNews?.content || ''}}></div>
</div>
<div className="actions ml-3">
<div className="actions ml-2">
<div className="close">
<CloseOutlined className="text-xl text-gray-400 hover:text-gray-800"
onClick={() => setActiveNews(undefined)}/>

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, IconEdit} from "@/components/icons";
import {useLocation} from "react-router-dom";
import {useTranslation} from "react-i18next";
import {find} from "lodash";
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 [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,110 +63,91 @@ 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, [])
const totalDuration = useMemo(() => {
if (!videoData || videoData.length == 0) return 0;
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)
// 计算总时长
//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})
}
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)
// if (!v) 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.playingId])
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[]) => {
deleteFromList(ids).then(() => {
showToast(t('delete_success'), 'success')
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({
@ -172,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'}>
@ -201,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={() => processDeleteVideo([v.id])}
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

@ -1,47 +1,53 @@
import React from "react";
import React, {useMemo} from "react";
import {isRouteErrorResponse, useNavigate, useRouteError} from 'react-router-dom';
import {Button} from "antd";
import error500 from "@/assets/images/error/Error500.png";
import {useTranslation} from "react-i18next";
// ==============================|| ELEMENT ERROR - COMMON ||============================== //
const ErrorBoundary: React.FC<{
minHeight?: string | number;
errorCode?: 401 | 404 | 503
}> = ({ errorCode}) => {
}> = ({errorCode}) => {
const {t, i18n} = useTranslation()
const error = useRouteError() as Error;
let errorMessage = '服务异常,请稍后再试或者联系管理员.'
const errorConfig: {
[key: number]: string
} = {
401: '您没有权限访问本页面!',
404: '访问的页面不存在!',
503: '服务异常请联系管理员!',
}
if (isRouteErrorResponse(error)) {
if (errorConfig[error.status]) {
errorMessage = `Error ${error.status} - ${errorConfig[error.status]}`;
console.log(error)
const errorMessage = useMemo(() => {
let _message = t('error_500')
const errorConfig: {
[key: number]: string
} = {
401: t('error_401'),
403: t('error_403'),
404: t('error_404'),
500: t('error_500'),
}
}
if (errorCode) {
if (errorConfig[errorCode]) {
errorMessage = `Error ${errorCode} - ${errorConfig[errorCode]}`;
if (isRouteErrorResponse(error)) {
if (errorConfig[error.status]) {
_message = `Error ${error.status} - ${errorConfig[error.status]}`;
}
}
}
if (errorCode) {
if (errorConfig[errorCode]) {
_message = `Error ${errorCode} - ${errorConfig[errorCode]}`;
}
}
return _message
}, [i18n, errorCode])
const navigate = useNavigate()
const handleGoBack = () => {
navigate('/')
}
return (<div className="max-w-screen-lg mx-auto flex items-center h-screen">
<div className={'flex flex-row '}>
return (<div className="max-w-screen-lg mx-auto flex justify-center items-center h-screen">
<div className={'flex flex-row items-center'}>
<div className="flex-col">
<div className="sm:w-396px">
<img src={error500} alt="error" className="w-full"/>
</div>
</div>
<div className="flex-col md:w-full ml-10">
<div className="flex-col md:w-full ml-20">
<div className="text-center">
<div className="text-4xl">
<div className="text-3xl">
Internal Server Error
</div>
<div className="text-gray-400 my-5">
@ -62,10 +68,10 @@ const ErrorBoundary: React.FC<{
whiteSpace: 'break-spaces'
}}>{error.stack}</code>
</pre>
</div>}
<div className="flex flex-grow gap-2 mt-5 justify-center">
</div>}
<div className="flex flex-grow gap-2 mt-10 justify-center">
<Button type='primary' className="px-5" onClick={handleGoBack}>
<h1></h1>
<h1>{t('go_to_home')}</h1>
</Button>
</div>
</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/>}
{path: '*', element: <VideoIndex />}
], {
basename: import.meta.env.VITE_APP_BASE_NAME,
future: {
@ -28,9 +27,9 @@ const router = createBrowserRouter([
}
})
const {globalConfig} = useGlobalConfig();
// future={{v7_startTransition: true,v7_relativeSplatPath: true}}
const AppRouter = () => {
const {globalConfig} = useGlobalConfig();
const {t,i18n} = useTranslation();
useEffect(() => {

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