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 { .article-title {
@apply px-6 pt-10 pb-6; border-bottom: 1px solid rgba(0,0,0,0.09);
} }
.article-body { .article-body {
@apply p-6 @apply p-6 pt-1;
} }
.modal-control-footer { .modal-control-footer {
@apply p-6 @apply p-6
} }
.hot-news-list{
@apply focus-within:bg-[#e6ebf1] focus-within:border-gray-100;
}
.input-box { .input-box {
// focus-within:shadow // 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; 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{ .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,15 +1,19 @@
import {Modal,App} from "antd"; import { Modal, App, Radio, Popover } from 'antd';
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from 'react';
import {useSetState} from "ahooks"; import { useSetState } from 'ahooks';
import {useTranslation} from "react-i18next"; import { useTranslation } from 'react-i18next';
import * as article from "@/service/api/article.ts"; import { TFunction } from 'i18next';
import {regenerate} from "@/service/api/video.ts";
import {push2video} from "@/service/api/article.ts"; import * as article from '@/service/api/article.ts';
import {showErrorToast, showToast} from "@/components/message.ts"; import { regenerate } from '@/service/api/video.ts';
import ArticleGroup, {HotNewsData} from "@/components/article/group.tsx"; import { push2video } from '@/service/api/article.ts';
import type {HookAPI as ModalHookAPI} from "antd/es/modal/useModal"; import { showErrorToast, showToast } from '@/components/message.ts';
import {TFunction} from "i18next"; import ArticleGroup, { HotNewsData } from '@/components/article/group.tsx';
import {IconWarningCircle} from "@/components/icons"; 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 = { type Props = {
id?: number; id?: number;
@ -24,15 +28,15 @@ const DEFAULT_STATE = {
msgTitle: '', msgTitle: '',
msgGroup: '', msgGroup: '',
error: '' error: ''
} };
function pushBlocksToGroup(blocks: BlockContent[], groups: BlockContent[][]) { 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) { if (lastGroup && lastGroup.filter(s => s.type == 'text').length == 0) {
// 如果上一个group中没有文本则直接合并 // 如果上一个group中没有文本则直接合并
lastGroup.push(...blocks) lastGroup.push(...blocks);
} else { } else {
groups.push(blocks) groups.push(blocks);
} }
} }
@ -43,46 +47,49 @@ function rebuildGroups(groups: BlockContent[][]) {
if (!blocks) return; if (!blocks) return;
blocks = blocks.filter(s => !!s).sort((a, b) => { blocks = blocks.filter(s => !!s).sort((a, b) => {
if (a.type == 'text' && b.type == 'text') return 1; 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 (blocks.length == 1) {
if (index == 0) _groups.push(blocks) if (index == 0) _groups.push(blocks);
else pushBlocksToGroup(blocks, _groups) else pushBlocksToGroup(blocks, _groups);
} else { } else {
if (index == 0) { if (index == 0) {
_groups.push([blocks[0]]) _groups.push([blocks[0]]);
_groups.push(blocks.slice(1)) _groups.push(blocks.slice(1));
} else { } else {
pushBlocksToGroup(blocks, _groups) pushBlocksToGroup(blocks, _groups);
} }
} }
}); });
if (_groups.length < 2) { if (_groups.length < 2) {
Array(2 - _groups.length).fill([{ type: 'text', content: '' }]).forEach((it) => { Array(2 - _groups.length).fill([{ type: 'text', content: '' }]).forEach((it) => {
_groups.push(it) _groups.push(it);
}) });
} }
// console.log('rebuildGroups', _groups) // console.log('rebuildGroups', _groups)
return _groups; return _groups;
} }
function groupHasImageAndText(blocks: BlockContent[]) { 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[][]) { function checkGroupsValid(_groups: BlockContent[][]) {
const groups = _groups.filter((_, index) => { const groups = _groups.filter((_, index) => {
if (index == 0) return true; 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; if (groups.length == 1) return true;
for (let index = 1; index < groups.length; index++) { for (let index = 1; index < groups.length; index++) {
if (!groupHasImageAndText(groups[index])) return false; if (!groupHasImageAndText(groups[index])) return false;
} }
return true; return true;
} }
function checkHotNewsValid(hotNews: HotNewsData,modal:ModalHookAPI,t:TFunction<"translation", undefined>) {
function checkHotNewsValid(hotNews: HotNewsData, modal: ModalHookAPI, t: TFunction<'translation', undefined>) {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
// 验证热点新闻数据是否正确 // 验证热点新闻数据是否正确
@ -94,36 +101,38 @@ function checkHotNewsValid(hotNews: HotNewsData,modal:ModalHookAPI,t:TFunction<"
content: <span dangerouslySetInnerHTML={{ __html: t('modal.hot_news.empty_notice_message') }}></span>, content: <span dangerouslySetInnerHTML={{ __html: t('modal.hot_news.empty_notice_message') }}></span>,
centered: true, centered: true,
onOk: () => { onOk: () => {
resolve(false) resolve(false);
}, },
onCancel: () => { onCancel: () => {
resolve(false) resolve(false);
} }
}) });
return; return;
} }
resolve(true) resolve(true);
}) });
} }
export default function ArticleEditModal(props: Props) { export default function ArticleEditModal(props: Props) {
const {t,i18n} = useTranslation() const { t, i18n } = useTranslation();
const {modal} = App.useApp() const { modal } = App.useApp();
const [groups, setGroups] = useState<BlockContent[][]>([]); const [groups, setGroups] = useState<BlockContent[][]>([]);
const [title, setTitle] = useState('') const [title, setTitle] = useState('');
const [tag, setTag] = useState('');
const [backgroundImage, setBackgroundImage] = useState('1');
const [hotNews, setHotNews] = useState<HotNewsData>({ const [hotNews, setHotNews] = useState<HotNewsData>({
list: ['', '', ''], list: ['', '', ''],
mode: 'auto' mode: 'auto'
}) });
const [state, setState] = useSetState({ const [state, setState] = useSetState({
...DEFAULT_STATE, ...DEFAULT_STATE,
generating: false, generating: false,
pushed: false, pushed: false
}) });
// 保存数据 // 保存数据
const handleSave = async () => { const handleSave = async () => {
setState({error: ''}) setState({ error: '' });
if (!title) { if (!title) {
// setState({msgTitle: '请输入标题内容'}); // setState({msgTitle: '请输入标题内容'});
return; return;
@ -138,14 +147,14 @@ export default function ArticleEditModal(props: Props) {
setState({ msgGroup: t('news.edit_empty_group_content') }); setState({ msgGroup: t('news.edit_empty_group_content') });
return; return;
} }
const hotNewsValid = await checkHotNewsValid(hotNews,modal,t) const hotNewsValid = await checkHotNewsValid(hotNews, modal, t);
if (!hotNewsValid) return; if (!hotNewsValid) return;
// if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) { // if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
// // setState({msgGroup: '请输入正文文本内容'}); // // setState({msgGroup: '请输入正文文本内容'});
// return; // return;
// } // }
const save = props.type == 'news' ? article.save : regenerate const save = props.type == 'news' ? article.save : regenerate;
setState({loading: true}) setState({ loading: true });
save({ save({
title, title,
metahuman_text: groups[0][0].content, metahuman_text: groups[0][0].content,
@ -153,13 +162,13 @@ export default function ArticleEditModal(props: Props) {
hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list, hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list,
id: props.id && props.id > 0 ? props.id : undefined id: props.id && props.id > 0 ? props.id : undefined
}).then(() => { }).then(() => {
props.onClose?.(true) props.onClose?.(true);
}).catch(e => { }).catch(e => {
setState({error: e.message || t('news.edit_save_failed')}) setState({ error: e.message || t('news.edit_save_failed') });
}).finally(() => { }).finally(() => {
setState({loading: false}) setState({ loading: false });
}); });
} };
const handlePush2Video = async () => { const handlePush2Video = async () => {
if (state.pushed) return; if (state.pushed) return;
if (!title) { if (!title) {
@ -177,59 +186,59 @@ export default function ArticleEditModal(props: Props) {
return; return;
} }
if (!props.id || state.generating) return; if (!props.id || state.generating) return;
const hotNewsValid = await checkHotNewsValid(hotNews,modal,t) const hotNewsValid = await checkHotNewsValid(hotNews, modal, t);
if (!hotNewsValid) return; if (!hotNewsValid) return;
setState({generating:true}) setState({ generating: true });
await article.save({ await article.save({
title, title,
metahuman_text: groups[0][0].content, metahuman_text: groups[0][0].content,
content_group: groups.slice(1), content_group: groups.slice(1),
hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list, hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list,
id: props.id, id: props.id
}) });
push2video([props.id]).then(() => { push2video([props.id]).then(() => {
showToast(t('news.push_stream_success'), 'success') showToast(t('news.push_stream_success'), 'success');
setState({pushed:true}) setState({ pushed: true });
props.onClose?.(true) props.onClose?.(true);
// props.onRefresh?.(); // props.onRefresh?.();
// navigate('/create?state=push-success',{ // navigate('/create?state=push-success',{
// state: 'push-success' // state: 'push-success'
// }) // })
// props.onSuccess?.() // props.onSuccess?.()
}).catch(showErrorToast).finally(() => { }).catch(showErrorToast).finally(() => {
setState({generating:false}) setState({ generating: false });
}) });
} };
useEffect(() => { useEffect(() => {
setState({...DEFAULT_STATE}) setState({ ...DEFAULT_STATE });
if (typeof (props.id) != 'undefined') { if (typeof (props.id) != 'undefined') {
// 如果传入了id则获取数据 // 如果传入了id则获取数据
if (props.id > 0) { if (props.id > 0) {
article.getById(props.id).then(res => { article.getById(props.id).then(res => {
if (res.hot_news) { if (res.hot_news) {
const len = res.hot_news.length const len = res.hot_news.length;
const list = len >= 3 ? res.hot_news :res.hot_news.concat(Array(3 - len).fill('')) 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'; const mode = res.hot_news && res.hot_news.filter(s => s.length > 0).length == 3 ? 'manual' : 'auto';
setHotNews({ setHotNews({
list, list,
mode mode
}) });
} }
setGroups(rebuildGroups([[{content: res.metahuman_text, type: "text"}], ...res.content_group])) setGroups(rebuildGroups([[{ content: res.metahuman_text, type: 'text' }], ...res.content_group]));
setTitle(res.title) setTitle(res.title);
}) });
} else { } else {
// 新增 // 新增
setGroups([]) setGroups([]);
setTitle('') setTitle('');
} }
} }
}, [props.id]) }, [props.id]);
return (<Modal return (<Modal
title={null} title={null}
centered={true} centered={true}
rootClassName={"article-edit-modal"} rootClassName={'article-edit-modal'}
open={props.id != undefined && props.id >= 0} open={props.id != undefined && props.id >= 0}
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
@ -241,28 +250,45 @@ export default function ArticleEditModal(props: Props) {
onOk={handleSave} onOk={handleSave}
okText={props.type == 'news' ? t('confirm_text') : t('news.edit_generate_video_again')} okText={props.type == 'news' ? t('confirm_text') : t('news.edit_generate_video_again')}
> >
<div className="article-title mt-5"> <div className="mt-5 px-6 pt-10">
<div className="flex items-center"> <div className="flex items-center pb-3 article-title">
<span className="mr-2 text-lg">{t('news.title')}</span> <span className="mr-2 text-lg">{t('news.title')}</span>
<input className={'input-box text-lg flex-1'} value={title} onChange={e => { <input className={'input-box text-lg flex-1 py-2'} value={title} onChange={e => {
setTitle(e.target.value) setTitle(e.target.value);
setState({msgTitle: e.target.value ? '' : t('news.edit_notice_enter_article_title1')}) setState({ msgTitle: e.target.value ? '' : t('news.edit_notice_enter_article_title1') });
}} placeholder={t('news.edit_notice_enter_article_title')} /> }} placeholder={t('news.edit_notice_enter_article_title')} />
</div> </div>
<div className="text-red-500 mt-2">{state.msgTitle}</div> <div className="text-red-500 mt-2">{state.msgTitle}</div>
</div> </div>
<div className="article-body"> <div className="article-body">
<div className="box"> <div className="box text-base">
<ArticleGroup <ArticleGroup
errorMessage={state.msgGroup} errorMessage={state.msgGroup}
editable editable
groups={groups} groups={groups}
hotNews={hotNews} hotNews={hotNews}
onChange={(list, hotNews) => { onChange={(list, hotNews) => {
setHotNews(hotNews) setHotNews(hotNews);
setGroups(() => list) setGroups(() => list);
setState({ msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? t('news.edit_empty_human_content') : '' }); 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 className="text-red-500 mt-2">{state.msgGroup}</div>
</div> </div>
@ -270,9 +296,11 @@ export default function ArticleEditModal(props: Props) {
</div> </div>
<div className="modal-control-footer flex justify-end"> <div className="modal-control-footer flex justify-end">
<div className="flex gap-10 "> <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 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>
</div> </div>
</Modal>); </Modal>);

View File

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

View File

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

View File

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

View File

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