diff --git a/src/assets/core.scss b/src/assets/core.scss index d3c84ff..519b99f 100644 --- a/src/assets/core.scss +++ b/src/assets/core.scss @@ -131,7 +131,7 @@ .page-live { .live-player { - max-height: calc(100vh - var(--app-header-header) - 130px); + max-height: calc(100vh - var(--app-header-header) - 150px); overflow: hidden; iframe { @@ -148,17 +148,80 @@ } .video-list-sort-container { - min-height: 300px; - max-height: calc(100vh - var(--app-header-header) - 300px); - overflow: auto; - padding-right: 10px; + .list-header { + } + + .list-row { + @apply flex bg-white mt-2 py-2 px-4 rounded-xl gap-2 border; + border-width: 2px; + &.playing{ + @apply border-primary-blue bg-[#d9eaff]; + } + &.disabled{ + @apply border-primary-blue bg-[#f4f7fc]; + } + &.header-row{ + background: none; + .col{ + height: 42px; + font-size: 18px; + } + } + + &.checked { + @apply border-primary-blue bg-primary-blue-bg; + } + + .col { + @apply flex items-center relative pl-4 text-center justify-center; + height: 80px; + + &:after { + @apply absolute; + border-right: solid 1px #e8e8e8; + content: ' '; + top: 2px; + bottom: 2px; + left: 0; + } + } + + .number { + width: 50px; + padding-left: 10px; + &:after { + display: none; + } + } + + .cover { + width: 120px; + img{ + @apply rounded-lg; + } + } + + .title { + @apply flex-1 text-base; + } + + .generated-time { + width: 180px; + } + + .operation { + @apply flex items-center ml-2 gap-4 text-lg text-gray-400 justify-between; + width: 180px; + padding: 0 20px 0 30px; + } + } } .live-video-list-sort-container { - min-height: 300px; - padding-right: 10px; - max-height: calc(100vh - var(--app-header-header) - 200px); - overflow: auto; + //min-height: 300px; + //padding-right: 10px; + //max-height: calc(100vh - var(--app-header-header) - 200px); + //overflow: auto; } .video-player { @@ -185,7 +248,8 @@ font-size: 24px; } } -.list-scroller-container{ + +.list-scroller-container { overflow: auto; margin-right: -20px; padding-right: 16px; @@ -196,7 +260,6 @@ @apply list-scroller-container; height: calc(100vh - var(--app-header-header) - 200px); - .data-list-container-inner { } @@ -235,34 +298,41 @@ .ant-modal-body { padding: 10px 0; } - .ant-modal-confirm-content{ + + .ant-modal-confirm-content { color: #999; } - .ant-modal-confirm-btns{ + + .ant-modal-confirm-btns { margin-top: 40px; } } } -.article-edit-modal{ + +.article-edit-modal { .ant-modal { .ant-modal-content { @apply bg-white p-0; - .ant-modal-body{ + .ant-modal-body { @apply p-0; } } } - .article-title{ + + .article-title { @apply px-6 pt-10 pb-6; } - .article-body{ + + .article-body { @apply p-6 } - .modal-control-footer{ + + .modal-control-footer { @apply p-6 } - .input-box{ + + .input-box { // focus-within:shadow @apply bg-[#f8f8f8] border border-transparent w-full px-4 py-2 focus-within:bg-[#f0f0f0] focus-within:border-gray-300; border-radius: 8px; @@ -288,6 +358,7 @@ } } +// 时间选择 .timer-select-container { .timer-select-value { @apply text-blue-500 px-4 cursor-pointer h-[31px]; @@ -306,6 +377,7 @@ } } +//来源选择 .tag-select-container { .select-value { @apply text-blue-500 px-4 cursor-pointer h-[31px]; diff --git a/src/components/button-batch.tsx b/src/components/button-batch.tsx index d81ab21..1df5ab6 100644 --- a/src/components/button-batch.tsx +++ b/src/components/button-batch.tsx @@ -1,5 +1,5 @@ import React, {useState} from "react"; -import {Button, Modal} from "antd"; +import {Modal} from "antd"; import {ButtonType} from "antd/es/button"; import {showErrorToast, showToast} from "@/components/message.ts"; import {BizError} from "@/service/types.ts"; @@ -12,7 +12,9 @@ type Props = { onProcess: (ids: Id[]) => Promise successMessage?: string; onSuccess?: () => void; - children?: React.ReactNode + children?: React.ReactNode; + title?: React.ReactNode; + className?:string; } /** @@ -21,7 +23,7 @@ type Props = { export default function ButtonBatch( { selected, emptyMessage, successMessage, children, - type, confirmMessage, onProcess,onSuccess + title, confirmMessage, onProcess,onSuccess,className }: Props) { const [loading, setLoading] = useState(false) const onBatchProcess = async () => { @@ -44,7 +46,7 @@ export default function ButtonBatch( return; } Modal.confirm({ - title: '操作提示', + title: title || '操作提示', centered: true, content: confirmMessage, onOk: onBatchProcess @@ -52,6 +54,6 @@ export default function ButtonBatch( } return ( - + ) } \ No newline at end of file diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx index 1c03959..dbdeb44 100644 --- a/src/components/icons/index.tsx +++ b/src/components/icons/index.tsx @@ -63,7 +63,9 @@ export const IconPin = ({style, className}: IconProps) => ( - + ) @@ -72,7 +74,7 @@ export const IconDelete = ({style, className}: IconProps) => ( width="0.86em" height="1em" viewBox="0 0 20 23" version="1.1"> + fill="currentColor"/> ) @@ -82,8 +84,9 @@ export const IconAddText = ({style, className}: IconProps) => ( - + ) @@ -118,8 +121,32 @@ export const IconAdd = ({style, className}: IconProps) => ( fill="currentColor"/> ) +export const IconLocked = ({style, className}: IconProps) => ( + + + +) +export const IconUnlock = ({style, className}: IconProps) => ( + + + +) + +export const IconPlaying = ({style, className}: IconProps) => ( + + + +) export const IconPlay = ({style, className}: IconProps) => ( ( export const IconEdit = ({style, className}: IconProps) => ( - + ) \ No newline at end of file diff --git a/src/components/video/video-list-item.tsx b/src/components/video/video-list-item.tsx index c9712d8..4c56ea1 100644 --- a/src/components/video/video-list-item.tsx +++ b/src/components/video/video-list-item.tsx @@ -2,12 +2,13 @@ import {useSortable} from "@dnd-kit/sortable"; import {useSetState} from "ahooks"; import React, {useEffect} from "react"; import {clsx} from "clsx"; -import {Popconfirm} from "antd"; -import {CheckCircleFilled, MenuOutlined, MinusCircleFilled,LoadingOutlined} from "@ant-design/icons"; +import {Checkbox, Popconfirm} from "antd"; +import {CheckCircleFilled, MenuOutlined, MinusCircleFilled, LoadingOutlined} from "@ant-design/icons"; import ImageCover from '@/assets/images/cover.png' -import {IconEdit, IconPlay} from "@/components/icons"; +import {IconEdit, IconPlay, IconPlaying} from "@/components/icons"; import {VideoStatus} from "@/service/api/video.ts"; +import {formatTime} from "@/util/strings.ts"; type Props = { video: VideoInfo | LiveVideoInfo, @@ -19,17 +20,18 @@ type Props = { onCheckedChange?: (checked: boolean) => void; onPlay?: () => void; onEdit?: () => void; + onItemClick?: () => void; onRemove?: () => void; id: number; className?: string; - type?:'live'|'create' + type?: 'live' | 'create' } export const VideoListItem = ( { - id, video, onPlay, onRemove, checked, + id, video, onRemove, checked, onCheckedChange, onEdit, active, editable, - className, sortable,type + className, sortable, type, index,onItemClick }: Props) => { const { attributes, listeners, @@ -42,48 +44,71 @@ export const VideoListItem = ( setState({checked}) }, [checked]) - return
- {/*{index && index > 0 &&
*/} - {/*
{index}
*/} - {/*
}*/} -
-
{video.title || video.video_title}
-
- -
- {type == 'create' && video.status == VideoStatus.Generating &&
- 视频生成中 -
} -
-
- {sortable && (!active ? : )} - {onPlay &&} + const generating = (type == 'create' && video.status == VideoStatus.Generating ) - {editable && <> - {onEdit && - } - - {onRemove && 请确认删除此视频?
} - onConfirm={onRemove} - okText="删除" - cancelText="取消" - > - - } - } + {/* && active*/} + {!generating && active &&
+
+ +
播放中
+
+
} +
+ +
+
+ {video.title || video.video_title} +
+
+
{video.publish_time ? formatTime(video.publish_time) : ''}
+
+ {sortable && !generating && (!active ? + : )} + + {editable && !generating && <> + {onEdit && + } + { + if (onCheckedChange) { + onCheckedChange(!state.checked) + } else { + setState({checked: !state.checked}) + } + }} /> + + {onRemove && 请确认删除此视频?
} + onConfirm={onRemove} + okText="删除" + cancelText="取消" + > + + } + } + } \ No newline at end of file diff --git a/src/components/video/video-list.tsx b/src/components/video/video-list.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index 274edaf..fca71a9 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useMemo, useRef, useState} from "react"; -import {Button, Modal} from "antd"; +import {Checkbox, Modal} from "antd"; import {SortableContext, arrayMove} from '@dnd-kit/sortable'; import {DndContext} from "@dnd-kit/core"; @@ -12,20 +12,25 @@ 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"; const cache: { flvPlayer?: FlvJs.Player, timerPlayNext?: any, timerLoadState?: any, prevUrl?: string } = {} export default function LiveIndex() { - const videoRef = useRef(null) const player = useRef(null) const [videoData, setVideoData] = useState([]) const [modal, contextHolder] = Modal.useModal() const [checkedIdArray, setCheckedIdArray] = useState([]) const [editable, setEditable] = useState(false) + const scrollerRef = useRef(null) const [state, setState] = useSetState({ activeIndex: -1, muted: true, + showToTop: false, + checkedAll: false, }) const activeIndex = useRef(state.activeIndex) useEffect(() => { @@ -123,6 +128,10 @@ export default function LiveIndex() { }).catch(showErrorToast) } const handleConfirm = () => { + if(!editable){ + setEditable(true) + return; + } modal.confirm({ title: '提示', content: '是否采纳移动视频位置操作?', @@ -134,20 +143,18 @@ export default function LiveIndex() { }).catch(() => { showToast('调整视频顺序失败,请重试!') }) - // showToast('编辑成功!!!', 'info'); - // console.log('origin list', videoData.map(s => s.id)) - } - }) - } - const handleCancelConfirm = () => { - modal.confirm({ - title: '提示', - content: '是否取消移动视频位置操作?', - onOk: () => { + }, + onCancel: () => { showToast('退出并清除移动视频位置操作!', 'info'); loadList() setEditable(false) - }, + } + }) + } + const handleAllCheckedChange = () => { + setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id)) + setState({ + checkedAll: !state.checkedAll }) } @@ -157,6 +164,12 @@ export default function LiveIndex() { return videoData.reduce((sum, v) => sum + Math.ceil(v.video_duration / 1000), 0); }, [videoData]) + const currentSelectedId = useMemo(()=>{ + if(state.activeIndex < 0 || state.activeIndex >= videoData.length) return []; + const currentId = videoData[state.activeIndex]; + return checkedIdArray.filter(id => currentId.id != id) + },[checkedIdArray,state.activeIndex]) + return (
{contextHolder}
@@ -172,37 +185,44 @@ export default function LiveIndex() { 视频时长: {formatDuration(totalDuration)}
-
-
-
- {editable ? <> -
- - -
- :
- -
} - {!editable &&
- 批量删除 -
} + +
+
+
+ 当前{state.activeIndex == -1?'暂未播放':`播放到${state.activeIndex}条`}, + 共{videoData.length}条
-
-
-
- {videoData.map((v, index) => ( -
-
{index + 1}
-
- ))} -
+ +
+
+ {editable?'已解锁':'锁定状态不可排序'} + + {editable ? : } + +
+
+ + handleAllCheckedChange()}/> +
+
+
+
+
+
No.
+
缩略图
+
标题
+
生成时间
+
+
+
+
+
+ setState({showToTop: top > 30})}>
{ const {active, over} = e; @@ -226,21 +246,37 @@ export default function LiveIndex() { className={`list-item-${index} mt-3 mb-2`} checked={checkedIdArray.includes(v.id)} onCheckedChange={(checked) => { - setCheckedIdArray(idArray => { - return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); - }) + 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} - sortable={editable} + editable={!editable && state.activeIndex != index} + sortable={editable && state.activeIndex != index} />))}
-
+
+
+ scrollerRef.current?.scrollToPosition(0)}/> + + 批量删除 + +
) } \ No newline at end of file diff --git a/src/pages/news/components/style.module.scss b/src/pages/news/components/style.module.scss index 7fd0a67..66f71f8 100644 --- a/src/pages/news/components/style.module.scss +++ b/src/pages/news/components/style.module.scss @@ -40,7 +40,7 @@ } .col{ @apply flex items-center relative pl-6; - height: 44px; + height: 54px; &:after{ @apply absolute; border-right: solid 1px #e8e8e8; @@ -68,6 +68,10 @@ } .header{ @apply bg-primary-bg; + .col{ + + height: 42px; + } .operations{ } } diff --git a/src/pages/news/edit.tsx b/src/pages/news/edit.tsx index 2a9f380..b593622 100644 --- a/src/pages/news/edit.tsx +++ b/src/pages/news/edit.tsx @@ -57,7 +57,7 @@ export default function NewEdit() { setSelectedRowKeys([]) } } - const scrollerRef = useRef(null) + const scrollerRef = useRef(null) return (
diff --git a/src/pages/video/components/button-push2room.tsx b/src/pages/video/components/button-push2room.tsx index d54fd8f..8e3efc1 100644 --- a/src/pages/video/components/button-push2room.tsx +++ b/src/pages/video/components/button-push2room.tsx @@ -2,6 +2,7 @@ import {Button, Modal} from "antd"; import React, {useState} from "react"; import {showErrorToast, showToast} from "@/components/message.ts"; import {push2room, VideoStatus} from "@/service/api/video.ts"; +import {IconArrowRight} from "@/components/icons"; export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];onSuccess?:()=>void; }) { @@ -33,6 +34,9 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on }) } return ( - + ) } \ No newline at end of file diff --git a/src/pages/video/index.tsx b/src/pages/video/index.tsx index 700de01..084ba6c 100644 --- a/src/pages/video/index.tsx +++ b/src/pages/video/index.tsx @@ -1,10 +1,8 @@ -import {Empty, Modal} from "antd"; +import {Checkbox, Empty, Modal} 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 {CheckCircleFilled} from "@ant-design/icons"; -import {clsx} from "clsx"; import {VideoListItem} from "@/components/video/video-list-item.tsx"; import ArticleEditModal from "@/components/article/edit-modal.tsx"; @@ -14,15 +12,20 @@ import ButtonBatch from "@/components/button-batch.tsx"; import {showToast} 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} from "@/components/icons"; export default function VideoIndex() { const [editId, setEditId] = useState(-1) const [videoData, setVideoData] = useState([]) const [modal, contextHolder] = Modal.useModal() - const player = useRef(null) + const player = useRef(null) + const scrollerRef = useRef(null) const [state, setState] = useSetState({ checkedAll: false, playingIndex: -1, + showToTop: false }) const [checkedIdArray, setCheckedIdArray] = useState([]) @@ -45,6 +48,8 @@ export default function VideoIndex() { // 播放视频 const playVideo = (video: VideoInfo, playingIndex: number) => { + console.log(video) + if(video.status == VideoStatus.Generating ) return; if (video.oss_video_url && video.status !== 1) { setState({playingIndex}) player.current?.play(video.oss_video_url, 0) @@ -76,110 +81,10 @@ export default function VideoIndex() { return (
{contextHolder}
-
-
-
- 视频时长: {formatDuration(totalDuration)} -
-
- { - showToast('删除成功!', 'success') - loadList() - }} - >批量删除 - - -
-
-
-
- {videoData.length == 0 ?
: <> -
- {videoData.map((v, index) => ( -
-
{index + 1}
-
- ))} -
-
- { - 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); - return arrayMove(items, oldIndex, newIndex); - }); - modal.confirm({ - title: '提示', - content: '是否要移动到指定位置', - onOk: handleModifySort, - onCancel: () => { - setVideoData(originArr); - } - }) - } - }}> - - {videoData.map((v, index) => ( - { - setCheckedIdArray(idArray => { - const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); - setState({checkedAll: newArr.length == videoData.length}) - return newArr; - }) - }} - onPlay={v.status == VideoStatus.Generating ? undefined :() => playVideo(v, index)} - onEdit={v.status == VideoStatus.Generating ? undefined : () => { - setEditId(v.article_id) - }} - editable={v.status != VideoStatus.Generating} - sortable={v.status != VideoStatus.Generating} - />))} - - -
- } -
-
-
- {/*一键推流*/} - -
-
-
-
预览视频
+
+
预览视频 - 点击视频列表播放
- {/**/} { @@ -188,6 +93,106 @@ export default function VideoIndex() { className="w-[360px] h-[640px] bg-white"/>
+
视频时长: {formatDuration(totalDuration)}
+
+
+
+
+
+ + handleAllCheckedChange()} /> +
+
+
+
+
+
No.
+
缩略图
+
标题
+
生成时间
+
+
+
+ setState({showToTop: top > 30})}> + { + videoData.length == 0 ?
: +
+ { + 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); + return arrayMove(items, oldIndex, newIndex); + }); + modal.confirm({ + title: '提示', + content: '是否要移动到指定位置', + onOk: handleModifySort, + onCancel: () => { + setVideoData(originArr); + } + }) + } + }}> + + {videoData.map((v, index) => ( + { + 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, index)} + onEdit={v.status == VideoStatus.Generating ? undefined : () => { + setEditId(v.article_id) + }} + editable={v.status != VideoStatus.Generating} + sortable={v.status != VideoStatus.Generating} + />))} + + +
+ } +
+
+
+
+ scrollerRef.current?.scrollToPosition(0)}/> + {/**/} + { + showToast('删除成功!', 'success') + loadList() + }} + > + 批量删除 + + +
setEditId(-1)}/> diff --git a/src/routes/layout/dashboard-layout.tsx b/src/routes/layout/dashboard-layout.tsx index ab216ee..88dd9f9 100644 --- a/src/routes/layout/dashboard-layout.tsx +++ b/src/routes/layout/dashboard-layout.tsx @@ -42,7 +42,7 @@ const NavigationUserContainer = () => {
) return (
- {user ? + {user ?
: }
) diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 9723974..933afcc 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -107,6 +107,7 @@ declare interface LiveVideoInfo { video_oss_url: string; status: number; order_no: string; + publish_time?: number|string; } declare interface LiveState{