历史视频添加
This commit is contained in:
parent
1026c35c08
commit
daba38f188
@ -295,7 +295,31 @@
|
||||
.video-history-list-container{
|
||||
height: calc(100vh - var(--app-header-header) - 130px);
|
||||
}
|
||||
|
||||
.checkbox{
|
||||
@apply bg-black/10 backdrop-blur border border-white hover:border-blue-500 cursor-pointer relative;
|
||||
--size: 22px;
|
||||
border-width: 2px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 2px;
|
||||
&::before {
|
||||
@apply absolute hidden;
|
||||
border-left:solid 2px white;
|
||||
border-bottom:solid 2px white;
|
||||
left: 3px;
|
||||
top: 4px;
|
||||
content: ' ';
|
||||
width: calc(var(--size) - 8px);
|
||||
height: 6px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
&.checked{
|
||||
@apply border-blue-500 bg-blue-500;
|
||||
&:before{
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
}
|
||||
// override antd style
|
||||
.data-list-load-spin{
|
||||
.ant-spin-container::after{
|
||||
@ -411,7 +435,7 @@
|
||||
|
||||
// 全局按钮
|
||||
.page-action {
|
||||
@apply fixed right-10 bottom-10 flex flex-col gap-4;
|
||||
@apply fixed right-10 bottom-10 flex flex-col gap-4 z-10;
|
||||
button {
|
||||
@apply border-0 min-w-[120px] h-[40px] rounded-3xl pr-4 flex items-center justify-between drop-shadow;
|
||||
.text {
|
||||
|
@ -1,21 +1,24 @@
|
||||
import React, {useState} from "react";
|
||||
import {Modal} from "antd";
|
||||
import {App} from "antd";
|
||||
import {ButtonType} from "antd/es/button";
|
||||
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {BizError} from "@/service/types.ts";
|
||||
import {IconWarningCircle} from "@/components/icons";
|
||||
import {LoadingOutlined} from "@ant-design/icons";
|
||||
|
||||
type Props = {
|
||||
selected: any[],
|
||||
type?: ButtonType;
|
||||
emptyMessage: string,
|
||||
confirmMessage: React.ReactNode,
|
||||
onProcess: (ids: Id[]) => Promise<any|void>
|
||||
confirmMessage?: React.ReactNode,
|
||||
icon?: React.ReactNode,
|
||||
onProcess: (ids: Id[]) => Promise<any | void>
|
||||
successMessage?: string;
|
||||
onSuccess?: () => void;
|
||||
children?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
className?:string;
|
||||
className?: string;
|
||||
|
||||
}
|
||||
/**
|
||||
@ -23,10 +26,11 @@ type Props = {
|
||||
*/
|
||||
export default function ButtonBatch(
|
||||
{
|
||||
selected, emptyMessage, successMessage, children,
|
||||
title, confirmMessage, onProcess,onSuccess,className
|
||||
selected, emptyMessage, successMessage, children, icon,
|
||||
title, confirmMessage, onProcess, onSuccess, className
|
||||
}: Props) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const {modal} = App.useApp()
|
||||
const onBatchProcess = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@ -42,22 +46,29 @@ export default function ButtonBatch(
|
||||
}
|
||||
}
|
||||
const handleBtnClick = () => {
|
||||
if(loading) return;
|
||||
if (loading) return;
|
||||
if (selected.length == 0) {
|
||||
showToast(emptyMessage, 'warning')
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
wrapClassName:'root-modal-confirm',
|
||||
title: title || '操作提示',
|
||||
centered: true,
|
||||
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
|
||||
content: confirmMessage,
|
||||
onOk: onBatchProcess
|
||||
})
|
||||
if(confirmMessage){
|
||||
modal.confirm({
|
||||
wrapClassName: 'root-modal-confirm',
|
||||
title: title || '操作提示',
|
||||
centered: true,
|
||||
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
|
||||
content: confirmMessage,
|
||||
onOk: onBatchProcess
|
||||
})
|
||||
}else{
|
||||
onBatchProcess().catch(showErrorToast);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button disabled={loading} className={className} onClick={handleBtnClick}>{children}</button>
|
||||
<button disabled={loading} className={className} onClick={handleBtnClick}>
|
||||
<span className={'text'}>{children}</span>
|
||||
{loading ? <LoadingOutlined/> : icon}
|
||||
</button>
|
||||
)
|
||||
}
|
@ -93,7 +93,7 @@ export const IconAddText = ({style, className}: IconProps) => (
|
||||
export const IconVideo = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-video`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 21 20" version="1.1">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 1.00268e-07C18.5046 -0.000159579 18.9906 0.190406 19.3605 0.533497C19.7305 0.876588 19.9572 1.34684 19.995 1.85L20 2V16C20.0002 16.5046 19.8096 16.9906 19.4665 17.3605C19.1234 17.7305 18.6532 17.9572 18.15 17.995L18 18H2C1.49542 18.0002 1.00943 17.8096 0.639452 17.4665C0.269471 17.1234 0.0428434 16.6532 0.00500021 16.15L1.00268e-07 16V2C-0.000159579 1.49542 0.190406 1.00943 0.533497 0.639452C0.876588 0.269471 1.34684 0.0428434 1.85 0.00500021L2 1.00268e-07H18ZM18 2H2V16H18V2ZM8.34 4.638L8.858 4.868L9.196 5.028L9.583 5.218L10.013 5.436L10.483 5.686L10.99 5.966L11.256 6.118L11.774 6.423L12.248 6.715L12.678 6.988L13.058 7.241L13.538 7.571L13.902 7.834L13.997 7.904C14.1513 8.01883 14.2767 8.16816 14.363 8.34005C14.4494 8.51194 14.4943 8.70164 14.4943 8.894C14.4943 9.08636 14.4494 9.27606 14.363 9.44795C14.2767 9.61984 14.1513 9.76917 13.997 9.884L13.674 10.119L13.234 10.427L12.878 10.666L12.473 10.929L12.02 11.212L11.521 11.512L10.987 11.821L10.478 12.103L10.007 12.353L9.577 12.573L9.191 12.761L8.569 13.049L8.339 13.149C8.16242 13.2251 7.97051 13.2589 7.77856 13.2476C7.58662 13.2364 7.39995 13.1805 7.23346 13.0843C7.06696 12.9881 6.92524 12.8544 6.8196 12.6937C6.71396 12.5331 6.64732 12.35 6.625 12.159L6.567 11.594L6.535 11.22L6.493 10.556L6.47 10.048L6.455 9.493L6.451 9.199L6.449 8.894C6.449 8.68733 6.451 8.48733 6.455 8.294L6.47 7.739L6.493 7.232L6.52 6.775L6.55 6.374L6.625 5.63C6.64719 5.43882 6.71376 5.25547 6.81939 5.09458C6.92502 4.93369 7.0668 4.79972 7.2334 4.70335C7.4 4.60698 7.58682 4.55089 7.77896 4.53954C7.97109 4.5282 8.16321 4.56191 8.34 4.638ZM8.951 7.139L8.515 6.921L8.486 7.408L8.464 7.959L8.451 8.569L8.449 8.894L8.451 9.219L8.464 9.828L8.474 10.111L8.5 10.631L8.515 10.866L8.949 10.648L9.436 10.392L9.971 10.098L10.255 9.936L10.806 9.61L11.3 9.304L11.736 9.024L11.932 8.894L11.525 8.624L11.059 8.33C10.7938 8.16584 10.5261 8.00582 10.256 7.85L9.973 7.689L9.439 7.395L8.951 7.139Z" fill="black"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M18 1.00268e-07C18.5046 -0.000159579 18.9906 0.190406 19.3605 0.533497C19.7305 0.876588 19.9572 1.34684 19.995 1.85L20 2V16C20.0002 16.5046 19.8096 16.9906 19.4665 17.3605C19.1234 17.7305 18.6532 17.9572 18.15 17.995L18 18H2C1.49542 18.0002 1.00943 17.8096 0.639452 17.4665C0.269471 17.1234 0.0428434 16.6532 0.00500021 16.15L1.00268e-07 16V2C-0.000159579 1.49542 0.190406 1.00943 0.533497 0.639452C0.876588 0.269471 1.34684 0.0428434 1.85 0.00500021L2 1.00268e-07H18ZM18 2H2V16H18V2ZM8.34 4.638L8.858 4.868L9.196 5.028L9.583 5.218L10.013 5.436L10.483 5.686L10.99 5.966L11.256 6.118L11.774 6.423L12.248 6.715L12.678 6.988L13.058 7.241L13.538 7.571L13.902 7.834L13.997 7.904C14.1513 8.01883 14.2767 8.16816 14.363 8.34005C14.4494 8.51194 14.4943 8.70164 14.4943 8.894C14.4943 9.08636 14.4494 9.27606 14.363 9.44795C14.2767 9.61984 14.1513 9.76917 13.997 9.884L13.674 10.119L13.234 10.427L12.878 10.666L12.473 10.929L12.02 11.212L11.521 11.512L10.987 11.821L10.478 12.103L10.007 12.353L9.577 12.573L9.191 12.761L8.569 13.049L8.339 13.149C8.16242 13.2251 7.97051 13.2589 7.77856 13.2476C7.58662 13.2364 7.39995 13.1805 7.23346 13.0843C7.06696 12.9881 6.92524 12.8544 6.8196 12.6937C6.71396 12.5331 6.64732 12.35 6.625 12.159L6.567 11.594L6.535 11.22L6.493 10.556L6.47 10.048L6.455 9.493L6.451 9.199L6.449 8.894C6.449 8.68733 6.451 8.48733 6.455 8.294L6.47 7.739L6.493 7.232L6.52 6.775L6.55 6.374L6.625 5.63C6.64719 5.43882 6.71376 5.25547 6.81939 5.09458C6.92502 4.93369 7.0668 4.79972 7.2334 4.70335C7.4 4.60698 7.58682 4.55089 7.77896 4.53954C7.97109 4.5282 8.16321 4.56191 8.34 4.638ZM8.951 7.139L8.515 6.921L8.486 7.408L8.464 7.959L8.451 8.569L8.449 8.894L8.451 9.219L8.464 9.828L8.474 10.111L8.5 10.631L8.515 10.866L8.949 10.648L9.436 10.392L9.971 10.098L10.255 9.936L10.806 9.61L11.3 9.304L11.736 9.024L11.932 8.894L11.525 8.624L11.059 8.33C10.7938 8.16584 10.5261 8.00582 10.256 7.85L9.973 7.689L9.439 7.395L8.951 7.139Z" fill="black"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import {Button} from "antd";
|
||||
import {ArrowUpOutlined} from "@ant-design/icons";
|
||||
|
||||
|
||||
@ -11,7 +10,6 @@ export default function ButtonToTop(props: ButtonToTopProps) {
|
||||
return (
|
||||
<div className={'page-action-to-top'}>
|
||||
{props.visible && <button className="btn-to-top text-white" onClick={()=>{
|
||||
console.log(props)
|
||||
if(props.onClick){
|
||||
props.onClick()
|
||||
}else if(props.container){
|
||||
|
@ -1,7 +1,6 @@
|
||||
import {Button, Form, Input, Select, Space} from "antd";
|
||||
import {Input} from "antd";
|
||||
import {useSetState} from "ahooks";
|
||||
import {PlayCircleOutlined, SearchOutlined} from "@ant-design/icons";
|
||||
import {SearchListTimes} from "@/pages/news/components/news-source.ts";
|
||||
import {SearchOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import TimeSelect from "@/components/form/time-select.tsx";
|
||||
|
||||
@ -11,13 +10,13 @@ type Props = {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export default function SearchForm({onSearch, onBtnStartClick, loading}: Props) {
|
||||
export default function SearchForm({onSearch}: Props) {
|
||||
const [state, setState] = useSetState<{
|
||||
pushing?: boolean;
|
||||
time_flag: number;
|
||||
title?: string;
|
||||
}>({
|
||||
time_flag:0
|
||||
time_flag: 0
|
||||
})
|
||||
|
||||
const onFinish = (params: Partial<VideoSearchParams>) => {
|
||||
@ -31,36 +30,28 @@ export default function SearchForm({onSearch, onBtnStartClick, loading}: Props)
|
||||
const handleTimeFilter = (time_flag: number) => {
|
||||
setState({time_flag})
|
||||
onFinish({
|
||||
title:state.title,time_flag
|
||||
title: state.title, time_flag
|
||||
})
|
||||
}
|
||||
|
||||
return (<div className={'search-panel pt-6 pb-2'}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="search-form">
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
className="w-[270px] rounded-3xl"
|
||||
prefix={<SearchOutlined/>}
|
||||
onChange={e=>setState({title: e.target.value})}
|
||||
onPressEnter={()=>onFinish(state)}
|
||||
onBlur={()=>onFinish(state)}
|
||||
allowClear
|
||||
placeholder={'请输入视频标题关键字进行信息'}
|
||||
/>
|
||||
<TimeSelect
|
||||
className="w-[120px] ml-1"
|
||||
value={state.time_flag}
|
||||
onChange={handleTimeFilter}
|
||||
/>
|
||||
</div>
|
||||
<div className="search-form">
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
className="w-[270px] rounded-3xl"
|
||||
prefix={<SearchOutlined/>}
|
||||
onChange={e => setState({title: e.target.value})}
|
||||
onPressEnter={() => onFinish(state)}
|
||||
onBlur={() => onFinish(state)}
|
||||
allowClear
|
||||
placeholder={'请输入视频标题关键字进行信息'}
|
||||
/>
|
||||
<TimeSelect
|
||||
className="w-[120px] ml-1"
|
||||
value={state.time_flag}
|
||||
onChange={handleTimeFilter}
|
||||
/>
|
||||
</div>
|
||||
<Space size={10}>
|
||||
<Button
|
||||
loading={state.pushing} type={'primary'}
|
||||
onClick={onBtnStartClick} icon={<PlayCircleOutlined/>}
|
||||
>一键推流</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
30
src/pages/library/components/style.module.scss
Normal file
30
src/pages/library/components/style.module.scss
Normal file
@ -0,0 +1,30 @@
|
||||
.videoItem {
|
||||
border: solid 3px transparent;
|
||||
|
||||
:global {
|
||||
.video-bottom {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.videoChecked {
|
||||
@apply border-blue-500;
|
||||
}
|
||||
|
||||
.playIcon {
|
||||
--size: 40px;
|
||||
@apply bg-black/70 flex items-center justify-center;
|
||||
border: solid 2px rgba(255, 255, 255, 0.5);
|
||||
border-radius: var(--size);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
&:hover{
|
||||
@apply bg-blue-500;
|
||||
}
|
||||
svg{
|
||||
font-size: 24px;
|
||||
transform: translate(2px);
|
||||
}
|
||||
}
|
@ -1,22 +1,17 @@
|
||||
import {Button, Input, Modal} from "antd";
|
||||
import {Modal} from "antd";
|
||||
import {saveAs} from "file-saver";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useSetState} from "ahooks";
|
||||
import {Player} from "@/components/video/player.tsx";
|
||||
|
||||
import ArticleGroup from "@/components/article/group";
|
||||
import * as article from "@/service/api/article.ts";
|
||||
import {push2room} from "@/service/api/video.ts";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {formatTime, timeFromNow} from "@/util/strings.ts";
|
||||
|
||||
type Props = {
|
||||
video?: VideoInfo;
|
||||
autoPlay?: boolean;
|
||||
onClose?: () => void
|
||||
}
|
||||
export default function VideoDetail({video, onClose}: Props) {
|
||||
const [groups, setGroups] = useState<BlockContent[][]>([]);
|
||||
|
||||
export default function VideoDetail({video, onClose,autoPlay}: Props) {
|
||||
const [state, setState] = useSetState({
|
||||
exporting: false,
|
||||
pushing: false,
|
||||
@ -41,42 +36,26 @@ export default function VideoDetail({video, onClose}: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (video) {
|
||||
if (video.id > 0) {
|
||||
article.getById(video.id).then(res => {
|
||||
setGroups(res.content_group)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [video])
|
||||
|
||||
return (<>
|
||||
<Modal open={!!video} title={null} width={1500} footer={null} onCancel={onClose}>
|
||||
<div className="header text-2xl" style={{marginTop:-10}}>{video?.title || "新闻视频详情"}</div>
|
||||
<div className="flex gap-2 my-5">
|
||||
<div className="news-video w-[350px]">
|
||||
<div className="video-container bg-gray-100 rounded overflow-hidden h-[560px]">
|
||||
<Player autoPlay={false} url={video?.oss_video_url} poster={video?.cover} showControls={true}
|
||||
className="w-[360px] h-[560px] bg-white"/>
|
||||
</div>
|
||||
<div className="video-info text-right text-sm text-gray-600 mt-3">
|
||||
<span>创建时间: {timeFromNow(video?.ctime)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail flex-1 ml-5">
|
||||
<div className="aricle-body mt-3">
|
||||
<ArticleGroup groups={groups}/>
|
||||
<Modal
|
||||
open={!!video} width={390} closeIcon={null} title={null} footer={null} onCancel={onClose}
|
||||
rootClassName={"article-edit-modal"}
|
||||
>
|
||||
<div className="flex gap-2 px-6 pt-6">
|
||||
<div className="news-video w-[340px]">
|
||||
<div className="video-container bg-gray-100 rounded overflow-hidden">
|
||||
<Player autoPlay={autoPlay} url={video?.oss_video_url} poster={video?.cover} showControls={true}
|
||||
className="w-[340px] h-[600px] bg-white"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer flex justify-between">
|
||||
<div className="action flex gap-2">
|
||||
<Button loading={state.pushing} type="primary" onClick={pushToRoom}>一键推流</Button>
|
||||
<Button onClick={downloadVideo}>下载视频</Button>
|
||||
</div>
|
||||
<div className="close">
|
||||
<Button onClick={onClose}>关闭</Button>
|
||||
<div className="flex justify-end modal-control-footer">
|
||||
<div className="flex gap-4">
|
||||
<button disabled={state.pushing} className="text-gray-400 hover:text-gray-800 " type="text" onClick={pushToRoom}>一键推流</button>
|
||||
<button disabled={state.exporting} className="text-gray-400 hover:text-gray-800 " onClick={downloadVideo}
|
||||
type="text">下载视频
|
||||
</button>
|
||||
<button onClick={onClose} type="text" className="text-gray-800 hover:text-blue-500">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -1,46 +1,41 @@
|
||||
import {Checkbox, Tag} from "antd";
|
||||
import {IconDelete} from "@/components/icons";
|
||||
import {useState} from "react";
|
||||
import clsx from "clsx";
|
||||
import {CaretRightOutlined} from "@ant-design/icons"
|
||||
import {timeFromNow} from "@/util/strings.ts";
|
||||
|
||||
import {formatDuration, timeFromNow} from "@/util/strings.ts";
|
||||
import styles from './style.module.scss'
|
||||
|
||||
type VideoItemProps = {
|
||||
videoInfo: VideoInfo;
|
||||
onLive?: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: (autoPlay:boolean) => void;
|
||||
onRemove?: () => void;
|
||||
onCheckedChange?: (checked:boolean) => void;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
checked?: boolean;
|
||||
}
|
||||
export default function VideoItem(props: VideoItemProps) {
|
||||
const [state, setState] = useState({
|
||||
checked: false
|
||||
})
|
||||
const handleCheckedChange = (checked:boolean) => {
|
||||
setState({checked})
|
||||
if (props.onCheckedChange) {
|
||||
props.onCheckedChange(checked)
|
||||
}
|
||||
}
|
||||
|
||||
return <div className={'video-item bg-white rounded overflow-hidden relative group'}>
|
||||
<div className={`controls absolute top-1 right-1 z-[2] p-1 rounded items-center gap-2 bg-white/80 ${state.checked?'flex':'hidden'} group-hover:flex`}>
|
||||
<span onClick={props.onRemove} className={'cursor-pointer text-blue-500 text-2xl cursor-pointer'}><IconDelete /></span>
|
||||
{!props.onLive && <Checkbox onChange={e=>handleCheckedChange(e.target.checked)} />}
|
||||
export default function VideoItem(props: VideoItemProps) {
|
||||
|
||||
return <div
|
||||
className={clsx(styles.videoItem, `rounded-lg h-[240px] overflow-hidden relative group ${props.checked ? styles.videoChecked : ''}`)}>
|
||||
<div className={`controls absolute top-1 right-1 z-[2] rounded items-center gap-2`}>
|
||||
{/*<span onClick={props.onRemove} className={'cursor-pointer text-blue-500 text-2xl cursor-pointer'}><IconDelete /></span>*/}
|
||||
<div className={clsx("checkbox", {checked: props.checked})}
|
||||
onClick={() => props.onCheckedChange?.(!props.checked)}></div>
|
||||
</div>
|
||||
<div className="cover" onClick={props.onClick}>
|
||||
<img className={'w-full cursor-pointer h-[180px] object-cover'} src={props.videoInfo.cover}/>
|
||||
</div>
|
||||
<div className="text-sm py-2 px-3">
|
||||
<div className="title my-1 cursor-pointer line-clamp-1" onClick={props.onClick}>{props.videoInfo.title}</div>
|
||||
<div className="info flex justify-between gap-2 text-sm">
|
||||
<div className="video-time-info text-gray-500">
|
||||
<span>时长: {formatDuration(Math.ceil(props.videoInfo.duration / 1000))}</span>
|
||||
<span className="ml-1">{timeFromNow(props.videoInfo.publish_time)}</span>
|
||||
</div>
|
||||
{props.videoInfo.status == 3 && <div className="live-info">
|
||||
<Tag color="processing" className="mr-0">已在直播间</Tag>
|
||||
</div>}
|
||||
<div className="cover">
|
||||
<img className={'w-full cursor-pointer object-cover'} src={props.videoInfo.cover}/>
|
||||
<div className={'absolute inset-x-0 top-0 flex items-center justify-center bottom-[36px]'}>
|
||||
<div className={styles.playIcon} onClick={()=>props.onClick?.(true)}><CaretRightOutlined /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="video-bottom bg-black/30 backdrop-blur-[2px] text-sm absolute inset-x-0 bottom-0 text-white py-2 px-3 items-center flex justify-between">
|
||||
<div className="title cursor-pointer flex-1 text-nowrap overflow-hidden text-ellipsis min-w-0 mr-4"
|
||||
onClick={()=>props.onClick?.(false)}>{props.videoInfo.title}</div>
|
||||
<div className="video-time-info">{timeFromNow(props.videoInfo.ctime)}</div>
|
||||
</div>
|
||||
<div
|
||||
className={"absolute top-1 left-1 bg-black/50 rounded-3xl text-white px-3 py-0.5"}>{Math.ceil(props.videoInfo.duration / 1000)}s
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -1,26 +1,52 @@
|
||||
import {useState} from "react";
|
||||
import {Empty, Modal, Pagination} from "antd";
|
||||
import {useRequest} from "ahooks";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {Checkbox, Modal, Space} from "antd";
|
||||
import {useRequest, useSetState} from "ahooks";
|
||||
|
||||
import VideoItem from "@/pages/library/components/video-item.tsx";
|
||||
import SearchForm from "@/pages/library/components/search-form.tsx";
|
||||
import VideoDetail from "@/pages/library/components/video-detail.tsx";
|
||||
import {search} from "@/service/api/video.ts";
|
||||
import InfiniteScroller from "@/components/scoller/infinite-scroller.tsx";
|
||||
import {deleteHistories, push2room, search} from "@/service/api/video.ts";
|
||||
import {getList} from "@/service/api/live.ts";
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import ButtonBatch from "@/components/button-batch.tsx";
|
||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import {IconArrowRight, IconDelete} from "@/components/icons";
|
||||
|
||||
const DEFAULT_PAGE_LIMIT = {
|
||||
|
||||
page: 1,
|
||||
limit: 12
|
||||
}
|
||||
export default function LibraryIndex() {
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
const [params, setParams] = useState<VideoSearchParams>({
|
||||
time_flag: 0,
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 12
|
||||
pagination: {...DEFAULT_PAGE_LIMIT}
|
||||
})
|
||||
const [state, setState] = useSetState({
|
||||
checkedAll: false,
|
||||
loading: false,
|
||||
pushedCount: 0,
|
||||
showToTop: false
|
||||
})
|
||||
const [data, setData] = useState<DataList<VideoInfo>>()
|
||||
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
|
||||
|
||||
const {loading} = useRequest(() => search(params), {
|
||||
refreshDeps: [params],
|
||||
onSuccess: (data) => {
|
||||
setData(prev => {
|
||||
// 判断页码是否是第1页
|
||||
if (data.pagination.page == 1) return data;
|
||||
return {
|
||||
list: [...(prev?.list || []), ...(data?.list || [])],
|
||||
pagination: data.pagination
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const {data,loading} = useRequest(() => search(params), {
|
||||
refreshDeps: [params]
|
||||
})
|
||||
|
||||
const handleRemove = (video: VideoInfo) => {
|
||||
modal.confirm({
|
||||
title: '删除提示',
|
||||
@ -40,12 +66,34 @@ export default function LibraryIndex() {
|
||||
}
|
||||
})
|
||||
}
|
||||
const [detailVideo, setDetailVideo] = useState<VideoInfo>()
|
||||
const [detailVideo, setDetailVideo] = useState<{
|
||||
video: VideoInfo,
|
||||
autoPlay: boolean
|
||||
}>()
|
||||
const handleAllCheckedChange = (checked: boolean) => {
|
||||
setCheckedIdArray(checked ? data.list.map(v => v.id) : [])
|
||||
setState({
|
||||
checkedAll: !state.checkedAll
|
||||
})
|
||||
}
|
||||
const loadPushedState = () => {
|
||||
getList().then((ret) => {
|
||||
if (ret.list) {
|
||||
setState({pushedCount: ret.list.length})
|
||||
}
|
||||
})
|
||||
}
|
||||
const refresh = () => {
|
||||
loadPushedState();
|
||||
setParams(prev => ({...prev, pagination: {page: 1, limit: DEFAULT_PAGE_LIMIT.limit}, request_time: Date.now()}))
|
||||
}
|
||||
|
||||
useEffect(loadPushedState, [])
|
||||
|
||||
return (<>
|
||||
<div className={'container pb-5'}>
|
||||
{contextHolder}
|
||||
<div className="search-form-container mb-5">
|
||||
<div className="search-form-container">
|
||||
<SearchForm
|
||||
onSearch={setParams}
|
||||
onBtnStartClick={handleLive}
|
||||
@ -53,14 +101,41 @@ export default function LibraryIndex() {
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
<InfiniteScroller loading={loading} rootClassName="video-history-list-container" pagination={data?.pagination} onCallback={()=>{}}>
|
||||
<div className={'video-list-container grid gap-5 grid-cols-3 xl:grid-cols-4'}>
|
||||
<div className="live-control flex justify-between mb-2">
|
||||
<div className="pl-[70px]"></div>
|
||||
<div className="flex items-center">
|
||||
<Space className="text-gray-400">
|
||||
<span>总共 {data?.list.length || 0} 条</span>
|
||||
<span>已推送: {state.pushedCount} 条</span>
|
||||
<span className={'text-blue-500'}>已选: {checkedIdArray.length} 条</span>
|
||||
</Space>
|
||||
<button className="hover:text-blue-300 text-gray-400 ml-2"
|
||||
onClick={() => handleAllCheckedChange(checkedIdArray.length != data?.list.length)}>
|
||||
<span className="text-sm mr-2">全选</span>
|
||||
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
|
||||
</button>
|
||||
<Checkbox checked={checkedIdArray.length == data?.list.length}
|
||||
onChange={e => handleAllCheckedChange(e.target.checked)}/>
|
||||
</div>
|
||||
</div>
|
||||
<InfiniteScroller
|
||||
ref={scrollerRef} loading={loading} rootClassName="video-history-list-container"
|
||||
pagination={data?.pagination} onCallback={(page) => {
|
||||
setParams(prev => ({
|
||||
...prev,
|
||||
pagination: {page, limit: DEFAULT_PAGE_LIMIT.limit}
|
||||
}))
|
||||
}} onScroll={(top) => setState({showToTop: top > 30})}
|
||||
>
|
||||
<div className={'video-list-container grid gap-4 grid-cols-3 xl:grid-cols-4'}>
|
||||
{data?.list?.map((it, idx) => (
|
||||
<VideoItem
|
||||
onLive={idx == 2} key={it.id}
|
||||
onLive={idx == 2}
|
||||
key={idx}
|
||||
videoInfo={it}
|
||||
onRemove={() => handleRemove(it)}
|
||||
onClick={() => setDetailVideo(it)}
|
||||
onClick={(autoPlay) => setDetailVideo({video: it, autoPlay})}
|
||||
checked={checkedIdArray.includes(it.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
setCheckedIdArray(idArray => {
|
||||
return checked ? idArray.concat(it.id) : idArray.filter(id => id != it.id);
|
||||
@ -70,25 +145,29 @@ export default function LibraryIndex() {
|
||||
))}
|
||||
</div>
|
||||
</InfiniteScroller>
|
||||
{/*<div className="video-page-container flex justify-center mt-5">*/}
|
||||
{/* {data?.pagination && data?.pagination.total > 0 ? <div className="flex justify-center mt-10">*/}
|
||||
{/* <Pagination*/}
|
||||
{/* current={params.pagination.page}*/}
|
||||
{/* total={data?.pagination.total}*/}
|
||||
{/* pageSize={data?.pagination.limit}*/}
|
||||
{/* showSizeChanger={false}*/}
|
||||
{/* simple={true}*/}
|
||||
{/* rootClassName={'simple-pagination'}*/}
|
||||
{/* onChange={(page) => setParams(prev=>({...prev,pagination: {page, limit: 10}}))}*/}
|
||||
{/* />*/}
|
||||
{/* </div> : <div className="py-10">*/}
|
||||
{/* <Empty />*/}
|
||||
{/* </div>*/}
|
||||
{/* }*/}
|
||||
{/* /!*<Pagination defaultCurrent={1} total={50}/>*!/*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
{detailVideo && <VideoDetail video={detailVideo} onClose={() => setDetailVideo(undefined)}/>}
|
||||
{detailVideo && <VideoDetail video={detailVideo.video} autoPlay={detailVideo.autoPlay}
|
||||
onClose={() => setDetailVideo(undefined)}/>}
|
||||
|
||||
<div className="page-action">
|
||||
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
|
||||
{checkedIdArray?.length > 0 && <ButtonBatch
|
||||
selected={checkedIdArray}
|
||||
onSuccess={refresh}
|
||||
className='bg-gray-300 hover:bg-gray-400 text-white'
|
||||
icon={<IconDelete className=""/>}
|
||||
title={`你确定要删除选择的 ${checkedIdArray.length} 条视频吗?`}
|
||||
confirmMessage={'删除后需重新生成视频'}
|
||||
onProcess={deleteHistories}
|
||||
>批量删除</ButtonBatch>}
|
||||
{checkedIdArray?.length > 0 && <ButtonBatch
|
||||
selected={checkedIdArray}
|
||||
onSuccess={refresh}
|
||||
className='bg-[#4096ff] hover:bg-blue-600 text-white'
|
||||
icon={<IconArrowRight className={'text-white'}/>}
|
||||
onProcess={push2room}
|
||||
>一键推流</ButtonBatch>}
|
||||
</div>
|
||||
</>)
|
||||
}
|
@ -6,6 +6,15 @@ export function getList() {
|
||||
export function search(params:VideoSearchParams) {
|
||||
return post<DataList<VideoInfo>>('/video/search',params)
|
||||
}
|
||||
export function deleteHistories(ids: Id[]) {
|
||||
console.log('deleteHistories',ids)
|
||||
return new Promise<number>((resolve)=>{
|
||||
setTimeout(()=>{
|
||||
resolve(1)
|
||||
},2000)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频列表的文章编辑(需要重新生成视频)
|
||||
* @param title
|
||||
|
3
src/types/api.d.ts
vendored
3
src/types/api.d.ts
vendored
@ -2,7 +2,8 @@ declare interface ApiRequestPageParams {
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
};
|
||||
request_time?: number;
|
||||
}
|
||||
|
||||
declare interface ApiArticleSearchParams extends ApiRequestPageParams{
|
||||
|
Loading…
x
Reference in New Issue
Block a user