Compare commits
3 Commits
f946e9d4f7
...
ddda905608
Author | SHA1 | Date | |
---|---|---|---|
ddda905608 | |||
c63b0c088e | |||
97d9200217 |
@ -1,5 +1,5 @@
|
|||||||
.blockContainer {
|
.blockContainer {
|
||||||
@apply flex mb-5;
|
@apply flex mb-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
@ -14,6 +14,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blockFist {
|
||||||
|
@apply p-0 border-0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.blockItem {
|
.blockItem {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -21,40 +25,61 @@
|
|||||||
.group {
|
.group {
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.imageList {
|
||||||
@apply border border-blue-200 p-2 flex-1 rounded focus:border-blue-200;
|
@apply grid grid-cols-4 gap-4 p-3 border border-blue-200;
|
||||||
min-height: 100px;
|
:global {
|
||||||
&:hover{
|
.ant-upload-wrapper {
|
||||||
@apply border-blue-500;
|
|
||||||
}
|
|
||||||
:global{
|
|
||||||
.ant-upload-wrapper{
|
|
||||||
display: block;
|
display: block;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.ant-upload{
|
|
||||||
|
.ant-upload {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
@apply block m-0;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: contain;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
@apply rounded bg-gray-100;
|
||||||
|
height: 100px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply border-blue-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.imageDelete{
|
||||||
|
@apply absolute flex items-center justify-center p-0.5 w-[22px] h-[22px] rounded-full border border-red-500 text-red-500 cursor-pointer z-10;
|
||||||
|
right:-10px;
|
||||||
|
top:-10px;
|
||||||
|
font-size: 14px;
|
||||||
|
&:hover{
|
||||||
|
@apply text-white bg-red-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
.uploadImage {
|
.uploadImage {
|
||||||
@apply flex justify-center items-center relative;
|
@apply flex justify-center items-center relative h-[100px] text-gray-400;
|
||||||
img {
|
|
||||||
display: block;
|
.uploadTips {
|
||||||
max-width: 100%;
|
@apply absolute inset-0 cursor-pointer opacity-0 transition rounded flex items-center justify-center bg-black/20 text-white;
|
||||||
max-height: 200px;
|
|
||||||
}
|
}
|
||||||
.uploadTips{
|
|
||||||
@apply absolute inset-0 cursor-pointer opacity-0 rounded flex items-center justify-center bg-black/50 text-white;
|
.imagePlaceholder {
|
||||||
}
|
|
||||||
.imagePlaceholder{
|
|
||||||
@apply flex items-center justify-center;
|
@apply flex items-center justify-center;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
&:hover{
|
|
||||||
.uploadTips{
|
&:hover {
|
||||||
|
@apply bg-gray-100 cursor-pointer rounded text-blue-500;
|
||||||
|
.uploadTips {
|
||||||
@apply opacity-100;
|
@apply opacity-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,10 +87,11 @@
|
|||||||
|
|
||||||
.text {
|
.text {
|
||||||
@apply border border-blue-200 overflow-hidden flex-1 rounded focus:border-blue-200 transition;
|
@apply border border-blue-200 overflow-hidden flex-1 rounded focus:border-blue-200 transition;
|
||||||
&:hover{
|
&:hover {
|
||||||
@apply border-blue-500;
|
@apply border-blue-500;
|
||||||
}
|
}
|
||||||
&:focus-within{
|
|
||||||
|
&:focus-within {
|
||||||
@apply border-blue-500 shadow-md;
|
@apply border-blue-500 shadow-md;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,56 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import {Popconfirm, Space} from "antd";
|
||||||
|
|
||||||
import {IconAdd, IconAddImage, IconAddText, IconDelete} from "@/components/icons";
|
import {IconAdd, IconDelete} from "@/components/icons";
|
||||||
import {BlockImage, BlockText} from "./item.tsx";
|
import ImageList from "@/components/article/list.tsx";
|
||||||
|
|
||||||
|
import { BlockText} from "./item.tsx";
|
||||||
import styles from './article.module.scss'
|
import styles from './article.module.scss'
|
||||||
import {Button, Popconfirm} from "antd";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
index?:number;
|
index?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
blocks: BlockContent[];
|
blocks: BlockContent[];
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
onChange?: (blocks: BlockContent[]) => void;
|
onChange?: (blocks: BlockContent[]) => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
onAdd?: () => void;
|
onAdd?: () => void;
|
||||||
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ArticleBlock({className, blocks, editable, onRemove, onAdd, onChange,index}: Props) {
|
function rebuildBlockArray(blocks: BlockContent[]) {
|
||||||
|
const textBlock: BlockContent = {
|
||||||
|
type: 'text',
|
||||||
|
content: ''
|
||||||
|
}
|
||||||
|
const _blocks: BlockContent[] = [textBlock];
|
||||||
|
const textArray: string[] = []
|
||||||
|
blocks.forEach(it => {
|
||||||
|
if (it.type == 'text') {
|
||||||
|
textArray.push(it.content)
|
||||||
|
} else {
|
||||||
|
_blocks.push(it)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
textBlock.content = textArray.join('\n')
|
||||||
|
return _blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function ArticleBlock(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
blocks: defaultBlocks,
|
||||||
|
editable,
|
||||||
|
onRemove,
|
||||||
|
onAdd,
|
||||||
|
onChange,
|
||||||
|
index,
|
||||||
|
errorMessage
|
||||||
|
}: Props) {
|
||||||
|
const blocks = rebuildBlockArray(defaultBlocks)
|
||||||
const handleBlockRemove = (index: number) => {
|
const handleBlockRemove = (index: number) => {
|
||||||
// 删除当前项
|
// 删除当前项
|
||||||
onChange?.(blocks.filter((_, idx) => index !== idx))
|
onChange?.(blocks.filter((_, idx) => index !== idx))
|
||||||
@ -43,72 +75,39 @@ export default function ArticleBlock({className, blocks, editable, onRemove, onA
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div className={styles.blockContainer}>
|
return <div className={styles.blockContainer}>
|
||||||
<div className={clsx(className || '', styles.block,' hover:bg-blue-10')}>
|
<div className={clsx(className || '', styles.block, index == 0 ? styles.blockFist : '', ' hover:bg-blue-10')}>
|
||||||
<div className={styles.blockBody}>
|
<div className={styles.blockBody}>
|
||||||
{blocks.map((it, idx) => {
|
<div>
|
||||||
const isFirstTextBlock = index == 0 && it.type ==='text' && firstTextBlockIndex == idx
|
<div className={clsx(index == 0 ? '' : styles.blockItem, 'flex')}>
|
||||||
return (<div key={idx}>
|
<BlockText
|
||||||
<div className={clsx(isFirstTextBlock?'':styles.blockItem, 'flex')}>
|
isFirstBlock={true}
|
||||||
{
|
onChange={(block) => handleBlockChange(0, block)}
|
||||||
it.type === 'text'
|
data={blocks[0]}
|
||||||
? <BlockText isFirstBlock={isFirstTextBlock} onChange={(block) => handleBlockChange(idx, block)} data={it}
|
isFirstBlock={index == 0}
|
||||||
editable={editable}/>
|
editable={editable}/>
|
||||||
: <BlockImage data={it} editable={editable}/>
|
</div>
|
||||||
}
|
{index == 0 && <div className="flex items-center text-red-500 justify-between text-sm mt-1">
|
||||||
{editable && <div className="create-container ml-2 flex flex-col justify-between">
|
<div>{errorMessage}</div>
|
||||||
{isFirstTextBlock?<span></span>:<Popconfirm
|
<div>该编辑框内容由数字人播报</div>
|
||||||
title="提示"
|
</div>}
|
||||||
description={<div style={{minWidth: 150}}>
|
</div>
|
||||||
<span>请确认删除此{it.type === 'text' ? '文本' : '图片'}?</span>
|
{index > 0 && <ImageList blocks={blocks} editable={editable} onChange={onChange}/>}
|
||||||
</div>}
|
|
||||||
onConfirm={() => handleBlockRemove(idx)}
|
|
||||||
okText="删除"
|
|
||||||
cancelText="取消"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="article-action-icon"
|
|
||||||
title={`删除此${it.type === 'text' ? '文本' : '图片'}`}>
|
|
||||||
<IconDelete style={{fontSize: 18}}/>
|
|
||||||
</span>
|
|
||||||
</Popconfirm>}
|
|
||||||
<div>
|
|
||||||
<span onClick={() => handleAddBlock('text', idx + 1)}
|
|
||||||
className="article-action-icon" title="新增文本"><IconAddText
|
|
||||||
style={{fontSize: 18}}/></span>
|
|
||||||
<span onClick={() => handleAddBlock('image', idx + 1)}
|
|
||||||
className="article-action-icon mt-1" title="新增图片"><IconAddImage
|
|
||||||
style={{fontSize: 16}}/></span>
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
{isFirstTextBlock && <div className={'text-red-500 text-right pr-6 mt-1 text-sm'}>该编辑框内容由数字人播报</div>}
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
{editable && blocks.length == 0 &&
|
|
||||||
<div style={{minHeight: 80}} className="flex items-center justify-center">
|
|
||||||
<div className="flex gap-5">
|
|
||||||
<Button onClick={() => handleAddBlock('text')}>添加文本</Button>
|
|
||||||
<Button onClick={() => handleAddBlock('image')}>添加图片</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{editable && <div className="ml-2 flex flex-col justify-between ">
|
{editable && <div className="ml-2 flex flex-col justify-between ">
|
||||||
<Popconfirm
|
{
|
||||||
title="提示"
|
index > 0 ? <Popconfirm
|
||||||
description={<div style={{minWidth: 150}}>
|
title={<div style={{minWidth: 150}}><span>请确认删除此删除此分组?</span></div>}
|
||||||
<span>请确认删除此删除此分组?</span>
|
onConfirm={onRemove}
|
||||||
</div>}
|
okText="删除"
|
||||||
onConfirm={onRemove}
|
cancelText="取消"
|
||||||
okText="删除"
|
>
|
||||||
cancelText="取消"
|
<span className="article-action-icon" title="删除此分组">
|
||||||
>
|
<IconDelete style={{fontSize: 24}}/>
|
||||||
<span className="article-action-icon" title="删除此分组"><IconDelete
|
</span>
|
||||||
style={{fontSize: 24}}/></span>
|
</Popconfirm> : <span></span>
|
||||||
</Popconfirm>
|
}
|
||||||
<span onClick={onAdd} className="article-action-icon" title="新增分组"><IconAdd
|
<span onClick={onAdd} className="article-action-icon" title="新增分组"><IconAdd
|
||||||
style={{fontSize: 24}}/></span>
|
style={{fontSize: 24}}/></span>
|
||||||
</div>}
|
</div>}
|
||||||
|
@ -2,11 +2,13 @@ import {Input, Modal} from "antd";
|
|||||||
import ArticleGroup from "@/components/article/group.tsx";
|
import ArticleGroup from "@/components/article/group.tsx";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {useSetState} from "ahooks";
|
import {useSetState} from "ahooks";
|
||||||
import {getById} from "@/service/api/article.ts";
|
import * as article from "@/service/api/article.ts";
|
||||||
|
import {regenerate} from "@/service/api/video.ts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id?: number;
|
id?: number;
|
||||||
onClose?: () => void;
|
type: 'news' | 'video';
|
||||||
|
onClose?: (saved?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ArticleEditModal(props: Props) {
|
export default function ArticleEditModal(props: Props) {
|
||||||
@ -16,29 +18,59 @@ export default function ArticleEditModal(props: Props) {
|
|||||||
|
|
||||||
const [state, setState] = useSetState({
|
const [state, setState] = useSetState({
|
||||||
loading: false,
|
loading: false,
|
||||||
open: false
|
open: false,
|
||||||
|
msgTitle: '',
|
||||||
|
msgGroup: '',
|
||||||
})
|
})
|
||||||
|
// 保存数据
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
console.log(groups, title)
|
||||||
|
if (!title) {
|
||||||
|
// setState({msgTitle: '请输入标题内容'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
|
||||||
|
// setState({msgGroup: '请输入正文文本内容'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const save = props.type == 'news' ? article.save : regenerate
|
||||||
|
setState({loading: true})
|
||||||
|
save(title, groups, props.id > 0 ? props.id : undefined).then(() => {
|
||||||
|
props.onClose?.(true)
|
||||||
|
}).finally(() => {
|
||||||
|
setState({loading: false})
|
||||||
|
});
|
||||||
props.onClose?.()
|
props.onClose?.()
|
||||||
// if (props.onSave) {
|
|
||||||
// setState({loading: true})
|
|
||||||
// props.onSave?.().then(() => {
|
|
||||||
// setState({loading: false, open: false})
|
|
||||||
// })
|
|
||||||
// } else {
|
|
||||||
// console.log(groups)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(props.id){
|
if (props.id) {
|
||||||
if(props.id > 0){
|
if (props.id > 0) {
|
||||||
getById(props.id).then(res => {
|
if (props.type == 'news') {
|
||||||
setGroups(res.content_group)
|
article.getById(props.id).then(res => {
|
||||||
setTitle(res.title)
|
setGroups(res.content_group)
|
||||||
})
|
setTitle(res.title)
|
||||||
}else{
|
})
|
||||||
setGroups([])
|
}
|
||||||
setTitle('')
|
} 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('韩国国会通过总统弹劾案 尹锡悦职务立即停止')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [props.id])
|
}, [props.id])
|
||||||
@ -49,7 +81,7 @@ export default function ArticleEditModal(props: Props) {
|
|||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
keyboard={false}
|
keyboard={false}
|
||||||
width={800}
|
width={800}
|
||||||
onCancel={props.onClose}
|
onCancel={()=>props.onClose?.()}
|
||||||
okButtonProps={{loading: state.loading}}
|
okButtonProps={{loading: state.loading}}
|
||||||
onOk={handleSave}
|
onOk={handleSave}
|
||||||
>
|
>
|
||||||
@ -59,18 +91,26 @@ export default function ArticleEditModal(props: Props) {
|
|||||||
<span className="require ml-1 font-bold text-red-500">*</span>
|
<span className="require ml-1 font-bold text-red-500">*</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="box mt-1">
|
<div className="box mt-1">
|
||||||
<Input value={title} onChange={e => {
|
<Input rootClassName={state.msgTitle ? 'border-red-500' : ''} value={title} onChange={e => {
|
||||||
setTitle(e.target.value)
|
setTitle(e.target.value)
|
||||||
|
setState({msgTitle: e.target.value ? '' : '请输入标题内容'})
|
||||||
}} placeholder={'请输入文章标题'}/>
|
}} placeholder={'请输入文章标题'}/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-red-500">{state.msgTitle}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="aricle-body mt-2">
|
<div className="aricle-body mt-3">
|
||||||
<div className="title">
|
<div className="title">
|
||||||
<span className="text text-base">正文</span>
|
<span className="text text-base">正文</span>
|
||||||
<span className="require ml-1 font-bold text-red-500">*</span>
|
<span className="require ml-1 font-bold text-red-500">*</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="box mt-1">
|
<div className="box mt-1">
|
||||||
<ArticleGroup editable groups={groups} onChange={list => setGroups(() => list)}/>
|
<ArticleGroup
|
||||||
|
errorMessage={state.msgGroup} editable groups={groups}
|
||||||
|
onChange={list => {
|
||||||
|
setGroups(() => list)
|
||||||
|
setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? '请输入正文文本内容' : ''});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>);
|
</Modal>);
|
||||||
|
@ -1,22 +1,46 @@
|
|||||||
|
|
||||||
import {message} from "antd"
|
import {message} from "antd"
|
||||||
import ArticleBlock from "@/components/article/block.tsx";
|
import ArticleBlock from "@/components/article/block.tsx";
|
||||||
|
|
||||||
import styles from './article.module.scss'
|
import styles from './article.module.scss'
|
||||||
|
import {showToast} from "@/components/message.ts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
groups: BlockContent[][];
|
groups: BlockContent[][];
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
onChange?: (groups: BlockContent[][]) => void;
|
onChange?: (groups: BlockContent[][]) => void;
|
||||||
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
export default function ArticleGroup({groups, editable, onChange}: Props) {
|
|
||||||
|
function rebuildGroups(groups: BlockContent[][]) {
|
||||||
|
if (groups.length < 2) {
|
||||||
|
Array(2 - groups.length).fill([{type: 'text', content: ''}]).forEach((it) => {
|
||||||
|
groups.push(it)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArticleGroup({groups: _groups, editable, onChange,errorMessage}: Props) {
|
||||||
|
const groups = rebuildGroups(_groups)
|
||||||
/**
|
/**
|
||||||
* 添加一个组
|
* 添加一个组
|
||||||
* @param insertIndex 插入的位置,-1表示插入到末尾
|
* @param insertIndex 插入的位置,-1表示插入到末尾
|
||||||
*/
|
*/
|
||||||
const handleAddGroup = ( insertIndex: number = -1) => {
|
const handleAddGroup = (insertIndex: number = -1) => {
|
||||||
const newGroup: BlockContent[] = []
|
if (insertIndex !== -1 && insertIndex !== 1) {
|
||||||
const _groups = [...groups]
|
const triggerGroup = insertIndex == -1 || insertIndex >= groups.length ? groups[groups.length - 1] : groups[insertIndex - 1];
|
||||||
|
// 判断
|
||||||
|
if (triggerGroup.length == 0 || triggerGroup.some(s => !s.content)) {
|
||||||
|
showToast('请先添加内容')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGroup: BlockContent[] = [{type: 'text', content: ''}]
|
||||||
|
const _groups = [...groups];
|
||||||
if (insertIndex == -1 || insertIndex >= groups.length) { // -1或者越界表示新增
|
if (insertIndex == -1 || insertIndex >= groups.length) { // -1或者越界表示新增
|
||||||
_groups.push(newGroup)
|
_groups.push(newGroup)
|
||||||
} else {
|
} else {
|
||||||
@ -34,6 +58,7 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
|
|||||||
groups[index] = blocks
|
groups[index] = blocks
|
||||||
onChange?.([...groups])
|
onChange?.([...groups])
|
||||||
}}
|
}}
|
||||||
|
errorMessage={errorMessage}
|
||||||
index={index}
|
index={index}
|
||||||
onAdd={() => {
|
onAdd={() => {
|
||||||
handleAddGroup?.(index + 1)
|
handleAddGroup?.(index + 1)
|
||||||
@ -48,6 +73,7 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{groups.length == 0 && editable &&
|
{groups.length == 0 && editable &&
|
||||||
<ArticleBlock editable onChange={blocks => onChange?.([blocks])} index={0} blocks={[{type:'text',content:''}]}/>}
|
<ArticleBlock editable onChange={blocks => onChange?.([blocks])} index={0}
|
||||||
|
blocks={[{type: 'text', content: ''}]}/>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -1,10 +1,12 @@
|
|||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {Button, Input, Spin, Upload, UploadProps} from "antd";
|
import {Input, Popconfirm, Spin, Upload, UploadProps} from "antd";
|
||||||
|
import {CloseOutlined,CloudUploadOutlined} from "@ant-design/icons";
|
||||||
|
import {clsx} from "clsx";
|
||||||
|
|
||||||
import styles from './article.module.scss'
|
import styles from './article.module.scss'
|
||||||
import {getOssPolicy} from "@/service/api/common.ts";
|
import {getOssPolicy} from "@/service/api/common.ts";
|
||||||
import {showToast} from "@/components/message.ts";
|
import {showToast} from "@/components/message.ts";
|
||||||
import {clsx} from "clsx";
|
import {IconAddImage} from "@/components/icons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@ -14,11 +16,15 @@ type Props = {
|
|||||||
onChange?: (data: BlockContent) => void;
|
onChange?: (data: BlockContent) => void;
|
||||||
isFirstBlock?: boolean;
|
isFirstBlock?: boolean;
|
||||||
}
|
}
|
||||||
|
type ImageProps = {
|
||||||
|
onRemove?: () => void;
|
||||||
|
onlyUpload?: boolean;
|
||||||
|
} & Props;
|
||||||
|
|
||||||
const MimeTypes = ['image/jpeg', 'image/png', 'image/jpg']
|
const MimeTypes = ['image/jpeg', 'image/png', 'image/jpg']
|
||||||
const Data: { uploadConfig?: TOSSPolicy } = {}
|
const Data: { uploadConfig?: TOSSPolicy } = {}
|
||||||
|
|
||||||
export function BlockImage({data, editable, onChange}: Props) {
|
export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: ImageProps) {
|
||||||
|
|
||||||
const [loading, setLoading] = useState<number>(-1)
|
const [loading, setLoading] = useState<number>(-1)
|
||||||
// oss上传文件所需的数据
|
// oss上传文件所需的数据
|
||||||
@ -48,7 +54,7 @@ export function BlockImage({data, editable, onChange}: Props) {
|
|||||||
console.log('onChange', file);
|
console.log('onChange', file);
|
||||||
if (file.status == 'done') {
|
if (file.status == 'done') {
|
||||||
setLoading(-1)
|
setLoading(-1)
|
||||||
onChange?.({type: 'image', content: Data.uploadConfig?.host + file.url})
|
onChange?.({type: 'image', content: Data.uploadConfig?.host + '/' + file.url})
|
||||||
} else if (file.status == 'error') {
|
} else if (file.status == 'error') {
|
||||||
setLoading(-1)
|
setLoading(-1)
|
||||||
showToast('上传图片失败,请重试', 'warning')
|
showToast('上传图片失败,请重试', 'warning')
|
||||||
@ -58,7 +64,15 @@ export function BlockImage({data, editable, onChange}: Props) {
|
|||||||
}
|
}
|
||||||
//
|
//
|
||||||
return <div className={styles.image}>
|
return <div className={styles.image}>
|
||||||
{editable ? <div>
|
{editable ? <div className={'relative'}>
|
||||||
|
{!onlyUpload && <Popconfirm
|
||||||
|
title={<div style={{minWidth: 150}}><span>请确认删除此删除此图片?</span></div>}
|
||||||
|
onConfirm={onRemove}
|
||||||
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<span className={styles.imageDelete}><CloseOutlined/></span>
|
||||||
|
</Popconfirm>}
|
||||||
<Spin spinning={loading >= 0} percent={loading == 0 ? 'auto' : loading}>
|
<Spin spinning={loading >= 0} percent={loading == 0 ? 'auto' : loading}>
|
||||||
<Upload
|
<Upload
|
||||||
multiple={false} maxCount={1} data={getUploadData}
|
multiple={false} maxCount={1} data={getUploadData}
|
||||||
@ -73,7 +87,10 @@ export function BlockImage({data, editable, onChange}: Props) {
|
|||||||
<span>更换图片</span>
|
<span>更换图片</span>
|
||||||
</div>
|
</div>
|
||||||
</> : <div className={styles.imagePlaceholder}>
|
</> : <div className={styles.imagePlaceholder}>
|
||||||
<Button>选择图片</Button>
|
<div className={'text-center'}>
|
||||||
|
<IconAddImage className={"text-4xl inline-block"} />
|
||||||
|
<div className={'text-sm'}>上传图片</div>
|
||||||
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</Upload>
|
</Upload>
|
||||||
@ -84,13 +101,14 @@ export function BlockImage({data, editable, onChange}: Props) {
|
|||||||
|
|
||||||
export function BlockText({data, editable, onChange, isFirstBlock}: Props) {
|
export function BlockText({data, editable, onChange, isFirstBlock}: Props) {
|
||||||
return <div className='flex-1'>
|
return <div className='flex-1'>
|
||||||
<div className={clsx(styles.text, isFirstBlock?'border-red-400 hover:border-red-500 focus-within:border-red-500':'')}>
|
<div
|
||||||
|
className={clsx(styles.text, isFirstBlock ? 'border-red-400 hover:border-red-500 focus-within:border-red-500' : '')}>
|
||||||
{editable ? <div className="relative">
|
{editable ? <div className="relative">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
onChange?.({type: 'text', content: e.target.value})
|
onChange?.({type: 'text', content: e.target.value})
|
||||||
}}
|
}}
|
||||||
placeholder={'请输入文本'} value={data.content} autoSize={{minRows: 3, maxRows: 8}}
|
placeholder={'请输入文本内容'} value={data.content} autoSize={{minRows: 3, maxRows: 8}}
|
||||||
variant={"borderless"}/>
|
variant={"borderless"}/>
|
||||||
</div> : <p className="p-2">{data.content}</p>}
|
</div> : <p className="p-2">{data.content}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
42
src/components/article/list.tsx
Normal file
42
src/components/article/list.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {BlockImage} from "@/components/article/item.tsx";
|
||||||
|
|
||||||
|
import styles from './article.module.scss'
|
||||||
|
|
||||||
|
export default function ImageList(props: {
|
||||||
|
blocks: BlockContent[];
|
||||||
|
editable?: boolean;
|
||||||
|
onChange?: (blocks: BlockContent[]) => void;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
// 处理删除
|
||||||
|
const handleRemove = (index: number) => {
|
||||||
|
props.onChange?.(props.blocks.filter((_, idx) => index !== idx))
|
||||||
|
const newBlocks = [...props.blocks]
|
||||||
|
newBlocks.splice(index, 1)
|
||||||
|
props.onChange?.(newBlocks)
|
||||||
|
}
|
||||||
|
// 处理新增
|
||||||
|
const handleAdd = (data: BlockContent) => {
|
||||||
|
props.onChange?.([...props.blocks, data])
|
||||||
|
}
|
||||||
|
// 处理更新
|
||||||
|
const handleUpdate = (index: number, data: BlockContent) => {
|
||||||
|
props.onChange?.(props.blocks.map((it, idx) => idx === index ? data : it))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (<div className={styles.imageList}>
|
||||||
|
{props.blocks.map((it, index) => (
|
||||||
|
it.type === 'image' ? <BlockImage
|
||||||
|
key={index} data={it} editable={props.editable}
|
||||||
|
onChange={(data) => handleUpdate(index, data)}
|
||||||
|
onRemove={() => handleRemove(index)}
|
||||||
|
/> : null
|
||||||
|
))}
|
||||||
|
{props.editable &&
|
||||||
|
<BlockImage onlyUpload onChange={handleAdd} data={{type: 'image', content: ''}} editable={true}/>}
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
|||||||
import {message} from "antd";
|
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') {
|
||||||
|
|
||||||
@ -8,6 +9,10 @@ export function showToast(content: string, type?: 'success' | 'info' | 'warning'
|
|||||||
className: 'aui-toast'
|
className: 'aui-toast'
|
||||||
}).then();
|
}).then();
|
||||||
}
|
}
|
||||||
|
export function showErrorToast(e:Error|BizError) {
|
||||||
|
showToast(String(((e instanceof BizError)?e.data:'') || e.message),'error')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function showLoading(content = 'Loading...') {
|
export function showLoading(content = 'Loading...') {
|
||||||
const key = 'globalLoading_' + (new Date().getTime());
|
const key = 'globalLoading_' + (new Date().getTime());
|
||||||
|
@ -8,7 +8,7 @@ import {IconEdit, IconPlay} from "@/components/icons";
|
|||||||
import {Popconfirm} from "antd";
|
import {Popconfirm} from "antd";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
video: VideoInfo,
|
video: VideoInfo | LiveVideoInfo,
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
index?: number;
|
index?: number;
|
||||||
@ -25,7 +25,6 @@ export const VideoListItem = (
|
|||||||
{
|
{
|
||||||
index, id, video, onPlay, onRemove, checked,
|
index, id, video, onPlay, onRemove, checked,
|
||||||
onCheckedChange, onEdit, active, editable,
|
onCheckedChange, onEdit, active, editable,
|
||||||
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
attributes, listeners,
|
attributes, listeners,
|
||||||
@ -42,14 +41,13 @@ export const VideoListItem = (
|
|||||||
className={'video-item flex items-center gap-3 mb-5'}
|
className={'video-item flex items-center gap-3 mb-5'}
|
||||||
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
|
<div className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{index}</div>
|
||||||
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{id}</div>
|
|
||||||
</div>}
|
</div>}
|
||||||
<div
|
<div
|
||||||
className={`video-item-info flex gap-2 flex-1 bg-gray-100 min-h-[40px] rounded-lg p-3 shadow-blue-500 ${active ? 'video-item-shadow' : ''}`}>
|
className={`video-item-info flex gap-2 flex-1 bg-gray-100 min-h-[40px] rounded-lg p-3 shadow-blue-500 ${active ? 'video-item-shadow' : ''}`}>
|
||||||
<div className={'video-title leading-7 flex-1'}>{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'}>
|
||||||
<img className="w-[100px] rounded-md" src={video.cover || ''} alt={video.title}/>
|
<img className="w-[100px] rounded-md" src={video.cover_url || ''} alt={video.video_title}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{editable &&
|
{editable &&
|
||||||
|
@ -17,9 +17,9 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) {
|
|||||||
timeRange: string;
|
timeRange: string;
|
||||||
keywords: string;
|
keywords: string;
|
||||||
searching: boolean;
|
searching: boolean;
|
||||||
time: string;
|
time: number;
|
||||||
}>({
|
}>({
|
||||||
keywords: "", searching: false, timeRange: "", time: '-1'
|
keywords: "", searching: false, timeRange: "", time: 0
|
||||||
})
|
})
|
||||||
const onFinish = (values: any) => {
|
const onFinish = (values: any) => {
|
||||||
setState({searching: true})
|
setState({searching: true})
|
||||||
@ -52,12 +52,11 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) {
|
|||||||
{/*<Form.Item label={'更新时间'} name="timeRange">*/}
|
{/*<Form.Item label={'更新时间'} name="timeRange">*/}
|
||||||
{/* <DatePicker.RangePicker />*/}
|
{/* <DatePicker.RangePicker />*/}
|
||||||
{/*</Form.Item>*/}
|
{/*</Form.Item>*/}
|
||||||
{/*<Form.Item>*/}
|
<Form.Item>
|
||||||
{/* <Space size={10}>*/}
|
<Space size={10}>
|
||||||
{/* <Button type={'primary'} htmlType={'submit'}>搜索</Button>*/}
|
<Button type={'primary'} htmlType={'submit'}>搜索</Button>
|
||||||
{/* <Button htmlType={'reset'}>重置</Button>*/}
|
</Space>
|
||||||
{/* </Space>*/}
|
</Form.Item>
|
||||||
{/*</Form.Item>*/}
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<Space size={10}>
|
<Space size={10}>
|
||||||
|
@ -1,15 +1,99 @@
|
|||||||
import React, {useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {Button, message, Modal} from "antd";
|
import {Button, message, Modal} from "antd";
|
||||||
import {SortableContext, arrayMove} from '@dnd-kit/sortable';
|
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} from "@/service/api/live.ts";
|
||||||
|
|
||||||
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
export default function LiveIndex() {
|
export default function LiveIndex() {
|
||||||
const [videoData, setVideoData] = useState<VideoInfo[]>()
|
const [videoData, setVideoData] = useState<LiveVideoInfo[]>([])
|
||||||
const [modal, contextHolder] = Modal.useModal()
|
const [modal, contextHolder] = Modal.useModal()
|
||||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||||
const [editable,setEditable] = useState<boolean>(false)
|
const [editable, setEditable] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
activeId: -1,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getList().then(res => {
|
||||||
|
setVideoData([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
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: 2,
|
||||||
|
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: 3,
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
const processDeleteVideo = async (_idArray: number[]) => {
|
const processDeleteVideo = async (_idArray: number[]) => {
|
||||||
message.info('删除成功!!!' + _idArray.join(''));
|
message.info('删除成功!!!' + _idArray.join(''));
|
||||||
}
|
}
|
||||||
@ -42,19 +126,19 @@ export default function LiveIndex() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="video-list-container flex-1">
|
<div className="video-list-container flex-1 ">
|
||||||
<div className=" bg-white py-8 px-6 rounded py-1">
|
<div className=" bg-white py-8 px-6 rounded py-1">
|
||||||
<div className="live-control flex justify-between mb-8">
|
<div className="live-control flex justify-between mb-8">
|
||||||
{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={() => setEditable(false)}>退出</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
<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>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -86,7 +170,7 @@ export default function LiveIndex() {
|
|||||||
video={v}
|
video={v}
|
||||||
index={index + 1}
|
index={index + 1}
|
||||||
id={v.id}
|
id={v.id}
|
||||||
active={index == 0}
|
active={state.activeId == v.id}
|
||||||
key={index}
|
key={index}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
setCheckedIdArray(idArray => {
|
setCheckedIdArray(idArray => {
|
||||||
|
3
src/pages/live/style.module.scss
Normal file
3
src/pages/live/style.module.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.videoListContainer{
|
||||||
|
|
||||||
|
}
|
@ -1,26 +1,26 @@
|
|||||||
import {Button, Modal} from "antd";
|
import {Button, Modal} from "antd";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {showToast} from "@/components/message.ts";
|
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||||
import {push2article} from "@/service/api/news.ts";
|
import {push2video} from "@/service/api/article.ts";
|
||||||
|
|
||||||
|
|
||||||
export default function ButtonPush2Video(props: { ids: Id[]}){
|
export default function ButtonPush2Video(props: { ids: Id[] }) {
|
||||||
const [loading,setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const handlePush = ()=>{
|
const handlePush = () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
push2article(props.ids).then(()=>{
|
push2video(props.ids).then(() => {
|
||||||
showToast('一键推流成功,已成功推入数字人视频生成,请前往数字人视频生成页面查看!', 'success')
|
showToast('一键推流成功,已成功推入数字人视频生成,请前往数字人视频生成页面查看!', 'success')
|
||||||
}).finally(()=>{
|
}).catch(showErrorToast).finally(() => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const onPushClick = ()=>{
|
const onPushClick = () => {
|
||||||
if (props.ids.length === 0) {
|
if (props.ids.length === 0) {
|
||||||
showToast('请选择要开播的新闻', 'warning')
|
showToast('请选择要开播的新闻', 'warning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title:'操作提示',
|
title: '操作提示',
|
||||||
content: '是否确定一键开播选中新闻?',
|
content: '是否确定一键开播选中新闻?',
|
||||||
onOk: handlePush
|
onOk: handlePush
|
||||||
})
|
})
|
||||||
|
@ -14,12 +14,9 @@ export default function NewEdit() {
|
|||||||
const [editId, setEditId] = useState(-1)
|
const [editId, setEditId] = useState(-1)
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<Id[]>([])
|
const [selectedRowKeys, setSelectedRowKeys] = useState<Id[]>([])
|
||||||
const [params, setParams] = useState<ApiArticleSearchParams>({
|
const [params, setParams] = useState<ApiArticleSearchParams>({
|
||||||
pagination: {
|
pagination: {page: 1, limit: 10}
|
||||||
page: 1,
|
|
||||||
limit: 10
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const {data} = useRequest(() => getList(params), {refreshDeps: [params]})
|
const {data, refresh} = useRequest(() => getList(params), {refreshDeps: [params]})
|
||||||
|
|
||||||
const columns: TableColumnsType<ListArticleItem> = [
|
const columns: TableColumnsType<ListArticleItem> = [
|
||||||
{
|
{
|
||||||
@ -86,15 +83,20 @@ export default function NewEdit() {
|
|||||||
showSizeChanger={false}
|
showSizeChanger={false}
|
||||||
simple={true}
|
simple={true}
|
||||||
rootClassName={'simple-pagination'}
|
rootClassName={'simple-pagination'}
|
||||||
onChange={(page) => setParams(prev=>({
|
onChange={(page) => setParams(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
pagination: {page, limit: 10}
|
pagination: {page, limit: 10}
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<ButtonPush2Video ids={selectedRowKeys} />
|
<ButtonPush2Video ids={selectedRowKeys}/>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<ArticleEditModal id={editId} onClose={() => setEditId(-1)}/>
|
<ArticleEditModal
|
||||||
|
type="news" id={editId}
|
||||||
|
onClose={(saved) => {
|
||||||
|
setEditId(-1)
|
||||||
|
if (saved) refresh()
|
||||||
|
}}/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
@ -95,7 +95,7 @@ export default function NewsIndex() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="title text-lg cursor-pointer" onClick={() => {
|
<div className="title text-lg cursor-pointer" onClick={() => {
|
||||||
handleViewNewsDetail(item.id)
|
handleViewNewsDetail(item.id)
|
||||||
}}>{item.id}{item.title}</div>
|
}}>{item.title}</div>
|
||||||
{item.internal_article_id > 0 &&
|
{item.internal_article_id > 0 &&
|
||||||
<div className="text-sm text-blue-500">已加入编辑界面</div>}
|
<div className="text-sm text-blue-500">已加入编辑界面</div>}
|
||||||
</div>
|
</div>
|
||||||
|
31
src/pages/video/components/button-push2room.tsx
Normal file
31
src/pages/video/components/button-push2room.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {Button, Modal} from "antd";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||||
|
import {push2room} from "@/service/api/video.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export default function ButtonPush2Room(props: { ids: Id[]}){
|
||||||
|
const [loading,setLoading] = useState(false)
|
||||||
|
const handlePush = ()=>{
|
||||||
|
setLoading(true)
|
||||||
|
push2room(props.ids).then(()=>{
|
||||||
|
showToast('一键推流成功,已推流至数字人直播间,请前往数字人直播间页面查看!', 'success')
|
||||||
|
}).catch(showErrorToast).finally(()=>{
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onPushClick = ()=>{
|
||||||
|
if (props.ids.length === 0) {
|
||||||
|
showToast('请选择要推流的新闻', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title:'操作提示',
|
||||||
|
content: '是否确定一键推流选中新闻视频??',
|
||||||
|
onOk: handlePush
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button type="primary" loading={loading} onClick={onPushClick}>一键推流</Button>
|
||||||
|
)
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
import {Button, message, Modal} from "antd";
|
import {message, Modal} from "antd";
|
||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||||
|
|
||||||
import {ArticleGroupList, MockVideoDataList} from "@/_local/mock-data";
|
|
||||||
import {DndContext} from "@dnd-kit/core";
|
import {DndContext} from "@dnd-kit/core";
|
||||||
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
|
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
|
||||||
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||||
@ -10,19 +8,18 @@ import {useSetState} from "ahooks";
|
|||||||
import {CheckCircleFilled} from "@ant-design/icons";
|
import {CheckCircleFilled} from "@ant-design/icons";
|
||||||
import {clsx} from "clsx";
|
import {clsx} from "clsx";
|
||||||
import {getList} from "@/service/api/video.ts";
|
import {getList} from "@/service/api/video.ts";
|
||||||
|
import {formatDuration} from "@/util/strings.ts";
|
||||||
|
import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx";
|
||||||
|
|
||||||
|
|
||||||
export default function CreateIndex() {
|
export default function VideoIndex() {
|
||||||
const [editNews, setEditNews] = useSetState<{
|
const [editId, setEditId] = useState(-1)
|
||||||
title?: string;
|
|
||||||
groups?: ArticleContentGroup[];
|
|
||||||
}>({})
|
|
||||||
|
|
||||||
const [videoData, setVideoData] = useState<VideoInfo[]>([])
|
const [videoData, setVideoData] = useState<VideoInfo[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getList({}).then((ret) => {
|
getList().then((ret) => {
|
||||||
setVideoData(ret.list)
|
setVideoData(ret.list || [])
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -63,13 +60,19 @@ export default function CreateIndex() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalDuration = useMemo(() => {
|
||||||
|
if(!videoData || videoData.length == 0) return 0;
|
||||||
|
// 计算总时长
|
||||||
|
return videoData.reduce((sum, v) => sum + v.duration, 0);
|
||||||
|
}, [videoData])
|
||||||
|
|
||||||
return (<div className="container py-10 page-live">
|
return (<div className="container py-10 page-live">
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="video-list-container bg-white p-10 rounded flex-1">
|
<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-8">
|
||||||
<div className="pl-[70px]">
|
<div className="pl-[70px]">
|
||||||
<span>视频时长: 00:00:29</span>
|
<span>视频时长: {formatDuration(totalDuration)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
||||||
@ -99,6 +102,7 @@ export default function CreateIndex() {
|
|||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<SortableContext items={videoData}>
|
<SortableContext items={videoData}>
|
||||||
|
|
||||||
{videoData.map((v, index) => (
|
{videoData.map((v, index) => (
|
||||||
<VideoListItem
|
<VideoListItem
|
||||||
video={v}
|
video={v}
|
||||||
@ -115,25 +119,25 @@ export default function CreateIndex() {
|
|||||||
}}
|
}}
|
||||||
onPlay={() => playVideo(v)}
|
onPlay={() => playVideo(v)}
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
setEditNews({title: v.title, groups: [...ArticleGroupList]})
|
setEditId(v.article_id)
|
||||||
}}
|
}}
|
||||||
editable
|
editable
|
||||||
/>))}
|
/>))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
<div className="text-right mt-10">
|
<div className="text-right mt-10">
|
||||||
<Button type="primary">一键推流</Button>
|
<ButtonPush2Room ids={checkedIdArray}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="video-player-container ml-8 w-[400px] flex flex-col">
|
<div className="video-player-container ml-6 w-[450px] flex flex-col">
|
||||||
<div className="text-center text-base mt-10">预览视频</div>
|
<div className="text-center text-base mt-10">预览视频</div>
|
||||||
<div className="video-player flex items-center justify-center flex-1">
|
<div className="video-player flex items-center justify-center mt-20">
|
||||||
<div className=" rounded overflow-hidden">
|
<div className=" rounded overflow-hidden">
|
||||||
<video ref={videoRef} controls autoPlay className="w-full bg-white min-w-[360px]"></video>
|
<video ref={videoRef} controls autoPlay className="w-full bg-white min-w-[360px]"></video>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ArticleEditModal title={editNews.title} groups={editNews.groups}/>
|
<ArticleEditModal id={editId} onClose={() => setEditId(-1)}/>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
@ -1,7 +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 CreateIndex from "@/pages/create";
|
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";
|
||||||
import NewsIndex from "@/pages/news";
|
import NewsIndex from "@/pages/news";
|
||||||
|
@ -22,12 +22,13 @@ export function getById(id: Id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function save(title: string, content_group: BlockContent[][], id: number) {
|
export function save(title: string, content_group: BlockContent[][], id: number) {
|
||||||
return post<{ content: string }>({
|
return post<{ content: string }>(id && id > 0 ? '/article/modify' : '/article/create/new', {
|
||||||
url: '/spider/article',
|
title,
|
||||||
data: {
|
content_group,
|
||||||
title,
|
id
|
||||||
content_group,
|
|
||||||
id
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function push2video(article_ids: Id[]) {
|
||||||
|
return post('/article/push2video', {article_ids})
|
||||||
}
|
}
|
20
src/service/api/live.ts
Normal file
20
src/service/api/live.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {post} from "@/service/request.ts";
|
||||||
|
|
||||||
|
export function playState() {
|
||||||
|
return post<{
|
||||||
|
id: number;
|
||||||
|
start_time?: number;
|
||||||
|
}>({url: '/room/playing'})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getList() {
|
||||||
|
return post<DataList<LiveVideoInfo>>('/room/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modifyOrder(ids: Id[]) {
|
||||||
|
return post('/video/order', {ids})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteById(ids: Id[]) {
|
||||||
|
return post('/video/remove', {ids})
|
||||||
|
}
|
@ -1,10 +1,7 @@
|
|||||||
import {post} from "@/service/request.ts";
|
import {post} from "@/service/request.ts";
|
||||||
|
|
||||||
export function getList(data: {
|
export function getList() {
|
||||||
title?: string,
|
return post<DataList<VideoInfo>>('/video/list')
|
||||||
time_flag?: number;
|
|
||||||
}) {
|
|
||||||
return post<DataList<VideoInfo>>({url: '/video/list', data})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -13,7 +10,7 @@ export function getList(data: {
|
|||||||
* @param content_group
|
* @param content_group
|
||||||
* @param article_id
|
* @param article_id
|
||||||
*/
|
*/
|
||||||
export function regenerate(title: string, content_group: BlockContent[][], article_id: number) {
|
export function regenerate(title: string, content_group: BlockContent[][], article_id?: Id) {
|
||||||
return post<{ content: string }>({
|
return post<{ content: string }>({
|
||||||
url: '/video/regenerate',
|
url: '/video/regenerate',
|
||||||
data: {
|
data: {
|
||||||
@ -31,9 +28,11 @@ export function getById(id: Id) {
|
|||||||
export function deleteById(id: Id) {
|
export function deleteById(id: Id) {
|
||||||
return post({url: '/video/detail/' + id})
|
return post({url: '/video/detail/' + id})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function modifyOrder(ids: Id[]) {
|
export function modifyOrder(ids: Id[]) {
|
||||||
return post({url: ' /video/modifyorder',data:{ids}})
|
return post('/video/modifyorder', {ids})
|
||||||
}
|
}
|
||||||
export function push2room(ids: Id[]) {
|
|
||||||
return post({url: ' /video/push2room',data:{ids}})
|
export function push2room(video_ids: Id[]) {
|
||||||
|
return post('/video/push2room', {video_ids})
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ import axios from 'axios';
|
|||||||
import {stringify} from 'qs'
|
import {stringify} from 'qs'
|
||||||
import {BizError} from './types';
|
import {BizError} from './types';
|
||||||
import {getAuthToken} from "@/hooks/useAuth.ts";
|
import {getAuthToken} from "@/hooks/useAuth.ts";
|
||||||
|
import {showToast} from "@/components/message.ts";
|
||||||
|
|
||||||
const JSON_FORMAT: string = 'application/json';
|
const JSON_FORMAT: string = 'application/json';
|
||||||
const REQUEST_TIMEOUT = 300000; // 超时时长5min
|
const REQUEST_TIMEOUT = 300000; // 超时时长5min
|
||||||
@ -23,6 +24,7 @@ Axios.interceptors.request.use(config => {
|
|||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}, err => {
|
}, err => {
|
||||||
|
console.log('请求拦截器报错',err)
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -46,11 +48,11 @@ export function request<T>(options: RequestOption) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// const
|
// const
|
||||||
const {code, message, data, request_id} = res.data
|
const {code, msg, data, trace_id} = res.data
|
||||||
if (code == 0) {
|
if (code == 0) {
|
||||||
resolve(data as unknown as T)
|
resolve(data as unknown as T)
|
||||||
} else {
|
} else {
|
||||||
reject(new BizError(message, code, request_id, data as unknown as AllType))
|
reject(new BizError(msg, code, trace_id, data as unknown as AllType))
|
||||||
}
|
}
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
reject(new BizError(e.message, 500))
|
reject(new BizError(e.message, 500))
|
||||||
@ -59,9 +61,13 @@ export function request<T>(options: RequestOption) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function post<T>(params: RequestOption) {
|
export function post<T>(params: RequestOption | string, _data?: AllType) {
|
||||||
|
const options = typeof params === 'string' ? {url: params} : params;
|
||||||
|
if (_data) {
|
||||||
|
options.data = _data
|
||||||
|
}
|
||||||
return request<T>({
|
return request<T>({
|
||||||
...params,
|
...options,
|
||||||
method: 'post'
|
method: 'post'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
12
src/types/api.d.ts
vendored
12
src/types/api.d.ts
vendored
@ -89,3 +89,15 @@ declare interface VideoInfo {
|
|||||||
article_id: number;
|
article_id: number;
|
||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
// room live
|
||||||
|
declare interface LiveVideoInfo {
|
||||||
|
id: number;
|
||||||
|
video_id: number;
|
||||||
|
video_title: string;
|
||||||
|
cover_url: string;
|
||||||
|
video_duration: number;
|
||||||
|
video_oss_url: string;
|
||||||
|
status: number;
|
||||||
|
order_no: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime"
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
|
import {padStart} from "lodash";
|
||||||
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
@ -79,4 +80,14 @@ export function calcContentLengthLikeWord(str:string) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return str.length
|
return str.length
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将时长转换成 时:分:秒
|
||||||
|
export function formatDuration(duration: number) {
|
||||||
|
const hour = Math.floor(duration / 3600);
|
||||||
|
const minute = Math.floor((duration - hour * 3600) / 60);
|
||||||
|
const second = duration - hour * 3600 - minute * 60;
|
||||||
|
// 需要补0
|
||||||
|
return padStart(hour.toString(), 2, '0') + ':' + padStart(minute.toString(), 2, '0') + ':' + padStart(second.toString(), 2, '0')
|
||||||
|
// return `${hour}:${minute}:${second}`
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user