Compare commits

...

2 Commits

Author SHA1 Message Date
58ace4514b feat: 新闻编辑 2024-12-16 19:14:39 +08:00
0592d97e39 feat: update 2024-12-16 16:54:06 +08:00
15 changed files with 316 additions and 168 deletions

View File

@ -24,6 +24,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"flv.js": "^1.6.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"qs": "^6.12.1", "qs": "^6.12.1",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -1,3 +1,5 @@
@use "./libs" as *;
:root { :root {
font-family: -apple-system, "PingFang SC", 'Microsoft YaHei', sans-serif; font-family: -apple-system, "PingFang SC", 'Microsoft YaHei', sans-serif;
line-height: 1.5; line-height: 1.5;
@ -25,32 +27,35 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #999; background: #ccc;
height: 10px; height: 10px;
border-radius: 5px; border-radius: 5px;
&:hover { &:hover {
background: #666; background: #999;
cursor: pointer; cursor: pointer;
} }
} }
.btn { @layer base {
@apply px-5 py-2 rounded-md bg-white border text-sm; .btn {
&:hover { @apply px-5 py-2 rounded-md bg-white border text-sm;
@apply bg-gray-100;
}
&.btn-primary {
@apply bg-blue-500 text-white border-blue-500;
&:hover { &:hover {
@apply bg-blue-600; @apply bg-gray-100;
}
&.btn-primary {
@apply bg-blue-500 text-white border-blue-500;
&:hover {
@apply bg-blue-600;
}
} }
} }
}
.card { .card {
@apply bg-white rounded-lg p-5 my-10; @apply bg-white rounded-lg p-5 my-10;
}
} }
.radio-icon, .checkbox-icon { .radio-icon, .checkbox-icon {
@ -121,31 +126,41 @@
} }
} }
} }
.page-live{
.live-player{ .page-live {
.live-player {
max-height: calc(100vh - var(--app-header-header) - 130px); max-height: calc(100vh - var(--app-header-header) - 130px);
overflow: hidden; overflow: hidden;
iframe{
iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
} }
} }
.video-item-shadow { .video-item-shadow {
box-shadow: 0 0 6px 0 var(--tw-shadow-color); box-shadow: 0 0 6px 0 var(--tw-shadow-color);
//filter: drop-shadow(0 0 6px var(--tw-shadow-color)); //filter: drop-shadow(0 0 6px var(--tw-shadow-color));
} }
.video-list-sort-container{
.video-list-sort-container {
min-height: 300px; min-height: 300px;
max-height: calc(100vh - var(--app-header-header) - 300px); max-height: calc(100vh - var(--app-header-header) - 300px);
overflow: auto; overflow: auto;
padding-right: 10px; padding-right: 10px;
} }
.live-video-list-sort-container{ .live-video-list-sort-container {
min-height: 300px; min-height: 300px;
padding-right: 10px; padding-right: 10px;
max-height: calc(100vh - var(--app-header-header) - 200px); max-height: calc(100vh - var(--app-header-header) - 200px);
overflow: auto; overflow: auto;
} }
.app-main-navigation {
@include media-breakpoint-down(md) {
display: none;
}
}

22
src/assets/libs.scss Normal file
View File

@ -0,0 +1,22 @@
@mixin media-breakpoint-down($name) {
@if $name == sm {
@media (max-width: 767px) {
@content;
}
}
@if $name == md {
@media (max-width: 991px) {
@content;
}
}
@if $name == lg {
@media (max-width: 1199px) {
@content;
}
}
@if $name == xl {
@media (max-width: 1399px) {
@content;
}
}
}

View File

@ -80,7 +80,6 @@ export default function ArticleBlock(
<div> <div>
<div className={clsx(index == 0 ? '' : styles.blockItem, 'flex')}> <div className={clsx(index == 0 ? '' : styles.blockItem, 'flex')}>
<BlockText <BlockText
isFirstBlock={true}
onChange={(block) => handleBlockChange(0, block)} onChange={(block) => handleBlockChange(0, block)}
data={blocks[0]} data={blocks[0]}
isFirstBlock={index == 0} isFirstBlock={index == 0}
@ -98,7 +97,7 @@ export default function ArticleBlock(
{editable && <div className="ml-2 flex flex-col justify-between "> {editable && <div className="ml-2 flex flex-col justify-between ">
{ {
index > 0 ? <Popconfirm index > 0 ? <Popconfirm
title={<div style={{minWidth: 150}}><span>?</span></div>} title={<div style={{minWidth: 150}}><span>?</span></div>}
onConfirm={onRemove} onConfirm={onRemove}
okText="删除" okText="删除"
cancelText="取消" cancelText="取消"

View File

@ -11,19 +11,46 @@ type Props = {
errorMessage?: string; errorMessage?: string;
} }
function pushBlocksToGroup(blocks: BlockContent[],groups: BlockContent[][]){
const lastGroup = groups[groups.length - 1]
if (lastGroup && lastGroup.filter(s=>s.type == 'text') == 0) {
// 如果上一个group中没有文本则直接合并
lastGroup.push(...blocks)
} else {
groups.push(blocks)
}
}
function rebuildGroups(groups: BlockContent[][]) { function rebuildGroups(groups: BlockContent[][]) {
if (groups.length < 2) { const _groups: BlockContent[][] = [];
Array(2 - groups.length).fill([{type: 'text', content: ''}]).forEach((it) => { if (!groups || groups.length == 0) return _groups;
groups.push(it) groups.forEach((blocks,index) => {
if(!blocks) return;
if (blocks.length == 1) {
if(index == 0) _groups.push(blocks)
else pushBlocksToGroup(blocks,_groups)
} else {
if(index == 0){
_groups.push([blocks[0]])
_groups.push(blocks.slice(1))
}else{
pushBlocksToGroup(blocks,_groups)
}
}
});
if (_groups.length < 2) {
Array(2 - _groups.length).fill([{type: 'text', content: ''}]).forEach((it) => {
_groups.push(it)
}) })
} }
console.log('rebuildGroups', _groups)
return groups; return _groups;
} }
export default function ArticleGroup({groups: _groups, editable, onChange,errorMessage}: Props) { export default function ArticleGroup({groups: _groups, editable, onChange, errorMessage}: Props) {
const groups = rebuildGroups(_groups) const groups = rebuildGroups(_groups)
/** /**
* *

View File

@ -2,7 +2,7 @@ import {useSortable} from "@dnd-kit/sortable";
import {useSetState} from "ahooks"; import {useSetState} from "ahooks";
import React, {useEffect} from "react"; import React, {useEffect} from "react";
import {clsx} from "clsx"; import {clsx} from "clsx";
import {Popconfirm} from "antd"; import {Image, Popconfirm} from "antd";
import {CheckCircleFilled, MenuOutlined, MinusCircleFilled} from "@ant-design/icons"; import {CheckCircleFilled, MenuOutlined, MinusCircleFilled} from "@ant-design/icons";
import ImageCover from '@/assets/images/cover.png' import ImageCover from '@/assets/images/cover.png'
@ -28,7 +28,7 @@ export const VideoListItem = (
// index, // index,
id, video, onPlay, onRemove, checked, id, video, onPlay, onRemove, checked,
onCheckedChange, onEdit, active, editable, onCheckedChange, onEdit, active, editable,
className, className, sortable
}: Props) => { }: Props) => {
const { const {
attributes, listeners, attributes, listeners,
@ -45,23 +45,23 @@ export const VideoListItem = (
className={`video-item flex items-center gap-3 ${className}`} className={`video-item flex items-center gap-3 ${className}`}
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}> ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}>
{/*{index && index > 0 && <div className="flex items-center px-2">*/} {/*{index && index > 0 && <div className="flex items-center px-2">*/}
{/* <div className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{index}</div>*/} {/* <div className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{index}</div>*/}
{/*</div>}*/} {/*</div>}*/}
<div <div
className={`video-item-info flex gap-2 flex-1 bg-gray-100 h-[80px] overflow-hidden rounded-lg p-3 shadow-blue-500 ${active ? 'video-item-shadow' : ''}`}> className={`video-item-info flex gap-2 flex-1 bg-gray-100 h-[80px] overflow-hidden rounded-lg p-3 shadow-blue-500 ${active ? 'video-item-shadow' : ''}`}>
<div className={'video-title leading-7 flex-1'}>{video.title || video.video_title}</div> <div className={'video-title leading-7 flex-1'}>{video.title || video.video_title}</div>
<div className={'video-item-cover'}> <div className={'video-item-cover bg-white rounded-md overflow-hidden'}>
<img className="w-[100px] rounded-md" src={video.cover_url || ImageCover} alt={video.video_title}/> <img className="w-[100px] h-[56px] object-cover" src={video.cover_url || video.cover || ImageCover} alt={video.video_title}/>
</div> </div>
</div> </div>
{editable && <div className="operation flex items-center ml-2 gap-3 text-lg text-gray-400">
<div className="operation flex items-center ml-2 gap-3 text-lg text-gray-400"> {sortable && (!active ? <button className="hover:text-blue-500 cursor-move" {...attributes} {...listeners}>
{!active ? <button className="hover:text-blue-500 cursor-move" {...attributes} {...listeners}> <MenuOutlined/>
<MenuOutlined/> </button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>)}
</button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>} {onPlay &&
{onPlay && <button className="hover:text-blue-500" onClick={onPlay} style={{fontSize: '1.3em'}}><IconPlay/>
<button className="hover:text-blue-500" onClick={onPlay} style={{fontSize: '1.3em'}}><IconPlay/> </button>}
</button>} {editable && <>
{onEdit && {onEdit &&
<button className="hover:text-blue-500" onClick={onEdit} style={{fontSize: '1.1em'}}><IconEdit/> <button className="hover:text-blue-500" onClick={onEdit} style={{fontSize: '1.1em'}}><IconEdit/>
</button>} </button>}
@ -73,15 +73,14 @@ export const VideoListItem = (
} }
}}><CheckCircleFilled className={clsx({'text-blue-500': state.checked})}/></button> }}><CheckCircleFilled className={clsx({'text-blue-500': state.checked})}/></button>
{onRemove && <Popconfirm {onRemove && <Popconfirm
title="提示" title={<div style={{minWidth: 150}}><span>?</span></div>}
description={<div style={{minWidth: 150}}><span>?</span></div>}
onConfirm={onRemove} onConfirm={onRemove}
okText="删除" okText="删除"
cancelText="取消" cancelText="取消"
> >
<button className="hover:text-blue-500"><MinusCircleFilled/></button> <button className="hover:text-blue-500"><MinusCircleFilled/></button>
</Popconfirm>} </Popconfirm>}
</div> </>}
} </div>
</div> </div>
} }

