feat: 添加批量操作按钮;优化视频展示;
This commit is contained in:
parent
b07f336bd5
commit
be22fc387a
@ -19,6 +19,22 @@
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #999;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
background: #666;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply px-5 py-2 rounded-md bg-white border text-sm;
|
||||
&:hover {
|
||||
@ -124,6 +140,7 @@
|
||||
min-height: 300px;
|
||||
max-height: calc(100vh - var(--app-header-header) - 300px);
|
||||
overflow: auto;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.live-video-list-sort-container{
|
||||
|
@ -11,20 +11,24 @@ type Props = {
|
||||
onClose?: (saved?: boolean) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
loading: false,
|
||||
open: false,
|
||||
msgTitle: '',
|
||||
msgGroup: '',
|
||||
error:''
|
||||
}
|
||||
export default function ArticleEditModal(props: Props) {
|
||||
|
||||
const [groups, setGroups] = useState<BlockContent[][]>([]);
|
||||
const [title, setTitle] = useState('')
|
||||
|
||||
const [state, setState] = useSetState({
|
||||
loading: false,
|
||||
open: false,
|
||||
msgTitle: '',
|
||||
msgGroup: '',
|
||||
...DEFAULT_STATE
|
||||
})
|
||||
// 保存数据
|
||||
const handleSave = () => {
|
||||
console.log(groups, title)
|
||||
setState({error: ''})
|
||||
if (!title) {
|
||||
// setState({msgTitle: '请输入标题内容'});
|
||||
return;
|
||||
@ -37,39 +41,24 @@ export default function ArticleEditModal(props: Props) {
|
||||
setState({loading: true})
|
||||
save(title, groups, props.id > 0 ? props.id : undefined).then(() => {
|
||||
props.onClose?.(true)
|
||||
}).catch(e=>{
|
||||
setState({error: e.data || '保存失败,请重试!'})
|
||||
}).finally(() => {
|
||||
setState({loading: false})
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
setState({...DEFAULT_STATE})
|
||||
if (props.id) {
|
||||
if (props.id > 0) {
|
||||
if (props.type == 'news') {
|
||||
article.getById(props.id).then(res => {
|
||||
setGroups(res.content_group)
|
||||
setTitle(res.title)
|
||||
})
|
||||
}
|
||||
article.getById(props.id).then(res => {
|
||||
setGroups(res.content_group)
|
||||
setTitle(res.title)
|
||||
})
|
||||
} else {
|
||||
// 新增
|
||||
setGroups([
|
||||
[{
|
||||
type: 'text',
|
||||
content: '韩国国会当地时间14日16时举行全体会议,就在野党阵营第二次提出的尹锡悦总统弹劾案进行表决。根据投票结果,有204票赞成,85票反对,3票弃权,8票无效,弹劾案最终获得通过,尹锡悦的总统职务立即停止。'
|
||||
}],
|
||||
[
|
||||
{
|
||||
type: 'text',
|
||||
content: '韩国宪法法院将在180天内完成弹劾案审判程序。如果宪法法院作出弹劾案不成立的裁决,尹锡悦将立即恢复总统职务;如果宪法法院认可弹劾案成立,尹锡悦将立即被罢免,预计韩国将在明年4月至6月间举行大选。'
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
content: 'https://zverse-on.oss-cn-shanghai.aliyuncs.com/metahuman/workbench/20241214/193c442df75.jpeg'
|
||||
},
|
||||
|
||||
],
|
||||
])
|
||||
setTitle('韩国国会通过总统弹劾案 尹锡悦职务立即停止')
|
||||
setGroups([])
|
||||
setTitle('')
|
||||
}
|
||||
}
|
||||
}, [props.id])
|
||||
@ -83,6 +72,7 @@ export default function ArticleEditModal(props: Props) {
|
||||
onCancel={()=>props.onClose?.()}
|
||||
okButtonProps={{loading: state.loading}}
|
||||
onOk={handleSave}
|
||||
okText={props.type == 'news' ? '确定' : '重新生成'}
|
||||
>
|
||||
<div className="article-title mt-5">
|
||||
<div className="title">
|
||||
@ -111,6 +101,7 @@ export default function ArticleEditModal(props: Props) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{state.error && <div className="text-red-500">{state.error}</div>}
|
||||
</div>
|
||||
</Modal>);
|
||||
}
|
56
src/components/button-batch.tsx
Normal file
56
src/components/button-batch.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React, {useState} from "react";
|
||||
import {Button, Modal} from "antd";
|
||||
import {ButtonType} from "antd/es/button";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
|
||||
type Props = {
|
||||
selected: any[],
|
||||
type?: ButtonType;
|
||||
emptyMessage: string,
|
||||
confirmMessage: React.ReactNode,
|
||||
onProcess: (ids: Id[]) => Promise<void>
|
||||
successMessage?: string;
|
||||
onSuccess?: () => void;
|
||||
children?: React.ReactNode
|
||||
|
||||
}
|
||||
/**
|
||||
* 统一批量操作按钮
|
||||
*/
|
||||
export default function ButtonBatch(
|
||||
{
|
||||
selected, emptyMessage, successMessage, children,
|
||||
type, confirmMessage, onProcess,onSuccess
|
||||
}: Props) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const onBatchProcess = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await onProcess(selected)
|
||||
if (successMessage) showToast(successMessage, 'success')
|
||||
if (onSuccess) {
|
||||
onSuccess()
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorToast(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
const handleBtnClick = () => {
|
||||
if (selected.length == 0) {
|
||||
showToast(emptyMessage, 'warning')
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '操作提示',
|
||||
centered: true,
|
||||
content: confirmMessage,
|
||||
onOk: onBatchProcess
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Button loading={loading} type={type} onClick={handleBtnClick}>{children}</Button>
|
||||
)
|
||||
}
|
@ -1,16 +1,18 @@
|
||||
import {message} from "antd";
|
||||
import {BizError} from "@/service/types.ts";
|
||||
|
||||
export function showToast(content: string, type?: 'success' | 'info' | 'warning' | 'error') {
|
||||
export function showToast(content: string, type?: 'success' | 'info' | 'warning' | 'error', duration?: number) {
|
||||
|
||||
message.open({
|
||||
type,
|
||||
content,
|
||||
duration,
|
||||
className: 'aui-toast'
|
||||
}).then();
|
||||
}
|
||||
export function showErrorToast(e:Error|BizError) {
|
||||
showToast(String(((e instanceof BizError)?e.data:'') || e.message),'error')
|
||||
|
||||
export function showErrorToast(e: Error | BizError) {
|
||||
showToast(String(((e instanceof BizError) ? e.data : '') || e.message), 'error')
|
||||
}
|
||||
|
||||
|
||||
@ -22,14 +24,14 @@ export function showLoading(content = 'Loading...') {
|
||||
content,
|
||||
}).then();
|
||||
return {
|
||||
update(content: string,type?: 'success' | 'info' | 'warning' | 'error'){
|
||||
update(content: string, type?: 'success' | 'info' | 'warning' | 'error') {
|
||||
message.open({
|
||||
key,
|
||||
content,
|
||||
type
|
||||
}).then();
|
||||
},
|
||||
close(){
|
||||
close() {
|
||||
message.destroy(key);
|
||||
}
|
||||
}
|
||||
|
@ -185,10 +185,6 @@ export default function LiveIndex() {
|
||||
</> : <div>
|
||||
<Button type="primary" onClick={() => setEditable(true)}>编辑</Button>
|
||||
</div>}
|
||||
<div className="flex gap-2">
|
||||
<Button type="primary" onClick={showFirst}>showFirst</Button>
|
||||
<Button type="primary" onClick={activeToNext}>Next</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="live-video-list-sort-container">
|
||||
<div className="flex">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Empty, message, Modal} from "antd";
|
||||
import {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";
|
||||
@ -8,17 +8,15 @@ import {clsx} from "clsx";
|
||||
|
||||
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
||||
import {getList} from "@/service/api/video.ts";
|
||||
import {deleteByIds, getList} from "@/service/api/video.ts";
|
||||
import {formatDuration} from "@/util/strings.ts";
|
||||
import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx";
|
||||
import ButtonBatch from "@/components/button-batch.tsx";
|
||||
|
||||
|
||||
export default function VideoIndex() {
|
||||
const [editId, setEditId] = useState(-1)
|
||||
|
||||
const [videoData, setVideoData] = useState<VideoInfo[]>([])
|
||||
|
||||
|
||||
const [modal, contextHolder] = Modal.useModal()
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||
const [state, setState] = useSetState({
|
||||
@ -26,45 +24,31 @@ export default function VideoIndex() {
|
||||
playingIndex: -1,
|
||||
})
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
const processDeleteVideo = async (_idArray: number[]) => {
|
||||
message.info('删除成功!!!' + _idArray.join(''));
|
||||
}
|
||||
useEffect(() => {
|
||||
|
||||
// 加载列表
|
||||
const loadList = () => {
|
||||
getList().then((ret) => {
|
||||
setVideoData(ret.list || [])
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleDeleteBatch = () => {
|
||||
modal.confirm({
|
||||
title: '提示',
|
||||
content: '是否要删除选择的视频?',
|
||||
onOk: () => processDeleteVideo(checkedIdArray)
|
||||
setState({checkedAll: false, playingIndex: -1})
|
||||
})
|
||||
}
|
||||
|
||||
// 播放视频
|
||||
const playVideo = (video: VideoInfo, playingIndex: number) => {
|
||||
setState({
|
||||
playingIndex
|
||||
})
|
||||
console.log('play', video)
|
||||
// if (videoRef.current) {
|
||||
// videoRef.current!.src = video.play_url
|
||||
// }
|
||||
setState({playingIndex})
|
||||
if (videoRef.current && video.oss_video_url) {
|
||||
videoRef.current!.src = video.oss_video_url
|
||||
}
|
||||
}
|
||||
// 处理全选
|
||||
const handleAllCheckedChange = () => {
|
||||
// setVideoData(list=>{
|
||||
// list.map(s=>{
|
||||
// s.checked = !state.checkedAll
|
||||
// })
|
||||
// return list
|
||||
// })
|
||||
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
|
||||
setState({
|
||||
checkedAll: !state.checkedAll
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
useEffect(loadList, [])
|
||||
const totalDuration = useMemo(() => {
|
||||
if (!videoData || videoData.length == 0) return 0;
|
||||
// 计算总时长
|
||||
@ -75,76 +59,86 @@ export default function VideoIndex() {
|
||||
{contextHolder}
|
||||
<div className="flex">
|
||||
<div className="video-list-container bg-white p-10 rounded flex-1">
|
||||
<div className="live-control flex justify-between mb-8">
|
||||
<div className="live-control flex justify-between mb-5">
|
||||
<div className="pl-[70px]">
|
||||
<span>视频时长: {formatDuration(totalDuration)}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
||||
<div className="flex gap-2 items-center pr-[10px]">
|
||||
<ButtonBatch
|
||||
onProcess={deleteByIds}
|
||||
selected={checkedIdArray}
|
||||
emptyMessage={`请选择要删除的新闻视频`}
|
||||
confirmMessage={`是否删除当前的${checkedIdArray.length}个新闻视频?`}
|
||||
onSuccess={loadList}
|
||||
>批量删除</ButtonBatch>
|
||||
|
||||
<button className="ml-5 hover:text-blue-300 text-gray-400 text-lg"
|
||||
onClick={handleAllCheckedChange}>
|
||||
<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex video-list-sort-container'}>
|
||||
{videoData.length == 0 ? <div className="m-auto"><Empty/></div> : <>
|
||||
<div className="sort-number-container mr-2">
|
||||
{videoData.map((v, index) => (
|
||||
<div key={index} className="flex items-center px-2 h-[80px] mb-5">
|
||||
<div
|
||||
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{index + 1}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="sort-list-container">
|
||||
<DndContext onDragEnd={(e) => {
|
||||
const {active, over} = e;
|
||||
if (over && active.id !== over.id) {
|
||||
let oldIndex = -1, newIndex = -1;
|
||||
const originArr = [...videoData]
|
||||
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: '是否要移动到指定位置',
|
||||
onCancel: () => {
|
||||
setVideoData(originArr);
|
||||
}
|
||||
})
|
||||
}
|
||||
}}>
|
||||
<SortableContext items={videoData}>
|
||||
{videoData.map((v, index) => (
|
||||
<VideoListItem
|
||||
video={v}
|
||||
index={index + 1}
|
||||
id={v.id}
|
||||
key={index}
|
||||
active={state.playingIndex == index}
|
||||
checked={checkedIdArray.includes(v.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
setCheckedIdArray(idArray => {
|
||||
const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
|
||||
setState({checkedAll: newArr.length == videoData.length})
|
||||
return newArr;
|
||||
})
|
||||
}}
|
||||
onPlay={() => playVideo(v, index)}
|
||||
onEdit={() => {
|
||||
setEditId(v.article_id)
|
||||
}}
|
||||
editable
|
||||
/>))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</>}
|
||||
<div className={'video-list-sort-container'}>
|
||||
<div className="flex my-2">
|
||||
{videoData.length == 0 ? <div className="m-auto"><Empty/></div> : <>
|
||||
<div className="sort-number-container mr-2">
|
||||
{videoData.map((v, index) => (
|
||||
<div key={index} className="flex items-center px-2 h-[80px] mt-3 mb-2">
|
||||
<div
|
||||
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{index + 1}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="sort-list-container flex-1">
|
||||
<DndContext onDragEnd={(e) => {
|
||||
const {active, over} = e;
|
||||
if (over && active.id !== over.id) {
|
||||
let oldIndex = -1, newIndex = -1;
|
||||
const originArr = [...videoData]
|
||||
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: '是否要移动到指定位置',
|
||||
onCancel: () => {
|
||||
setVideoData(originArr);
|
||||
}
|
||||
})
|
||||
}
|
||||
}}>
|
||||
<SortableContext items={videoData}>
|
||||
{videoData.map((v, index) => (
|
||||
<VideoListItem
|
||||
video={v}
|
||||
index={index + 1}
|
||||
id={v.id}
|
||||
key={index}
|
||||
active={state.playingIndex == index}
|
||||
checked={checkedIdArray.includes(v.id)}
|
||||
className={`list-item-${index} mt-3 mb-2`}
|
||||
onCheckedChange={(checked) => {
|
||||
setCheckedIdArray(idArray => {
|
||||
const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
|
||||
setState({checkedAll: newArr.length == videoData.length})
|
||||
return newArr;
|
||||
})
|
||||
}}
|
||||
onPlay={() => playVideo(v, index)}
|
||||
onEdit={() => {
|
||||
setEditId(v.article_id)
|
||||
}}
|
||||
editable
|
||||
/>))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right mt-10">
|
||||
<div className="text-right mt-5">
|
||||
<ButtonPush2Room ids={checkedIdArray}/>
|
||||
</div>
|
||||
</div>
|
||||
@ -157,6 +151,6 @@ export default function VideoIndex() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ArticleEditModal id={editId} onClose={() => setEditId(-1)}/>
|
||||
<ArticleEditModal type={'video'} id={editId} onClose={() => setEditId(-1)}/>
|
||||
</div>)
|
||||
}
|
@ -25,10 +25,11 @@ export function getById(id: Id) {
|
||||
return post<VideoInfo>({url: '/video/detail/' + id})
|
||||
}
|
||||
|
||||
export function deleteById(id: Id) {
|
||||
return post({url: '/video/detail/' + id})
|
||||
export function deleteByIds(ids: Id[]) {
|
||||
return post('/video/remove', {ids})
|
||||
}
|
||||
|
||||
|
||||
export function modifyOrder(ids: Id[]) {
|
||||
return post('/video/modifyorder', {ids})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user