feat: ️ 新增新闻热点功能,支持手动/自动填充热点内容并完善相关验证逻辑

This commit is contained in:
LittleBoy 2025-04-06 20:26:24 +08:00
parent de7088f642
commit e61bfcc26c
9 changed files with 257 additions and 98 deletions

View File

@ -0,0 +1,61 @@
import styles from './article.module.scss'
import {useTranslation} from "react-i18next";
import {Input, Switch} from "antd";
import {useMemo} from "react";
type HotNewsProps = {
news: string[];
mode: string;
onValueChange: (values: {
news: string[],
mode: string
}) => void;
}
function HotNews({news, mode, onValueChange}: HotNewsProps) {
const {t,i18n} = useTranslation()
const demoPlaceholderList = useMemo(()=>{
return i18n.language == 'zh-CN' ? [
'例:韩正会见英国汇丰集团主席',
'例: 丁薛祥出席全国高校毕业生等青年就业创业工作视频...',
'例:俄称乌方再度袭击俄能源设施 乌称击退俄军进攻',
] : [
'please type hot news',
'please type hot news',
'please type hot news',
]
},[i18n.language])
const handleValueChange = (value: string, index: number) => {
const values = [...news]
values[index] = value
onValueChange({news: values, mode})
}
return (
<div className={`${styles.hotNews} mt-3`}>
<div className="flex justify-between">
<div className="area-title">
<span className="title">{t("modal.hot_news.title")}</span>
</div>
<div className="mode">
<span className="mr-2">{mode == 'auto' ? t("modal.hot_news.edit_auto") : t("modal.hot_news.edit_manual")}</span>
<Switch size="small" checked={mode == 'auto'} onChange={checked => {
onValueChange({news, mode: checked ? 'auto' : 'manual'})
}}/>
</div>
</div>
<div className="hot-news-list panel-body p-3 ">
{news.map((item, index) => <div key={index} className={`hot-news-item bg-gray-50 ${index == 0?'':'mt-3'} rounded-xl`}>
<Input
variant={"borderless"}
readOnly={mode == 'auto'}
placeholder={mode != 'auto' ? demoPlaceholderList[index] : ''}
value={item}
onChange={e => handleValueChange(e.target.value, index)}/>
</div>)}
</div>
</div>
)
}
export default HotNews

View File