View File

@ -4,11 +4,12 @@ import {SortableContext, arrayMove} from '@dnd-kit/sortable';
import {DndContext} from "@dnd-kit/core"; import {DndContext} from "@dnd-kit/core";
import {VideoListItem} from "@/components/video/video-list-item.tsx"; import {VideoListItem} from "@/components/video/video-list-item.tsx";
import {getList, playState} from "@/service/api/live.ts"; import {deleteByIds, getList, modifyOrder, playState} from "@/service/api/live.ts";
import styles from './style.module.scss' import styles from './style.module.scss'
import {set} from "lodash"; import {set} from "lodash";
import {showToast} from "@/components/message.ts"; import {showErrorToast, showToast} from "@/components/message.ts";
import ButtonBatch from "@/components/button-batch.tsx";
export default function LiveIndex() { export default function LiveIndex() {
const [videoData, setVideoData] = useState<LiveVideoInfo[]>([]) const [videoData, setVideoData] = useState<LiveVideoInfo[]>([])
@ -38,6 +39,7 @@ export default function LiveIndex() {
}) })
} }
} }
const activeToNext = (index?: number) => { const activeToNext = (index?: number) => {
const endToFirst = index != undefined && index > -1 ? false : state.activeIndex >= videoData.length - 1 const endToFirst = index != undefined && index > -1 ? false : state.activeIndex >= videoData.length - 1
const activeIndex = index != undefined && index > -1 ? index : (endToFirst ? 0 : state.activeIndex + 1) const activeIndex = index != undefined && index > -1 ? index : (endToFirst ? 0 : state.activeIndex + 1)
@ -60,105 +62,51 @@ export default function LiveIndex() {
}) })
}); });
} }
const loadList = () => {
useEffect(() => {
getList().then(res => { getList().then(res => {
res.list = [ console.log('origin list', res.list.map(s => s.id))
{
id: 11,
video_id: 1,
video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要',
cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg',
video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4',
video_duration: 100,
status: 1,
order_no: '1'
},
{
id: 0,
video_id: 1,
video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要',
cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg',
video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4',
video_duration: 100,
status: 1,
order_no: '1'
},
{
id: 10,
video_id: 1,
video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要',
cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg',
video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4',
video_duration: 100,
status: 1,
order_no: '1'
},
{
id: 4,
video_id: 1,
video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要',
cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg',
video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4',
video_duration: 100,
status: 1,
order_no: '1'
},
{
id: 5,
video_id: 1,
video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要',
cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg',
video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4',
video_duration: 100,
status: 1,
order_no: '1'
},
{
id: 6,
video_id: 1,
video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要',
cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg',
video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4',
video_duration: 100,
status: 1,
order_no: '1'
},
{
id: 7,
video_id: 1,
video_title: '333专家分析丨叙土边境曼比季地理位置为何如此重要',
cover_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.jpg',
video_oss_url: 'https://gachafun.oss-cn-beijing.aliyuncs.com/2021/08/13/1628840269744.mp4',
video_duration: 100,
status: 1,
order_no: '1'
}
]
setVideoData(res.list || []) setVideoData(res.list || [])
initPlayingState(res.list || []) setCheckedIdArray([])
}) initPlayingState(res.list)
}, []) });
const processDeleteVideo = async (_idArray: number[]) => {
message.info('删除成功!!!' + _idArray.join(''));
} }
const handleDeleteBatch = () => {
modal.confirm({ useEffect(loadList, [])
title: '提示',
content: '是否要删除选择的视频?', const processDeleteVideo = async (ids: number[]) => {
onOk: () => processDeleteVideo(checkedIdArray) deleteByIds(ids).then(()=>{
}) showToast('删除成功!','success')
loadList()
}).catch(showErrorToast)
} }
const handleConfirm = () => { const handleConfirm = () => {
modal.confirm({ modal.confirm({
title: '提示', title: '提示',
content: '是否采纳全部编辑操作?', content: '是否采纳移动视频位置操作?',
onOk: () => { onOk: () => {
showToast('编辑成功!!!', 'info'); //showToast('编辑成功!!!', 'info');
modifyOrder(videoData.map(s => s.id)).then(() => {
setEditable(false)
loadList()
}).catch(() => {
showToast('调整视频顺序失败,请重试!')
})
// showToast('编辑成功!!!', 'info');
// console.log('origin list', videoData.map(s => s.id))
} }
}) })
} }
const handleCancelConfirm = () => {
modal.confirm({
title: '提示',
content: '是否取消移动视频位置操作?',
onOk: () => {
showToast('退出并清除移动视频位置操作!', 'info');
loadList()
setEditable(false)
},
})
}
return (<div className="container py-10 page-live"> return (<div className="container py-10 page-live">
{contextHolder} {contextHolder}
@ -177,14 +125,20 @@ export default function LiveIndex() {
{editable ? <> {editable ? <>
<div className="flex gap-2"> <div className="flex gap-2">
<Button type="primary" onClick={handleConfirm}></Button> <Button type="primary" onClick={handleConfirm}></Button>
<Button onClick={() => setEditable(false)}>退</Button> <Button onClick={handleCancelConfirm}>退</Button>
</div>
<div>
<span className="cursor-pointer" onClick={handleDeleteBatch}></span>
</div> </div>
</> : <div> </> : <div>
<Button type="primary" onClick={() => setEditable(true)}></Button> <Button type="primary" onClick={() => setEditable(true)}></Button>
</div>} </div>}
{!editable && <div>
<ButtonBatch
selected={checkedIdArray}
emptyMessage={`请选择要删除的新闻视频`}
confirmMessage={`是否删除当前的${checkedIdArray.length}个新闻视频?`}
onSuccess={loadList}
onProcess={processDeleteVideo}
></ButtonBatch>
</div>}
</div> </div>
<div className="live-video-list-sort-container"> <div className="live-video-list-sort-container">
<div className="flex"> <div className="flex">
@ -201,22 +155,22 @@ export default function LiveIndex() {
const {active, over} = e; const {active, over} = e;
if (over && active.id !== over.id) { if (over && active.id !== over.id) {
let oldIndex = -1, newIndex = -1; let oldIndex = -1, newIndex = -1;
const originArr = [...videoData] // const originArr = [...videoData]
setVideoData((items) => { setVideoData((items) => {
oldIndex = items.findIndex(s => s.id == active.id); oldIndex = items.findIndex(s => s.id == active.id);
newIndex = items.findIndex(s => s.id == over.id); newIndex = items.findIndex(s => s.id == over.id);
return arrayMove(items, oldIndex, newIndex); return arrayMove(items, oldIndex, newIndex);
}); });
modal.confirm({ // modal.confirm({
title: '提示', // title: '提示',
content: '是否要移动到指定位置', // content: '是否要移动到指定位置',
onCancel: () => { // onCancel: () => {
setVideoData(originArr); // setVideoData(originArr);
}, // },
onOk: () => { // onOk: () => {
setVideoData([...videoData]) // setVideoData([...videoData])
} // }
}) // })
} }
}}> }}>
<SortableContext items={videoData}> <SortableContext items={videoData}>
@ -225,16 +179,18 @@ export default function LiveIndex() {
video={v} video={v}
index={index + 1} index={index + 1}
id={v.id} id={v.id}
active={state.activeIndex == index}
key={index} key={index}
active={state.activeIndex == index}
className={`list-item-${index} mt-3 mb-2`} className={`list-item-${index} mt-3 mb-2`}
checked={checkedIdArray.includes(v.id)}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
setCheckedIdArray(idArray => { setCheckedIdArray(idArray => {
return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
}) })
}} }}
onRemove={() => processDeleteVideo([v.id])} onRemove={() => processDeleteVideo([v.id])}
editable={editable} editable={!editable}
sortable={editable}
/>))} />))}
</SortableContext> </SortableContext>
</DndContext> </DndContext>

