feat: 新闻编辑UI新增背景选择

This commit is contained in:
LittleBoy 2025-04-21 17:13:57 +08:00
parent b7b15e7471
commit d270d615a2
8 changed files with 173 additions and 120 deletions

View File

@ -443,21 +443,32 @@
}
.article-title {
@apply px-6 pt-10 pb-6;
border-bottom: 1px solid rgba(0,0,0,0.09);
}
.article-body {
@apply p-6
@apply p-6 pt-1;
}
.modal-control-footer {
@apply p-6
}
.hot-news-list{
@apply focus-within:bg-[#e6ebf1] focus-within:border-gray-100;
}
.input-box {
// focus-within:shadow
@apply bg-[#f8f8f8] border border-transparent w-full px-4 py-2 focus-within:bg-[#f0f0f0] focus-within:border-gray-300;
@apply text-base bg-[#f8f8f8] border border-transparent w-full px-3 focus-within:bg-[#f3f3f3] focus-within:border-gray-100;
border-radius: 8px;
color:#3d3d3d;
}
.main-human-text{
@apply focus-within:bg-[#e6ebf1] focus-within:border-gray-100;
}
.main-human-text-input{
// focus-within:shadow
@apply text-base bg-[#f8f8f8] border border-transparent w-full p-2;
min-height: 100%;
}
}
.icon-language{

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,21 +1,25 @@
import {Modal,App} from "antd";
import React, {useEffect, useState} from "react";
import {useSetState} from "ahooks";
import {useTranslation} from "react-i18next";
import * as article from "@/service/api/article.ts";
import {regenerate} from "@/service/api/video.ts";
import {push2video} from "@/service/api/article.ts";
import {showErrorToast, showToast} from "@/components/message.ts";
import ArticleGroup, {HotNewsData} from "@/components/article/group.tsx";
import type {HookAPI as ModalHookAPI} from "antd/es/modal/useModal";
import {TFunction} from "i18next";
import {IconWarningCircle} from "@/components/icons";
import { Modal, App, Radio, Popover } from 'antd';
import React, { useEffect, useState } from 'react';
import { useSetState } from 'ahooks';
import { useTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
import * as article from '@/service/api/article.ts';
import { regenerate } from '@/service/api/video.ts';
import { push2video } from '@/service/api/article.ts';
import { showErrorToast, showToast } from '@/components/message.ts';
import ArticleGroup, { HotNewsData } from '@/components/article/group.tsx';
import type { HookAPI as ModalHookAPI } from 'antd/es/modal/useModal';
import { IconWarningCircle } from '@/components/icons';
import Bg1 from './assets/bg1.jpg'
import Bg2 from './assets/bg2.jpg'
type Props = {
id?: number;
type: 'news' | 'video';
onClose?: (saved?: boolean) => void;
onRefresh?: ()=>void
onRefresh?: () => void
}
const DEFAULT_STATE = {
@ -24,15 +28,15 @@ const DEFAULT_STATE = {
msgTitle: '',
msgGroup: '',
error: ''
}
};
function pushBlocksToGroup(blocks: BlockContent[], groups: BlockContent[][]) {
const lastGroup = groups[groups.length - 1]
const lastGroup = groups[groups.length - 1];
if (lastGroup && lastGroup.filter(s => s.type == 'text').length == 0) {
// 如果上一个group中没有文本则直接合并
lastGroup.push(...blocks)
lastGroup.push(...blocks);
} else {
groups.push(blocks)
groups.push(blocks);
}
}
@ -43,109 +47,114 @@ function rebuildGroups(groups: BlockContent[][]) {
if (!blocks) return;
blocks = blocks.filter(s => !!s).sort((a, b) => {
if (a.type == 'text' && b.type == 'text') return 1;
return a.type == 'text' ? -1 : 1
})
return a.type == 'text' ? -1 : 1;
});
if (blocks.length == 1) {
if (index == 0) _groups.push(blocks)
else pushBlocksToGroup(blocks, _groups)
if (index == 0) _groups.push(blocks);
else pushBlocksToGroup(blocks, _groups);
} else {
if (index == 0) {
_groups.push([blocks[0]])
_groups.push(blocks.slice(1))
_groups.push([blocks[0]]);
_groups.push(blocks.slice(1));
} else {
pushBlocksToGroup(blocks, _groups)
pushBlocksToGroup(blocks, _groups);
}
}
});
if (_groups.length < 2) {
Array(2 - _groups.length).fill([{type: 'text', content: ''}]).forEach((it) => {
_groups.push(it)
})
Array(2 - _groups.length).fill([{ type: 'text', content: '' }]).forEach((it) => {
_groups.push(it);
});
}
// console.log('rebuildGroups', _groups)
return _groups;
}
function groupHasImageAndText(blocks: BlockContent[]) {
return blocks.some(s=>s.type == 'image' && s.content.trim().length > 0) && blocks.some(s=>s.type == 'text' && s.content.trim().length > 0)
return blocks.some(s => s.type == 'image' && s.content.trim().length > 0) && blocks.some(s => s.type == 'text' && s.content.trim().length > 0);
}
// 验证分组数据是否合法
function checkGroupsValid(_groups: BlockContent[][]) {
const groups = _groups.filter((_,index)=>{
const groups = _groups.filter((_, index) => {
if (index == 0) return true;
return _.length > 1 || (_.length == 1 && _[0].content.trim().length > 0) ;
})
return _.length > 1 || (_.length == 1 && _[0].content.trim().length > 0);
});
if (groups.length == 1) return true;
for (let index = 1;index< groups.length; index ++) {
if(!groupHasImageAndText(groups[index])) return false;
for (let index = 1; index < groups.length; index++) {
if (!groupHasImageAndText(groups[index])) return false;
}
return true;
}
function checkHotNewsValid(hotNews: HotNewsData,modal:ModalHookAPI,t:TFunction<"translation", undefined>) {
return new Promise<boolean>((resolve)=>{
function checkHotNewsValid(hotNews: HotNewsData, modal: ModalHookAPI, t: TFunction<'translation', undefined>) {
return new Promise<boolean>((resolve) => {
// 验证热点新闻数据是否正确
if(hotNews.mode == 'manual' && hotNews.list.filter(s=>s.trim().length > 0).length < 3){
if (hotNews.mode == 'manual' && hotNews.list.filter(s => s.trim().length > 0).length < 3) {
modal.warning({
wrapClassName: 'root-modal-confirm',
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle /></span>,
title: t('modal.hot_news.empty_notice_title'),
content: <span dangerouslySetInnerHTML={{__html:t('modal.hot_news.empty_notice_message')}}></span>,
centered:true,
content: <span dangerouslySetInnerHTML={{ __html: t('modal.hot_news.empty_notice_message') }}></span>,
centered: true,
onOk: () => {
resolve(false)
resolve(false);
},
onCancel: () => {
resolve(false)
resolve(false);
}
})
});
return;
}
resolve(true)
})
resolve(true);
});
}
export default function ArticleEditModal(props: Props) {
const {t,i18n} = useTranslation()
const {modal} = App.useApp()
const { t, i18n } = useTranslation();
const { modal } = App.useApp();
const [groups, setGroups] = useState<BlockContent[][]>([]);
const [title, setTitle] = useState('')
const [hotNews,setHotNews] = useState<HotNewsData>({
list: ['','',''],
const [title, setTitle] = useState('');
const [tag, setTag] = useState('');
const [backgroundImage, setBackgroundImage] = useState('1');
const [hotNews, setHotNews] = useState<HotNewsData>({
list: ['', '', ''],
mode: 'auto'
})
});
const [state, setState] = useSetState({
...DEFAULT_STATE,
generating:false,
pushed: false,
})
generating: false,
pushed: false
});
// 保存数据
const handleSave = async () => {
setState({error: ''})
setState({ error: '' });
if (!title) {
// setState({msgTitle: '请输入标题内容'});
return;
}
if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
setState({msgGroup: t('news.edit_empty_human_content')});
setState({ msgGroup: t('news.edit_empty_human_content') });
return;
}
// 验证图文都存在时,文图是否匹配
if(!checkGroupsValid(groups)) {
if (!checkGroupsValid(groups)) {
// 获取图文设置不正确的数据
setState({msgGroup: t('news.edit_empty_group_content')});
setState({ msgGroup: t('news.edit_empty_group_content') });
return;
}
const hotNewsValid = await checkHotNewsValid(hotNews,modal,t)
if(!hotNewsValid) return;
const hotNewsValid = await checkHotNewsValid(hotNews, modal, t);
if (!hotNewsValid) 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})
const save = props.type == 'news' ? article.save : regenerate;
setState({ loading: true });
save({
title,
metahuman_text: groups[0][0].content,
@ -153,83 +162,83 @@ export default function ArticleEditModal(props: Props) {
hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list,
id: props.id && props.id > 0 ? props.id : undefined
}).then(() => {
props.onClose?.(true)
props.onClose?.(true);
}).catch(e => {
setState({error: e.message || t('news.edit_save_failed')})
setState({ error: e.message || t('news.edit_save_failed') });
}).finally(() => {
setState({loading: false})
setState({ loading: false });
});
}
const handlePush2Video = async () =>{
if(state.pushed) return;
};
const handlePush2Video = async () => {
if (state.pushed) return;
if (!title) {
// setState({msgTitle: '请输入标题内容'});
return;
}
if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
setState({msgGroup: t('news.edit_empty_human_content')});
setState({ msgGroup: t('news.edit_empty_human_content') });
return;
}
// 验证图文都存在时,文图是否匹配
if(!checkGroupsValid(groups)) {
if (!checkGroupsValid(groups)) {
// 获取图文设置不正确的数据
setState({msgGroup: t('news.edit_empty_group_content')});
setState({ msgGroup: t('news.edit_empty_group_content') });
return;
}
if(!props.id || state.generating) return;
const hotNewsValid = await checkHotNewsValid(hotNews,modal,t)
if(!hotNewsValid) return;
setState({generating:true})
if (!props.id || state.generating) return;
const hotNewsValid = await checkHotNewsValid(hotNews, modal, t);
if (!hotNewsValid) return;
setState({ generating: true });
await article.save({
title,
metahuman_text: groups[0][0].content,
content_group: groups.slice(1),
hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list,
id: props.id,
})
id: props.id
});
push2video([props.id]).then(() => {
showToast(t('news.push_stream_success'), 'success')
setState({pushed:true})
props.onClose?.(true)
showToast(t('news.push_stream_success'), 'success');
setState({ pushed: true });
props.onClose?.(true);
// props.onRefresh?.();
// navigate('/create?state=push-success',{
// state: 'push-success'
// })
// props.onSuccess?.()
}).catch(showErrorToast).finally(()=>{
setState({generating:false})
})
}
}).catch(showErrorToast).finally(() => {
setState({ generating: false });
});
};
useEffect(() => {
setState({...DEFAULT_STATE})
setState({ ...DEFAULT_STATE });
if (typeof (props.id) != 'undefined') {
// 如果传入了id则获取数据
if (props.id > 0) {
article.getById(props.id).then(res => {
if(res.hot_news){
const len = res.hot_news.length
const list = len >= 3 ? res.hot_news :res.hot_news.concat(Array(3 - len).fill(''))
const mode = res.hot_news && res.hot_news.filter(s=>s.length > 0).length == 3 ?'manual':'auto';
if (res.hot_news) {
const len = res.hot_news.length;
const list = len >= 3 ? res.hot_news : res.hot_news.concat(Array(3 - len).fill(''));
const mode = res.hot_news && res.hot_news.filter(s => s.length > 0).length == 3 ? 'manual' : 'auto';
setHotNews({
list,
mode
})
});
}
setGroups(rebuildGroups([[{content: res.metahuman_text, type: "text"}], ...res.content_group]))
setTitle(res.title)
})
setGroups(rebuildGroups([[{ content: res.metahuman_text, type: 'text' }], ...res.content_group]));
setTitle(res.title);
});
} else {
// 新增
setGroups([])
setTitle('')
setGroups([]);
setTitle('');
}
}
}, [props.id])
}, [props.id]);
return (<Modal
title={null}
centered={true}
rootClassName={"article-edit-modal"}
rootClassName={'article-edit-modal'}
open={props.id != undefined && props.id >= 0}
maskClosable={false}
keyboard={false}
@ -237,32 +246,49 @@ export default function ArticleEditModal(props: Props) {
footer={null}
closeIcon={null}
onCancel={() => props.onClose?.()}
okButtonProps={{loading: state.loading}}
okButtonProps={{ loading: state.loading }}
onOk={handleSave}
okText={props.type == 'news' ? t('confirm_text') : t('news.edit_generate_video_again')}
>
<div className="article-title mt-5">
<div className="flex items-center">
<div className="mt-5 px-6 pt-10">
<div className="flex items-center pb-3 article-title">
<span className="mr-2 text-lg">{t('news.title')}</span>
<input className={'input-box text-lg flex-1'} value={title} onChange={e => {
setTitle(e.target.value)
setState({msgTitle: e.target.value ? '' : t('news.edit_notice_enter_article_title1')})
}} placeholder={t('news.edit_notice_enter_article_title')}/>
<input className={'input-box text-lg flex-1 py-2'} value={title} onChange={e => {
setTitle(e.target.value);
setState({ msgTitle: e.target.value ? '' : t('news.edit_notice_enter_article_title1') });
}} placeholder={t('news.edit_notice_enter_article_title')} />
</div>
<div className="text-red-500 mt-2">{state.msgTitle}</div>
</div>
<div className="article-body">
<div className="box">
<div className="box text-base">
<ArticleGroup
errorMessage={state.msgGroup}
editable
groups={groups}
hotNews={hotNews}
onChange={(list,hotNews) => {
setHotNews(hotNews)
setGroups(() => list)
setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? t('news.edit_empty_human_content') : ''});
onChange={(list, hotNews) => {
setHotNews(hotNews);
setGroups(() => list);
setState({ msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? t('news.edit_empty_human_content') : '' });
}}
leftPanelHeader={<div>
<div className="row tag flex items-center mt-2">
<span className="mr-2">{t('news.edit.tag')}</span>
<input className={'input-box flex-1 py-1.5'} value={tag} onChange={e => {
setTag(e.target.value);
}} placeholder={t('news.edit.tag_placeholder')} />
</div>
<div className="row bg flex items-center my-3">
<span className="mr-2">{t('news.edit.bg')}</span>
<div className="bg-radio-container">
<Radio.Group>
<Popover placement="bottomLeft" arrow={false} content={<img src={Bg1} />}><Radio value="1">1</Radio></Popover>
<Popover placement="bottomLeft" arrow={false} content={<img src={Bg2} />}><Radio value="2">2</Radio></Popover>
</Radio.Group>
</div>
</div>
</div>}
/>
<div className="text-red-500 mt-2">{state.msgGroup}</div>
</div>
@ -270,9 +296,11 @@ export default function ArticleEditModal(props: Props) {
</div>
<div className="modal-control-footer flex justify-end">
<div className="flex gap-10 ">
{props.type == 'news' && props.id ? <button className="text-gray-400 hover:text-gray-800" onClick={handlePush2Video}>{t('news.edit_generate_video')}{state.pushed?`${i18n.language == 'zh-CN'?'中':''}...`:(state.generating?`${i18n.language == 'zh-CN'?'推送中':'Pushing'}...`:'')}</button> : null}
{props.type == 'news' && props.id ? <button className="text-gray-400 hover:text-gray-800"
onClick={handlePush2Video}>{t('news.edit_generate_video')}{state.pushed ? `${i18n.language == 'zh-CN' ? '中' : ''}...` : (state.generating ? `${i18n.language == 'zh-CN' ? '推送中' : 'Pushing'}...` : '')}</button> : null}
<button className="text-gray-400 hover:text-gray-800" onClick={() => props.onClose?.()}>{t('cancel')}</button>
<button onClick={handleSave} className="text-gray-800 hover:text-blue-500">{props.type == 'news' ? t('news.save_text') : t('news.edit_generate_again')}</button>
<button onClick={handleSave}
className="text-gray-800 hover:text-blue-500">{props.type == 'news' ? t('news.save_text') : t('news.edit_generate_again')}</button>
</div>
</div>
</Modal>);

View File

@ -18,10 +18,11 @@ type Props = {
onChange?: (groups: BlockContent[][], hotNews: HotNewsData) => void;
errorMessage?: string;
hotNews: HotNewsData;
leftPanelHeader?: React.ReactNode;
}
export default function ArticleGroup({groups, editable, onChange, errorMessage, hotNews}: Props) {
export default function ArticleGroup({groups, editable, onChange, errorMessage, hotNews, leftPanelHeader}: Props) {
const {t, i18n} = useTranslation()
// const groups = rebuildGroups(_groups)
/**
@ -57,19 +58,21 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage,
return <div className={styles.group}>
<div className={'panel digital-person h-[544px]'}>
{leftPanelHeader}
<div className="area-title">
<span className="">{t('news.edit_digital_text')}</span>
{i18n.language == 'zh-CN' && <span className="text-gray-400"></span>}
</div>
<div className="panel-body p-3 flex-1 ">
<div className="panel-body p-3 flex-1 main-human-text">
{/* value={groups || groups[0][0].content}*/}
<div className={`pt-2 h-full rounded-xl overflow-hidden bg-gray-50`}>
<div className="human-tts">
{editable ? <div className="relative">
<div className={`h-full rounded-xl overflow-hidden bg-gray-50`}>
<div className="human-tts h-full">
{editable ? <div className="relative h-full">
<Input.TextArea
placeholder={t('news.edit_notice_enter_text')}
className="main-human-text-input"
value={groups && groups.length > 0 ? groups[0][0].content : ''}
autoSize={{maxRows: hotNews.mode == 'auto'?20:13}}
autoSize={{maxRows: hotNews.mode == 'auto'?15:8}}
variant={"borderless"}
onChange={e => {
handleDigitalPersonContentChange(e.target.value)

View File

@ -93,6 +93,11 @@
"delete_the_picture": "Are you sure delete the picture?",
"download_empty": "Please select the news to download",
"download_failed": "Download failed!",
"edit": {
"bg": "Background",
"tag": "Tag",
"tag_placeholder": "Example: Enterprise dynamics"
},
"edit_add_group": "Add Group",
"edit_delete_group": "Delete Group",
"edit_delete_group_confirm": "Are you sure you want to delete the group?",

View File

@ -93,6 +93,11 @@
"delete_the_picture": "请确认删除此图片",
"download_empty": "请选择要下载的新闻",
"download_failed": "下载新闻失败,请重试!",
"edit": {
"bg": "背景",
"tag": "标签",
"tag_placeholder": "例:企业动态"
},
"edit_add_group": "新增分组",
"edit_delete_group": "删除此分组",
"edit_delete_group_confirm": "请确认删除此分组?",

View File

@ -87,6 +87,7 @@ export default function LiveIndex() {
const playedTime = (Date.now() / 1000 >> 0) - liveState.live_start_time
if (playedTime < 0 || playedTime > duration) { // 已播放时间大于总时长了
//initPlayingState() // 重新获取播放状态
console.log('已播放时间大于总时长')
return;
}
player.current?.play(video.video_oss_url, playedTime)