@ -46,7 +46,7 @@
@apply flex gap-4;
:global{
.area-title{
@apply text-gray-400 text-sm text-gray-800;
@apply text-gray-400 text-base text-gray-800;
}
.digital-person{
width: 450px;
@ -132,4 +132,10 @@
.textarea {
@apply border-0
}
// hot news
.hotNews{
.title{}
}

View File

@ -1,12 +1,15 @@
import {Modal} from "antd";
import ArticleGroup from "@/components/article/group.tsx";
import {useEffect, useState} from "react";
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 {useTranslation} from "react-i18next";
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";
type Props = {
id?: number;
@ -70,7 +73,7 @@ function groupHasImageAndText(blocks: BlockContent[]) {
function checkGroupsValid(_groups: BlockContent[][]) {
const groups = _groups.filter((_,index)=>{
if (index == 0) return true;
return _.length>1;
return _.length > 1 || (_.length == 1 && _[0].content.trim().length > 0) ;
})
if (groups.length == 1) return true;
for (let index = 1;index< groups.length; index ++) {
@ -78,19 +81,46 @@ function checkGroupsValid(_groups: BlockContent[][]) {
}
return true;
}
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){
modal.confirm({
wrapClassName: 'root-modal-confirm',
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
title: t('modal.hot_news.empty_notice_title'),
content: t('modal.hot_news.empty_notice_message'),
centered:true,
onOk: () => {
resolve(true)
},
onCancel: () => {
resolve(false)
}
})
return;
}
resolve(true)
})
}
export default function ArticleEditModal(props: Props) {
const {t} = useTranslation()
const {modal} = App.useApp()
const [groups, setGroups] = useState<BlockContent[][]>([]);
const [title, setTitle] = useState('')
const [hotNews,setHotNews] = useState<HotNewsData>({
list: ['','',''],
mode: 'auto'
})
const [state, setState] = useSetState({
...DEFAULT_STATE,
generating:false
})
// 保存数据
const handleSave = () => {
const handleSave = async () => {
setState({error: ''})
if (!title) {
// setState({msgTitle: '请输入标题内容'});
@ -106,13 +136,21 @@ export default function ArticleEditModal(props: Props) {
setState({msgGroup: t('news.edit_empty_group_content')});
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})
save(title, groups[0][0].content, groups.slice(1), props.id && props.id > 0 ? props.id : undefined).then(() => {
save({
title,
metahuman_text: groups[0][0].content,
content_group: groups.slice(1),
hot_news: hotNews.list,
id: props.id && props.id > 0 ? props.id : undefined
}).then(() => {
props.onClose?.(true)
}).catch(e => {
setState({error: e.message || t('news.edit_save_failed')})
@ -121,6 +159,7 @@ export default function ArticleEditModal(props: Props) {
});
}
const handlePush2Video = async () =>{
//
if (!title) {
// setState({msgTitle: '请输入标题内容'});
return;
@ -136,8 +175,16 @@ export default function ArticleEditModal(props: Props) {
return;
}
if(!props.id || state.generating) return;
const hotNewsValid = await checkHotNewsValid(hotNews,modal,t)
if(!hotNewsValid) return;
setState({generating:true})
await article.save(title, groups[0][0].content, groups.slice(1), props.id)
await article.save({
title,
metahuman_text: groups[0][0].content,
content_group: groups.slice(1),
hot_news: hotNews.list,
id: props.id,
})
push2video([props.id]).then(() => {
showToast(t('news.push_stream_success'), 'success')
// navigate('/create?state=push-success',{
@ -154,6 +201,13 @@ export default function ArticleEditModal(props: Props) {
// 如果传入了id则获取数据
if (props.id > 0) {
article.getById(props.id).then(res => {
const len = res.hot_news.length
const list = len >= 3 ? res.hot_news :res.hot_news.concat(Array(3 - len).fill(''))
console.log('list,',list,res.hot_news)
setHotNews({
list,
mode: res.hot_news_mode ?? 'auto'
})
setGroups(rebuildGroups([[{content: res.metahuman_text, type: "text"}], ...res.content_group]))
setTitle(res.title)
})
@ -190,8 +244,12 @@ export default function ArticleEditModal(props: Props) {
<div className="article-body">
<div className="box">
<ArticleGroup
errorMessage={state.msgGroup} editable groups={groups}
onChange={list => {
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') : ''});
}}

View File

@ -6,23 +6,29 @@ import {showToast} from "@/components/message.ts";
import React from "react";
import {useTranslation} from "react-i18next";
import {IconAdd} from "@/components/icons";
import HotNews from "@/components/article/HotNews.tsx";
export type HotNewsData = {
list: string[];
mode: string
}
type Props = {
groups: BlockContent[][];
editable?: boolean;
onChange?: (groups: BlockContent[][]) => void;
onChange?: (groups: BlockContent[][], hotNews: HotNewsData) => void;
errorMessage?: string;
hotNews: HotNewsData;
}
export default function ArticleGroup({groups, editable, onChange, errorMessage}: Props) {
const {t,i18n} = useTranslation()
export default function ArticleGroup({groups, editable, onChange, errorMessage, hotNews}: Props) {
const {t, i18n} = useTranslation()
// const groups = rebuildGroups(_groups)
/**
*
* @param insertIndex -1
*/
const handleAddGroup = (insertIndex: number,checkId:number) => {
const handleAddGroup = (insertIndex: number, checkId: number) => {
// && insertIndex !== 1
if (checkId > 0 && checkId < groups.length) {
//const index = insertIndex == -1 || insertIndex >= groups.length ? groups.length - 1 : insertIndex - 1
@ -41,12 +47,12 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
} else {
_groups.splice(insertIndex, 0, newGroup)
}
onChange?.(_groups)
onChange?.(_groups, hotNews)
}
const handleDigitalPersonContentChange = (content:string) => {
const handleDigitalPersonContentChange = (content: string) => {
groups[0] = [{type: 'text', content}]
onChange?.([...groups])
onChange?.([...groups], hotNews)
}
return <div className={styles.group}>
@ -57,20 +63,31 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
</div>
<div className="panel-body p-3">
{/* value={groups || groups[0][0].content}*/}
<div className="h-[486px] pt-2 rounded-xl overflow-hidden bg-gray-50">
{editable ? <div className="relative">
<Input.TextArea
placeholder={t('news.edit_notice_enter_text')}
value={groups && groups.length > 0 ? groups[0][0].content : ''}
autoSize={{minRows: 20, maxRows: 21}}
variant={"borderless"}
onChange={e => {
handleDigitalPersonContentChange(e.target.value)
}}
/>
</div> : <p className="p-2">{groups && groups.length > 0 ? groups[0][0].content : ''}</p>}
<div className="h-[306px] pt-2 rounded-xl overflow-hidden bg-gray-50">
<div className="human-tts">
{editable ? <div className="relative">
<Input.TextArea
placeholder={t('news.edit_notice_enter_text')}
value={groups && groups.length > 0 ? groups[0][0].content : ''}
autoSize={{minRows: 20, maxRows: 21}}
variant={"borderless"}
onChange={e => {
handleDigitalPersonContentChange(e.target.value)
}}
/>
</div> : <p className="p-2">{groups && groups.length > 0 ? groups[0][0].content : ''}</p>}
</div>
</div>
</div>
<div className="hot-news-container">
<HotNews
news={hotNews.list} mode={hotNews.mode}
onValueChange={(hotNews) => {
onChange?.([...groups], {
list:hotNews.news,mode: hotNews.mode
})
}}/>
</div>
</div>
<div className={"panel groups-list flex-1"}>
<div className={"area-title"}>
@ -81,9 +98,12 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
<div className="panel-body py-3">
<div className="max-h-[485px] overflow-auto py-4">
{editable && groups.length == 1 && <div className={`${styles.blockContainer} group`}><div className={'divider-container before'}><Divider>
<span onClick={()=>handleAddGroup?.(1,1)} className="article-action-add" title={t('news.materials.add_group')}><IconAdd style={{fontSize: 24}}/></span>
</Divider></div></div> }
{editable && groups.length == 1 && <div className={`${styles.blockContainer} group`}>
<div className={'divider-container before'}><Divider>
<span onClick={() => handleAddGroup?.(1, 1)} className="article-action-add"
title={t('news.materials.add_group')}><IconAdd style={{fontSize: 24}}/></span>
</Divider></div>
</div>}
{groups.map((g, index) => (
index == 0 ? null : <ArticleBlock
@ -92,20 +112,20 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
blocks={g}
onChange={(blocks) => {
groups[index] = blocks
onChange?.([...groups])
onChange?.([...groups], hotNews)
}}
errorMessage={errorMessage}
index={index}
onAdd={(_index,checkIndex) => {
handleAddGroup?.(_index ? _index :index + 1,checkIndex)
onAdd={(_index, checkIndex) => {
handleAddGroup?.(_index ? _index : index + 1, checkIndex)
}}
disableRemoveMessage={groups.length <= 1?t('news.edit_notice_keep_1'):''}
disableRemoveMessage={groups.length <= 1 ? t('news.edit_notice_keep_1') : ''}
onRemove={async () => {
if (groups.length <= 1) {
message.warning(t('news.edit_notice_keep_1'))
return;
}
onChange?.(groups.filter((_, idx) => index !== idx))
onChange?.(groups.filter((_, idx) => index !== idx), hotNews)
}}
/>
))}
@ -113,7 +133,7 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
</div>
</div>
{groups.length == 0 && editable &&
<ArticleBlock editable onChange={blocks => onChange?.([blocks])} index={0}
blocks={[{type: 'text', content: ''}]}/>}
<ArticleBlock editable onChange={blocks => onChange?.([blocks],hotNews)} index={0}
blocks={[{type: 'text', content: ''}]}/>}
</div>
}

View File

@ -1,14 +1,8 @@
{
"AppTitle": "AI Livesteam",
"go_to_home": "Go to Homepage" ,
"Hello": "Hello",
"cancel": "Cancel",
"close": "Close",
"service_error": "Service exception, please contact customer support.",
"error_401": "You do not have permission to access this page",
"error_403": "You do not have permission to access this page",
"error_404": "Page not found",
"error_500": "Service exception, please contact customer support.",
"confirm": {
"push_title": "Push Notice",
"push_video": "Are you sure editing selected news?",
@ -20,9 +14,14 @@
"delete_failed": "Delete failed",
"delete_success": "Delete success",
"download": "Download",
"error_401": "You do not have permission to access this page",
"error_403": "You do not have permission to access this page",
"error_404": "Page not found",
"error_500": "Service exception, please contact customer support.",
"generating": {
"title": "Preview - Click the video to play"
},
"go_to_home": "Go to Homepage",
"history": {
"delete_confirm": "Are you sure you want to delete this video?",
"push_success": "Streaming success",
@ -49,6 +48,26 @@
"username": "Please enter your phone number",
"welcome": "Welcome"
},
"modal": {
"hot_news": {
"edit_auto": "Smart",
"edit_manual": "Manual",
"empty_notice_message": "\"Hot News\" has not been filled in yet. Should it be filled in automatically by the system?",
"empty_notice_title": "Notice",
"title": "Hot news"
},
"push_article": {
"action_all": "Still generating",
"action_cancel": "Cancel",
"action_skip": "Skip the news",
"content_error": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, and <span class=\"modal-count-warning\">{{error_count}}</span> metahuman contents are too short in these news below. Do you want to transfer them to videos?",
"content_error_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected, and the metahuman content is too short in this news. Do you want to transfer it to a video?",
"content_normal": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, Do you want to transfer them into videos?",
"content_normal_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected. Do you want to transfer it to a video?",
"error_title": "Abnormal news"
},
"warning": "Warning"
},
"nav": {
"editing": "Editing",
"generating": "Generating",
@ -89,8 +108,8 @@
"get_detail_error": "Get new details failed",
"image_count": "Images",
"materials": {
"title": "News Materials",
"add_group": "Add Group"
"add_group": "Add Group",
"title": "News Materials"
},
"news_all_source": "All",
"push_empty": "please select the news to edit",
@ -118,6 +137,7 @@
"text": "Select",
"total": "Total: {{count}}"
},
"service_error": "Service exception, please contact customer support.",
"time_filter": {
"all": "All",
"last_week": "Last week",
@ -142,8 +162,8 @@
"delete_description_count": "Are you sure you want to delete these {{count}} videos?",
"delete_empty": "Select the video you want to delete",
"download": "Download",
"generating": "Generating",
"generate_failed": "Generate Failed",
"generating": "Generating",
"playing": "Playing",
"push_confirm": "Are you sure you want to streaming these video?",
"push_empty": "Select the video you want to streaming",
@ -159,18 +179,5 @@
"title_generated_time": "Time stamp",
"title_operation": "",
"title_thumb": "Cover"
},
"modal": {
"warning": "Warning",
"push_article": {
"content_normal": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, Do you want to transfer them into videos?",
"content_normal_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected. Do you want to transfer it to a video?",
"content_error": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, and <span class=\"modal-count-warning\">{{error_count}}</span> metahuman contents are too short in these news below. Do you want to transfer them to videos?",
"content_error_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected, and the metahuman content is too short in this news. Do you want to transfer it to a video?",
"error_title": "Abnormal news",
"action_cancel": "Cancel",
"action_skip": "Skip the news",
"action_all": "Still generating"
}
}
}

View File

@ -1,14 +1,8 @@
{
"AppTitle": "数字人直播",
"go_to_home": "返回首页" ,
"Hello": "你好",
"cancel": "取消",
"close": "关闭",
"service_error": "新闻异常,无法生成,请咨询客服",
"error_401": "您没有权限访问本页面",
"error_403": "您没有权限访问本页面",
"error_404": "访问的页面不存在",
"error_500": "服务异常,请咨询客服.",
"confirm": {
"push_title": "推流提示",
"push_video": "是否确定一键推流选中新闻视频?",
@ -20,9 +14,14 @@
"delete_failed": "删除失败",
"delete_success": "删除成功",
"download": "下载",
"error_401": "您没有权限访问本页面",
"error_403": "您没有权限访问本页面",
"error_404": "访问的页面不存在",
"error_500": "服务异常,请咨询客服.",
"generating": {
"title": "预览视频 - 点击视频列表播放"
},
"go_to_home": "返回首页",
"history": {
"delete_confirm": "是否要删除该视频",
"push_success": "一键推流成功,已推流至数字人直播间,请查看!",
@ -49,6 +48,26 @@
"username": "请输入账号",
"welcome": "欢迎登录"
},
"modal": {
"hot_news": {
"edit_auto": "智能填充",
"edit_manual": "手动编辑",
"empty_notice_message": "“新闻热点”尚未填写,是否由系统自动填充?",
"empty_notice_title": "操作提示",
"title": "新闻热点"
},
"push_article": {
"action_all": "全部生成",
"action_cancel": "全部取消",
"action_skip": "跳过异常新闻",
"content_error": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
"content_error_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
"content_normal": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否全部生成?",
"content_normal_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否生成?",
"error_title": "异常新闻"
},
"warning": "操作提示"
},
"nav": {
"editing": "新闻编辑",
"generating": "视频生成",
@ -89,8 +108,8 @@
"get_detail_error": "获取新闻详情失败",
"image_count": "图片数",
"materials": {
"title": "新闻素材",
"add_group": "新增分组"
"add_group": "新增分组",
"title": "新闻素材"
},
"news_all_source": "全部来源",
"push_empty": "请选择要推入编辑的新闻",
@ -118,6 +137,7 @@
"text": "选择",
"total": "总共 {{count}} 条"
},
"service_error": "新闻异常,无法生成,请咨询客服",
"time_filter": {
"all": "所有时间",
"last_week": "近一周",
@ -142,8 +162,8 @@
"delete_description_count": "已选择{{count}}条,确定要全部删除吗?",
"delete_empty": "请选择要删除的视频",
"download": "下载视频",
"generating": "生成中",
"generate_failed": "生成失败",
"generating": "生成中",
"playing": "播放中",
"push_confirm": "是否确定一键推流选中新闻视频?",
"push_empty": "请选择要推流的新闻视频",
@ -159,18 +179,5 @@
"title_generated_time": "生成时间",
"title_operation": "操作",
"title_thumb": "缩略图"
},
"modal": {
"warning": "操作提示",
"push_article": {
"content_normal": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否全部生成?",
"content_normal_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否生成?",
"content_error": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
"content_error_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
"error_title": "异常新闻",
"action_cancel": "全部取消",
"action_skip": "跳过异常新闻",
"action_all": "全部生成"
}
}
}

View File

@ -20,13 +20,8 @@ export function getById(id: Id) {
return post<ArticleDetail>({url: '/article/detail/' + id})
}
export function save(title: string, metahuman_text: string, content_group: BlockContent[][], id?: number) {
return post<{ content: string }>(id && id > 0 ? '/article/modify' : '/article/create/new', {
title,
metahuman_text,
content_group,
id
})
export function save(params:{title: string, metahuman_text: string, content_group: BlockContent[][],hot_news: string[], id?: number}) {
return post<{ content: string }>(params.id && params.id > 0 ? '/article/modify' : '/article/create/new',params)
}
export function push2video(article_ids: Id[]) {

View File

@ -17,21 +17,24 @@ export function deleteHistories(ids: Id[]) {
* @param content_group
* @param article_id
*/
export function regenerate(title: string, metahuman_text: string, content_group: BlockContent[][], article_id?: Id) {
export function regenerate(params:{title: string, metahuman_text: string, content_group: BlockContent[][], id?: Id}) {
return post<{ content: string }>({
url: '/video/regenerate',
data: {
title,
metahuman_text,
content_group,
article_id
...params,
article_id:params.id
}
})
}
// 重新生成视频
export async function regenerateById(article_id: Id) {
const article = await getArticle(article_id);
return await regenerate(article.title, article.metahuman_text, article.content_group, article_id)
return await regenerate({
title:article.title,
metahuman_text:article.metahuman_text,
content_group:article.content_group,
id:article_id
})
}
export function getById(id: Id) {

2
src/types/core.d.ts vendored
View File

@ -32,6 +32,8 @@ declare interface ArticleDetail {
id: number;
title: string;
metahuman_text: string;
hot_news_mode?: string;
hot_news: string[]; // 4月 6 日新增
content_group: BlockContent[][]
}