View File

@ -1,13 +1,12 @@
import {useState} from "react"; import {useState} from "react";
import {Checkbox, Empty, Modal, Pagination, Space} from "antd"; import {Checkbox, Empty, Modal, Pagination, Space} from "antd";
import {useRequest, useSetState} from "ahooks"; import {useRequest} from "ahooks";
import {Card} from "@/components/card"; import {Card} from "@/components/card";
import {getList} from "@/service/api/article.ts";
import SearchPanel from "@/pages/news/components/search-panel.tsx"; import SearchPanel from "@/pages/news/components/search-panel.tsx";
import styles from './style.module.scss' import styles from './style.module.scss'
import {getById} from "@/service/api/news.ts"; import {getById,getList} from "@/service/api/news.ts";
import {showLoading} from "@/components/message.ts"; import {showLoading} from "@/components/message.ts";
import {formatTime} from "@/util/strings.ts"; import {formatTime} from "@/util/strings.ts";
import ButtonPushNews2Article from "@/pages/news/components/button-push-news2article.tsx"; import ButtonPushNews2Article from "@/pages/news/components/button-push-news2article.tsx";

90
src/pages/test.tsx Normal file
View File

@ -0,0 +1,90 @@
import {useRef, useState} from "react";
import {Button} from "antd";
import FlvJs from "flv.js";
const list = [
{
"id": 10,
"cover_url": "",
"video_id": 51,
"video_title": "以军称在加沙地带打死一名哈马斯高级官员",
"video_duration": 31910,
"video_oss_url": "https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185251497659736064.flv",
"status": 4,
"order_no": ""
},
{
"id": 8,
"cover_url": "",
"video_id": 43,
"video_title": "历时12天史上第三人 尹锡悦总统弹劾案获通过 一文梳理韩国政坛众生相",
"video_duration": 728840,
"video_oss_url": "https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185229869001351168.flv",
"status": 4,
"order_no": ""
},
{
"id": 9,
"cover_url": "",
"video_id": 44,
"video_title": "推动房地产市场止跌回稳,发力重点在哪里?",
"video_duration": 57500,
"video_oss_url": "https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185229857764810752.flv",
"status": 4,
"order_no": ""
},
{
"id": 11,
"cover_url": "",
"video_id": 52,
"video_title": "以军称在加沙地带打死一名哈马斯高级官员",
"video_duration": 37980,
"video_oss_url": "https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185251495390617600.flv",
"status": 4,
"order_no": ""
}
]
const cache:{
flvPlayer?: FlvJs.Player
} = {
}
export default function Test() {
const videoRef = useRef<HTMLVideoElement | null>(null)
const [index, setIndex] = useState(-1)
const load = (url: string) => {
if (FlvJs.isSupported()) {
if(cache.flvPlayer){
cache.flvPlayer.pause()
cache.flvPlayer.unload()
}
cache.flvPlayer = FlvJs.createPlayer({
type: 'flv',
url: url
})
cache.flvPlayer.attachMediaElement(videoRef.current!)
cache.flvPlayer.load()
cache.flvPlayer.play()
}
// const url = 'https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-14/1185229869001351168.flv'
// if (videoRef.current) {
// videoRef.current!.src = url
// videoRef.current?.play()
// }
}
const play = () => {
const next = index >= list.length - 1 ? 0 : index + 1
load(list[next].video_oss_url)
setIndex(next)
}
return (<div className="test container m-auto">
<div className="my-10">
<video controls className="border w-[400px] max-h-[600px]" ref={videoRef}></video>
</div>
<Button onClick={play}>load {index > -1 ? <span>
{index} {list[index].video_title}
</span>:''}</Button>
</div>)
}

