Compare commits
10 Commits
826712f910
...
9e1a7f521d
Author | SHA1 | Date | |
---|---|---|---|
9e1a7f521d | |||
fcf31294b7 | |||
45b0912d48 | |||
0bf20343d0 | |||
d782801420 | |||
0dec5aa1f2 | |||
51e133b273 | |||
54056aec3a | |||
496192061f | |||
b65631ad9c |
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
}
|
@ -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
|
||||
};
|
||||
}
|
@ -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",
|
||||
|
@ -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": "请选择要推入编辑的新闻",
|
||||
|
@ -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>
|
||||
</>)
|
||||
|
@ -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,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>)
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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)}/>
|
||||
|
@ -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>)
|
||||
}
|
@ -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>
|
||||
|
@ -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(() => {
|
||||
|
@ -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