历史视频添加

This commit is contained in:
LittleBoy 2024-12-28 15:02:02 +08:00 committed by Coding
parent 1026c35c08
commit daba38f188
11 changed files with 277 additions and 160 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

@ -2,7 +2,8 @@ declare interface ApiRequestPageParams {
pagination: {
page: number;
limit: number;
}
};
request_time?: number;
}
declare interface ApiArticleSearchParams extends ApiRequestPageParams{