View File

@ -10,11 +10,11 @@ import {VideoListItem} from "@/components/video/video-list-item.tsx";
import ArticleEditModal from "@/components/article/edit-modal.tsx"; import ArticleEditModal from "@/components/article/edit-modal.tsx";
import {deleteByIds, getList, modifyOrder, push2room} from "@/service/api/video.ts"; import {deleteByIds, getList, modifyOrder, push2room} from "@/service/api/video.ts";
import {formatDuration} from "@/util/strings.ts"; import {formatDuration} from "@/util/strings.ts";
import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx";
import ButtonBatch from "@/components/button-batch.tsx"; import ButtonBatch from "@/components/button-batch.tsx";
import {showErrorToast, showToast} from "@/components/message.ts"; import {showToast} from "@/components/message.ts";
import FlvJs from "flv.js";
const cache:{flvPlayer?: FlvJs.Player} = {}
export default function VideoIndex() { export default function VideoIndex() {
const [editId, setEditId] = useState(-1) const [editId, setEditId] = useState(-1)
const [videoData, setVideoData] = useState<VideoInfo[]>([]) const [videoData, setVideoData] = useState<VideoInfo[]>([])
@ -29,7 +29,6 @@ export default function VideoIndex() {
// 加载列表 // 加载列表
const loadList = () => { const loadList = () => {
getList().then((ret) => { getList().then((ret) => {
console.log('origin list', ret.list.map(s => s.id))
setCheckedIdArray([]) setCheckedIdArray([])
setVideoData(ret.list || []) setVideoData(ret.list || [])
setState({checkedAll: false, playingIndex: -1}) setState({checkedAll: false, playingIndex: -1})
@ -40,6 +39,21 @@ export default function VideoIndex() {
const playVideo = (video: VideoInfo, playingIndex: number) => { const playVideo = (video: VideoInfo, playingIndex: number) => {
if (videoRef.current && video.oss_video_url) { if (videoRef.current && video.oss_video_url) {
setState({playingIndex}) setState({playingIndex})
if (FlvJs.isSupported()) {
// 已经有播放实例 则销毁
if(cache.flvPlayer){
cache.flvPlayer.pause()
cache.flvPlayer.unload()
}
cache.flvPlayer = FlvJs.createPlayer({
type: 'flv',
url: video.oss_video_url
})
cache.flvPlayer.attachMediaElement(videoRef.current!)
cache.flvPlayer.load()
cache.flvPlayer.play()
}
videoRef.current!.src = video.oss_video_url videoRef.current!.src = video.oss_video_url
} }
} }
@ -51,7 +65,6 @@ export default function VideoIndex() {
}) })
} }
const handleModifySort = () => { const handleModifySort = () => {
setVideoData((items) => { setVideoData((items) => {
modifyOrder(items.map(s => s.id)).catch(() => { modifyOrder(items.map(s => s.id)).catch(() => {
showToast('调整视频顺序失败,请重试!') showToast('调整视频顺序失败,请重试!')
@ -81,7 +94,10 @@ export default function VideoIndex() {
selected={checkedIdArray} selected={checkedIdArray}
emptyMessage={`请选择要删除的新闻视频`} emptyMessage={`请选择要删除的新闻视频`}
confirmMessage={`是否删除当前的${checkedIdArray.length}个新闻视频?`} confirmMessage={`是否删除当前的${checkedIdArray.length}个新闻视频?`}
onSuccess={loadList} onSuccess={()=>{
showToast('删除成功!','success')
loadList()
}}
></ButtonBatch> ></ButtonBatch>
<button className="ml-5 hover:text-blue-300 text-gray-400 text-lg" <button className="ml-5 hover:text-blue-300 text-gray-400 text-lg"
@ -144,7 +160,8 @@ export default function VideoIndex() {
onEdit={() => { onEdit={() => {
setEditId(v.article_id) setEditId(v.article_id)
}} }}
editable editable={true}
sortable={true}
/>))} />))}
</SortableContext> </SortableContext>
</DndContext> </DndContext>

View File

@ -36,7 +36,7 @@ const NavItems = [
] ]
export function DashboardNavigation() { export function DashboardNavigation() {
return (<div className={'flex'}> return (<div className={'flex app-main-navigation'}>
{NavItems.map((it, idx) => ( {NavItems.map((it, idx) => (
<NavLink to={it.path} key={idx} className={clsx('nav-item cursor-pointer items-center')}> <NavLink to={it.path} key={idx} className={clsx('nav-item cursor-pointer items-center')}>
<span className="menu-text ml-1">{it.name}</span> <span className="menu-text ml-1">{it.name}</span>

View File

@ -1,6 +1,7 @@
import {RouteObject} from "react-router-dom"; import {RouteObject} from "react-router-dom";
import ErrorBoundary from "@/routes/error.tsx"; import ErrorBoundary from "@/routes/error.tsx";
import UserAuth from "@/pages/user"; import UserAuth from "@/pages/user";
import Test from "@/pages/test";
import CreateIndex from "../pages/video"; import CreateIndex from "../pages/video";
import LibraryIndex from "@/pages/library"; import LibraryIndex from "@/pages/library";
import LiveIndex from "@/pages/live"; import LiveIndex from "@/pages/live";
@ -13,6 +14,10 @@ const routes: RouteObject[] = [
path: '/user', path: '/user',
element: <UserAuth/>, element: <UserAuth/>,
}, },
{
path: '/test',
element: <Test/>,
},
{ {
path: '/', path: '/',
element: <DashboardLayout/>, element: <DashboardLayout/>,

View File

@ -12,9 +12,9 @@ export function getList() {
} }
export function modifyOrder(ids: Id[]) { export function modifyOrder(ids: Id[]) {
return post('/video/order', {ids}) return post('/room/order', {ids})
} }
export function deleteById(ids: Id[]) { export function deleteByIds(ids: Id[]) {
return post('/video/remove', {ids}) return post('/room/remove', {ids})
} }

View File

@ -1,7 +1,7 @@
import {post} from "@/service/request.ts"; import {post} from "@/service/request.ts";
export function getList(data: ApiArticleSearchParams & ApiRequestPageParams) { export function getList(data: ApiArticleSearchParams & ApiRequestPageParams) {
return post<DataList<ListCrawlerNewsItem>>({url: '/article/search', data}) return post<DataList<ListCrawlerNewsItem>>({url: '/spider/search', data})
} }
export function getById(id: Id) { export function getById(id: Id) {

View File

@ -1412,6 +1412,11 @@ es-errors@^1.3.0:
resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.npmmirror.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
esbuild@^0.21.3: esbuild@^0.21.3:
version "0.21.5" version "0.21.5"
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
@ -1625,6 +1630,14 @@ flatted@^3.2.9:
resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
flv.js@^1.6.2:
version "1.6.2"
resolved "https://registry.npmmirror.com/flv.js/-/flv.js-1.6.2.tgz#fa3340fe3f7ee01d3977f7876aee66b8436e5922"
integrity sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==
dependencies:
es6-promise "^4.2.8"
webworkify-webpack "^2.1.5"
follow-redirects@^1.15.6: follow-redirects@^1.15.6:
version "1.15.9" version "1.15.9"
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
@ -3163,6 +3176,11 @@ vite@^5.2.0:
optionalDependencies: optionalDependencies:
fsevents "~2.3.3" fsevents "~2.3.3"
webworkify-webpack@^2.1.5:
version "2.1.5"
resolved "https://registry.npmmirror.com/webworkify-webpack/-/webworkify-webpack-2.1.5.tgz#bf4336624c0626cbe85cf1ffde157f7aa90b1d1c"
integrity sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==
which@^2.0.1: which@^2.0.1:
version "2.0.2" version "2.0.2"
resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"