diff --git a/src/assets/core.scss b/src/assets/core.scss index 78ec3bc..9467ea5 100644 --- a/src/assets/core.scss +++ b/src/assets/core.scss @@ -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 { diff --git a/src/components/button-batch.tsx b/src/components/button-batch.tsx index 3e0a6b4..04c4d2c 100644 --- a/src/components/button-batch.tsx +++ b/src/components/button-batch.tsx @@ -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 + confirmMessage?: React.ReactNode, + icon?: React.ReactNode, + onProcess: (ids: Id[]) => Promise 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: , - content: confirmMessage, - onOk: onBatchProcess - }) + if(confirmMessage){ + modal.confirm({ + wrapClassName: 'root-modal-confirm', + title: title || '操作提示', + centered: true, + icon: , + content: confirmMessage, + onOk: onBatchProcess + }) + }else{ + onBatchProcess().catch(showErrorToast); + } } return ( - + ) } \ No newline at end of file diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx index ba6b990..ecaf57c 100644 --- a/src/components/icons/index.tsx +++ b/src/components/icons/index.tsx @@ -93,7 +93,7 @@ export const IconAddText = ({style, className}: IconProps) => ( export const IconVideo = ({style, className}: IconProps) => ( - + ) diff --git a/src/components/scoller/button-to-top.tsx b/src/components/scoller/button-to-top.tsx index 6353d62..b376000 100644 --- a/src/components/scoller/button-to-top.tsx +++ b/src/components/scoller/button-to-top.tsx @@ -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 (
{props.visible && -
) } \ No newline at end of file diff --git a/src/pages/library/components/style.module.scss b/src/pages/library/components/style.module.scss new file mode 100644 index 0000000..85d203b --- /dev/null +++ b/src/pages/library/components/style.module.scss @@ -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); + } +} \ No newline at end of file diff --git a/src/pages/library/components/video-detail.tsx b/src/pages/library/components/video-detail.tsx index dd95026..c62ab83 100644 --- a/src/pages/library/components/video-detail.tsx +++ b/src/pages/library/components/video-detail.tsx @@ -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([]); - +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 (<> - -
{video?.title || "新闻视频详情"}
-
-
-
- -
-
- 创建时间: {timeFromNow(video?.ctime)} -
-
-
-
- + +
+
+
+
-
-
- - -
-
- +
+
+ + +
diff --git a/src/pages/library/components/video-item.tsx b/src/pages/library/components/video-item.tsx index 1dc0c06..ae67107 100644 --- a/src/pages/library/components/video-item.tsx +++ b/src/pages/library/components/video-item.tsx @@ -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
-
- - {!props.onLive && handleCheckedChange(e.target.checked)} />} +export default function VideoItem(props: VideoItemProps) { + + return
+
+ {/**/} +
props.onCheckedChange?.(!props.checked)}>
-
- -
-
-
{props.videoInfo.title}
-
-
- 时长: {formatDuration(Math.ceil(props.videoInfo.duration / 1000))} - {timeFromNow(props.videoInfo.publish_time)} -
- {props.videoInfo.status == 3 &&
- 已在直播间 -
} +
+ +
+
props.onClick?.(true)}>
+
+
props.onClick?.(false)}>{props.videoInfo.title}
+
{timeFromNow(props.videoInfo.ctime)}
+
+
{Math.ceil(props.videoInfo.duration / 1000)}s +
} \ No newline at end of file diff --git a/src/pages/library/index.tsx b/src/pages/library/index.tsx index 4a67d31..3b65e21 100644 --- a/src/pages/library/index.tsx +++ b/src/pages/library/index.tsx @@ -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([]) const [params, setParams] = useState({ 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>() + const scrollerRef = useRef(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() + 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 (<>
{contextHolder} -
+
- {}}> -
+
+
+
+ + 总共 {data?.list.length || 0} 条 + 已推送: {state.pushedCount} 条 + 已选: {checkedIdArray.length} 条 + + + handleAllCheckedChange(e.target.checked)}/> +
+
+ { + setParams(prev => ({ + ...prev, + pagination: {page, limit: DEFAULT_PAGE_LIMIT.limit} + })) + }} onScroll={(top) => setState({showToTop: top > 30})} + > +
{data?.list?.map((it, idx) => ( 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() { ))}
- {/*
*/} - {/* {data?.pagination && data?.pagination.total > 0 ?
*/} - {/* setParams(prev=>({...prev,pagination: {page, limit: 10}}))}*/} - {/* />*/} - {/*
:
*/} - {/* */} - {/*
*/} - {/* }*/} - {/* /!**!/*/} - {/*
*/}
- {detailVideo && setDetailVideo(undefined)}/>} + {detailVideo && setDetailVideo(undefined)}/>} + +
+ scrollerRef.current?.scrollToPosition(0)}/> + {checkedIdArray?.length > 0 && } + title={`你确定要删除选择的 ${checkedIdArray.length} 条视频吗?`} + confirmMessage={'删除后需重新生成视频'} + onProcess={deleteHistories} + >批量删除} + {checkedIdArray?.length > 0 && } + onProcess={push2room} + >一键推流} +
) } \ No newline at end of file diff --git a/src/service/api/video.ts b/src/service/api/video.ts index 6c7ad0c..694f09f 100644 --- a/src/service/api/video.ts +++ b/src/service/api/video.ts @@ -6,6 +6,15 @@ export function getList() { export function search(params:VideoSearchParams) { return post>('/video/search',params) } +export function deleteHistories(ids: Id[]) { + console.log('deleteHistories',ids) + return new Promise((resolve)=>{ + setTimeout(()=>{ + resolve(1) + },2000) + }) +} + /** * 视频列表的文章编辑(需要重新生成视频) * @param title diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 347fb0d..7a27b1f 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -2,7 +2,8 @@ declare interface ApiRequestPageParams { pagination: { page: number; limit: number; - } + }; + request_time?: number; } declare interface ApiArticleSearchParams extends ApiRequestPageParams{