Compare commits
10 Commits
a1bae30e2d
...
e022bc8036
Author | SHA1 | Date | |
---|---|---|---|
e022bc8036 | |||
71e90e7edd | |||
5b791716e2 | |||
1b72d9d4f5 | |||
a478c9dd09 | |||
a8b672037c | |||
08e4de4a90 | |||
|
213074760d | ||
|
3161a5ee27 | ||
|
68691c0e54 |
@ -17,6 +17,11 @@ npm run build
|
||||
```
|
||||
生成的资源在dist目录中,将此目录中所有文件放置在待部署web目录即可。
|
||||
|
||||
#### 直播页面
|
||||
1、直接正常部署后,使用访问 /live.html 即可
|
||||
|
||||
2、单独域名部署,设置环境变量ONLY_LIVE=yes,使用正常编译即可
|
||||
|
||||
**使用docker**
|
||||
|
||||
[x] TODO
|
||||
|
52
public/live.html
Normal file
52
public/live.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>数字人直播</title>
|
||||
<link href="https://web.sdk.qcloud.com/player/tcplayer/release/v5.2.0/tcplayer.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
*, :before, :after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
}
|
||||
body{
|
||||
margin: 0;
|
||||
}
|
||||
.wrapper{}
|
||||
.wrapper .video-js{
|
||||
max-width: 480px;
|
||||
margin: auto;
|
||||
height: 100vh;
|
||||
}
|
||||
.wrapper video{
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<video id="player-container" width="414" height="270" preload="auto" playsinline webkit-playsinline>
|
||||
</video>
|
||||
</div>
|
||||
<!--播放器脚本文件-->
|
||||
<script src="https://web.sdk.qcloud.com/player/tcplayer/release/v5.2.0/tcplayer.v5.2.0.min.js"></script>
|
||||
<script>
|
||||
function init(){
|
||||
fetch('/api/v1/tencent/get_pull_url').then(r=>r.json()).then(ret=>{
|
||||
console.log(ret)
|
||||
TCPlayer('player-container', {
|
||||
sources: [{
|
||||
src: ret.data.flv_url, // 播放地址
|
||||
}],
|
||||
licenseUrl: 'https://license.vod2.myqcloud.com/license/v2/1328581896_1/v_cube.license', // license 地址,必传。参考准备工作部分,在视立方控制台申请 license 后可获得 licenseUrl
|
||||
});
|
||||
})
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,15 +1,16 @@
|
||||
import AppRouter from "@/routes";
|
||||
import {ConfigProvider} from "@/contexts/config";
|
||||
import {AuthProvider} from "@/contexts/auth";
|
||||
import LivePlayer from "@/pages/live-player";
|
||||
|
||||
console.log(`APP-BUILD-AT: ${AppBuildVersion}`)
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<AuthProvider>
|
||||
{AppConfig.ONLY_LIVE && AppConfig.ONLY_LIVE == 'yes'?<LivePlayer />:<AuthProvider>
|
||||
<AppRouter/>
|
||||
</AuthProvider>
|
||||
</AuthProvider>}
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
@ -164,6 +164,7 @@
|
||||
@apply text-sm;
|
||||
background: none;
|
||||
.col{
|
||||
@apply text-sm text-gray-800;
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ body {
|
||||
font-size: 16px;
|
||||
font-family: -apple-system, "PingFang SC", 'Microsoft YaHei', sans-serif;
|
||||
background-color: var(--main-bg-color);
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
.dashboard-layout {
|
||||
|
@ -2,11 +2,12 @@ import React from "react";
|
||||
import clsx from "clsx";
|
||||
import {Divider, Popconfirm} from "antd";
|
||||
|
||||
import {IconAdd, IconAddCircle, IconDelete, IconWarningCircle} from "@/components/icons";
|
||||
import {IconAdd, IconDelete, IconWarningCircle} from "@/components/icons";
|
||||
import ImageList from "@/components/article/list.tsx";
|
||||
|
||||
import { BlockText} from "./item.tsx";
|
||||
import styles from './article.module.scss'
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
@ -49,6 +50,7 @@ export default function ArticleBlock(
|
||||
onChange,
|
||||
index,
|
||||
}: Props) {
|
||||
const {t} = useTranslation()
|
||||
const blocks = rebuildBlockArray(defaultBlocks)
|
||||
|
||||
const handleBlockChange = (index: number, block: BlockContent) => {
|
||||
@ -59,7 +61,7 @@ export default function ArticleBlock(
|
||||
|
||||
return <div className={`${styles.blockContainer} group`}>
|
||||
{editable && index == 1 && <div className={'divider-container before'}><Divider>
|
||||
<span onClick={()=>onAdd?.(1)} className="article-action-add" title="新增分组"><IconAdd style={{fontSize: 24}}/></span>
|
||||
<span onClick={()=>onAdd?.(1)} className="article-action-add" title={t('news.edit_add_group')}><IconAdd style={{fontSize: 24}}/></span>
|
||||
</Divider></div> }
|
||||
<div className={styles.blockInner}>
|
||||
<div className={clsx(className || '', styles.block, index == 0 ? styles.blockFist : '', ' hover:bg-blue-10')}>
|
||||
@ -83,12 +85,12 @@ export default function ArticleBlock(
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={<div style={{minWidth: 150}}><span>请确认删除此分组?</span></div>}
|
||||
title={<div style={{minWidth: 150}}><span>{t('news.edit_delete_group_confirm')}</span></div>}
|
||||
onConfirm={onRemove}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
okText={t('delete')}
|
||||
cancelText={t('cancel')}
|
||||
>
|
||||
<span className="article-action-icon hidden group-hover:block ml-1" title="删除此分组">
|
||||
<span className="article-action-icon hidden group-hover:block ml-1" title={t('news.edit_delete_group')}>
|
||||
<IconDelete style={{fontSize: 24}}/>
|
||||
</span>
|
||||
</Popconfirm> : <span></span>
|
||||
|
@ -6,6 +6,7 @@ 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";
|
||||
|
||||
type Props = {
|
||||
id?: number;
|
||||
@ -64,7 +65,7 @@ function rebuildGroups(groups: BlockContent[][]) {
|
||||
}
|
||||
|
||||
export default function ArticleEditModal(props: Props) {
|
||||
|
||||
const {t} = useTranslation()
|
||||
const [groups, setGroups] = useState<BlockContent[][]>([]);
|
||||
const [title, setTitle] = useState('')
|
||||
|
||||
@ -85,19 +86,28 @@ export default function ArticleEditModal(props: Props) {
|
||||
}
|
||||
const save = props.type == 'news' ? article.save : regenerate
|
||||
setState({loading: true})
|
||||
save(title, groups, props.id && props.id > 0 ? props.id : undefined).then(() => {
|
||||
save(title, groups[0][0].content, groups.slice(1), props.id && props.id > 0 ? props.id : undefined).then(() => {
|
||||
props.onClose?.(true)
|
||||
}).catch(e => {
|
||||
setState({error: e.data || '保存失败,请重试!'})
|
||||
setState({error: e.data || t('news.edit_save_failed')})
|
||||
}).finally(() => {
|
||||
setState({loading: false})
|
||||
});
|
||||
}
|
||||
const handlePush2Video = () =>{
|
||||
const handlePush2Video = async () =>{
|
||||
if (!title) {
|
||||
// setState({msgTitle: '请输入标题内容'});
|
||||
return;
|
||||
}
|
||||
if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
|
||||
// setState({msgGroup: '请输入正文文本内容'});
|
||||
return;
|
||||
}
|
||||
if(!props.id || state.generating) return;
|
||||
setState({generating:true})
|
||||
await article.save(title, groups[0][0].content, groups.slice(1), props.id)
|
||||
push2video([props.id]).then(() => {
|
||||
showToast('推流成功', 'success')
|
||||
showToast(t('news.push_stream_success'), 'success')
|
||||
// navigate('/create?state=push-success',{
|
||||
// state: 'push-success'
|
||||
// })
|
||||
@ -111,7 +121,7 @@ export default function ArticleEditModal(props: Props) {
|
||||
if (typeof (props.id) != 'undefined') {
|
||||
if (props.id > 0) {
|
||||
article.getById(props.id).then(res => {
|
||||
setGroups(rebuildGroups(res.content_group))
|
||||
setGroups(rebuildGroups([[{content: res.metahuman_text, type: "text"}], ...res.content_group]))
|
||||
setTitle(res.title)
|
||||
})
|
||||
} else {
|
||||
@ -135,13 +145,13 @@ export default function ArticleEditModal(props: Props) {
|
||||
onCancel={() => props.onClose?.()}
|
||||
okButtonProps={{loading: state.loading}}
|
||||
onOk={handleSave}
|
||||
okText={props.type == 'news' ? '确定' : '重新生成'}
|
||||
okText={props.type == 'news' ? t('confirm') : t('news.edit_generate_video_again')}
|
||||
>
|
||||
<div className="article-title mt-5">
|
||||
<input className={'input-box text-lg'} value={title} onChange={e => {
|
||||
setTitle(e.target.value)
|
||||
setState({msgTitle: e.target.value ? '' : '请输入标题内容'})
|
||||
}} placeholder={'请输入文章标题'}/>
|
||||
setState({msgTitle: e.target.value ? '' : t('news.edit_notice_enter_article_title1')})
|
||||
}} placeholder={t('news.edit_notice_enter_article_title')}/>
|
||||
<div className="text-red-500">{state.msgTitle}</div>
|
||||
</div>
|
||||
<div className="article-body">
|
||||
@ -150,7 +160,7 @@ export default function ArticleEditModal(props: Props) {
|
||||
errorMessage={state.msgGroup} editable groups={groups}
|
||||
onChange={list => {
|
||||
setGroups(() => list)
|
||||
setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? '请输入正文文本内容' : ''});
|
||||
setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? t('news.edit_notice_enter_article_content') : ''});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -158,9 +168,9 @@ 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}>{state.generating?'推送中...':'生成视频'}</button> : null}
|
||||
<button className="text-gray-400 hover:text-gray-800" onClick={() => props.onClose?.()}>取消</button>
|
||||
<button onClick={handleSave} className="text-gray-800 hover:text-blue-500">{props.type == 'news' ? '确定' : '重新生成'}</button>
|
||||
{props.type == 'news' && props.id ? <button className="text-gray-400 hover:text-gray-800" onClick={handlePush2Video}>{t('news.edit_generate_video')}{state.generating?'推送中...':''}</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('confirm') : t('news.edit_generate_again')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>);
|
||||
|
@ -4,6 +4,7 @@ import ArticleBlock from "@/components/article/block.tsx";
|
||||
import styles from './article.module.scss'
|
||||
import {showToast} from "@/components/message.ts";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
groups: BlockContent[][];
|
||||
@ -14,6 +15,7 @@ type Props = {
|
||||
|
||||
|
||||
export default function ArticleGroup({groups, editable, onChange, errorMessage}: Props) {
|
||||
const {t,i18n} = useTranslation()
|
||||
// const groups = rebuildGroups(_groups)
|
||||
/**
|
||||
* 添加一个组
|
||||
@ -24,7 +26,7 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
|
||||
const triggerGroup = insertIndex == -1 || insertIndex >= groups.length ? groups[groups.length - 1] : groups[insertIndex - 1];
|
||||
// 判断
|
||||
if (triggerGroup.length == 0 || triggerGroup.some(s => !s.content)) {
|
||||
showToast('请先添加内容')
|
||||
showToast(t('news.edit_notice_enter_text'))
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -47,15 +49,15 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
|
||||
return <div className={styles.group}>
|
||||
<div className={'panel digital-person'}>
|
||||
<div className="area-title">
|
||||
<span className="">数字人主播台编辑区</span>
|
||||
<span className="text-gray-400">(出现数字人形象)</span>
|
||||
<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">
|
||||
{/* 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={'请输入文本内容'}
|
||||
placeholder={t('news.edit_notice_enter_text')}
|
||||
value={groups && groups.length > 0 ? groups[0][0].content : ''}
|
||||
autoSize={{minRows: 20, maxRows: 21}}
|
||||
variant={"borderless"}
|
||||
@ -63,14 +65,14 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
|
||||
handleDigitalPersonContentChange(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div> : <p className="p-2">12123</p>}
|
||||
</div> : <p className="p-2">{groups && groups.length > 0 ? groups[0][0].content : ''}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"panel groups-list flex-1"}>
|
||||
<div className={"area-title"}>
|
||||
<span className="">素材融合呈现编辑区</span>
|
||||
<span className="text-gray-400">(文、图、视频,不出现数字人形象)</span>
|
||||
<span className="">{t('news.edit_other_text')}</span>
|
||||
{i18n.language == 'zh-CN' && <span className="text-gray-400">(文、图、视频,不出现数字人形象)</span>}
|
||||
</div>
|
||||
|
||||
<div className="panel-body py-3">
|
||||
@ -91,7 +93,7 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
|
||||
}}
|
||||
onRemove={async () => {
|
||||
if (groups.length == 1) {
|
||||
message.warning('至少保留一个内容块')
|
||||
message.warning(t('news.edit_notice_keep_1'))
|
||||
return;
|
||||
}
|
||||
onChange?.(groups.filter((_, idx) => index !== idx))
|
||||
|
@ -7,6 +7,7 @@ import styles from './article.module.scss'
|
||||
import {getOssPolicy} from "@/service/api/common.ts";
|
||||
import {showToast} from "@/components/message.ts";
|
||||
import {IconAddImage, IconWarningCircle} from "@/components/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
@ -25,7 +26,7 @@ const MimeTypes = ['image/jpeg', 'image/png', 'image/jpg']
|
||||
const Data: { uploadConfig?: TOSSPolicy } = {}
|
||||
|
||||
export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: ImageProps) {
|
||||
|
||||
const {t} = useTranslation()
|
||||
const [loading, setLoading] = useState<number>(-1)
|
||||
// oss上传文件所需的数据
|
||||
const getUploadData: UploadProps['data'] = (file) => ({
|
||||
@ -57,7 +58,7 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
|
||||
onChange?.({type: 'image', content: Data.uploadConfig?.host + '/' + file.url})
|
||||
} else if (file.status == 'error') {
|
||||
setLoading(-1)
|
||||
showToast('上传图片失败,请重试', 'warning')
|
||||
showToast(t('upload.upload_failed'), 'warning')
|
||||
} else if (file.status == 'uploading') {
|
||||
setLoading(file.percent)
|
||||
}
|
||||
@ -79,10 +80,10 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
|
||||
{!onlyUpload && <Popconfirm
|
||||
rootClassName={'popconfirm-main'}
|
||||
placement={'right'}
|
||||
title={<div style={{minWidth: 150}}><span>请确认删除此删除此图片?</span></div>}
|
||||
title={<div style={{minWidth: 150}}><span>{t('upload.delete_confirm')}</span></div>}
|
||||
onConfirm={onRemove}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
okText={t('delete')}
|
||||
cancelText={t('cancel')}
|
||||
>
|
||||
<span className={styles.imageDelete}><CloseOutlined/></span>
|
||||
</Popconfirm>}
|
||||
@ -90,7 +91,7 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
|
||||
</> : <div className={styles.imagePlaceholder}>
|
||||
<div className={'text-center'}>
|
||||
<IconAddImage className={"text-4xl inline-block"} />
|
||||
<div className={'text-sm'}>上传图片</div>
|
||||
<div className={'text-sm'}>{t('upload.upload_image')}</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
@ -104,10 +105,10 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
|
||||
placement={'right'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={<div style={{minWidth: 150}}><span>请确认删除此图片?</span></div>}
|
||||
title={<div style={{minWidth: 150}}><span>{t('upload.delete_confirm')}</span></div>}
|
||||
onConfirm={onRemove}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
okText={t('delete')}
|
||||
cancelText={t('cancel')}
|
||||
>
|
||||
<span className={styles.imageDelete}><CloseOutlined/></span>
|
||||
</Popconfirm>}
|
||||
@ -117,15 +118,17 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
|
||||
}
|
||||
|
||||
export function BlockText({data, editable, onChange, isFirstBlock}: Props) {
|
||||
const {t} = useTranslation()
|
||||
return <div className='flex-1'>
|
||||
<div
|
||||
className={clsx(styles.text, isFirstBlock ? 'border-red-400 hover:border-red-500 focus-within:border-red-500' : '')}>
|
||||
{editable ? <div className="relative">
|
||||
{/*请输入文本内容*/}
|
||||
<Input.TextArea
|
||||
onChange={e => {
|
||||
onChange?.({type: 'text', content: e.target.value})
|
||||
}}
|
||||
placeholder={'请输入文本内容'} value={data.content} autoSize={{minRows: 4, maxRows: 5}}
|
||||
placeholder={t('news.edit_notice_enter_article_content')} value={data.content} autoSize={{minRows: 4, maxRows: 5}}
|
||||
variant={"borderless"}/>
|
||||
</div> : <p className="p-2">{data.content}</p>}
|
||||
</div>
|
||||
|
@ -56,7 +56,7 @@ export default function ButtonBatch(
|
||||
if(confirmMessage){
|
||||
modal.confirm({
|
||||
wrapClassName: 'root-modal-confirm',
|
||||
title: title || t('notice.title'),
|
||||
title: title || t('confirm.title'),
|
||||
centered: true,
|
||||
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
|
||||
content: confirmMessage,
|
||||
|
@ -2,6 +2,7 @@ import React, {useEffect, useMemo, useRef} from "react";
|
||||
import {Checkbox, Popover} from "antd";
|
||||
import {useBoolean, useClickAway} from "ahooks";
|
||||
import {CaretUpOutlined} from "@ant-design/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type ValueType = Id[][];
|
||||
type ValueFunc = (prev:ValueType)=>ValueType;
|
||||
@ -101,6 +102,7 @@ const TagSelect = (props: {
|
||||
|
||||
return parentList.findIndex(s => s[1] == item.value) != -1;
|
||||
}
|
||||
const {t} = useTranslation()
|
||||
const ref = useRef<HTMLDivElement|null>(null)
|
||||
useClickAway(()=>{
|
||||
set(false)
|
||||
@ -120,7 +122,7 @@ const TagSelect = (props: {
|
||||
set(!visible)
|
||||
}}
|
||||
>
|
||||
<span>{checkedAll || selectValues.length == 0 ? '全部来源' : '来源'}</span>
|
||||
<span>{checkedAll || selectValues.length == 0 ? t('news.news_all_source') : t('news.source')}</span>
|
||||
<CaretUpOutlined className={`ml-2 arrow-icon ${visible ? 'rotate-0' : 'rotate-180'}`}/>
|
||||
</div>
|
||||
<div className={`options-list-container absolute ${visible ? 'block' : 'hidden'}`}>
|
||||
@ -129,7 +131,7 @@ const TagSelect = (props: {
|
||||
<li className="select-option-item relative">
|
||||
<div className="option-value whitespace-nowrap flex justify-between">
|
||||
<span className="text-center flex-1"
|
||||
onClick={() => handleAllChanged(!checkedAll)}>全部来源</span>
|
||||
onClick={() => handleAllChanged(!checkedAll)}>t('news.news_all_source')</span>
|
||||
<Checkbox className="ml-6" checked={checkedAll}
|
||||
onChange={e => handleAllChanged(e.target.checked)}/>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {useMemo, useState} from "react";
|
||||
import {CaretUpOutlined} from "@ant-design/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export type TimeSelectProps = {
|
||||
value: number;
|
||||
@ -10,33 +11,35 @@ type OptionItem = {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
const AllTimeOption: OptionItem[] = [
|
||||
{
|
||||
label: '半小时内',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: '一小时内',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: '四小时内',
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
label: '一天内',
|
||||
value: 4
|
||||
},
|
||||
{
|
||||
label: '近一周',
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
label: '所有时间',
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
|
||||
const TimeSelect = (props: TimeSelectProps) => {
|
||||
const {t,i18n} = useTranslation();
|
||||
const AllTimeOption: OptionItem[] = useMemo(()=>([
|
||||
{
|
||||
label: t('time_filter.past_30_min'),
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
label: t('time_filter.past_hour'),
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: t('time_filter.past_4_hour'),
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
label: t('time_filter.past_24_hour'),
|
||||
value: 4
|
||||
},
|
||||
{
|
||||
label: t('time_filter.last_week'),
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
label: t('time_filter.all'),
|
||||
value: 0
|
||||
}
|
||||
]),[i18n.language])
|
||||
const selectLabel = useMemo(() => {
|
||||
return AllTimeOption.find(item => item.value == props.value)?.label || ''
|
||||
}, [props.value])
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import useConfig from "@/hooks/useConfig.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const AppLogo = ({style}: { style?: React.CSSProperties, theme?: 'origin' | 'color' }) => {
|
||||
return (
|
||||
@ -21,11 +22,11 @@ const AppLogo = ({style}: { style?: React.CSSProperties, theme?: 'origin' | 'col
|
||||
)
|
||||
}
|
||||
export const LogoText = ({style, className}: { style?: React.CSSProperties, className?: string }) => {
|
||||
const {appName} = useConfig()
|
||||
const {t} = useTranslation()
|
||||
return (
|
||||
<div className={`flex h-full ${className}`}>
|
||||
<AppLogo style={style}/>
|
||||
<span className={'ml-2 text-lg relative top-1'}>{appName}</span>
|
||||
<span className={'ml-2 text-lg relative top-1'}>{t('AppTitle')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import ImageCover from '@/assets/images/cover.png'
|
||||
import {IconDelete, IconEdit, IconPlaying, IconWarningCircle} from "@/components/icons";
|
||||
import {VideoStatus} from "@/service/api/video.ts";
|
||||
import {formatTime} from "@/util/strings.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
video: VideoInfo | LiveVideoInfo,
|
||||
@ -37,7 +38,7 @@ export const VideoListItem = (
|
||||
setNodeRef, transform
|
||||
} = useSortable({resizeObserverConfig: {}, id})
|
||||
|
||||
|
||||
const {t} = useTranslation()
|
||||
const [state, setState] = useSetState<{ checked?: boolean }>({})
|
||||
useEffect(() => {
|
||||
setState({checked})
|
||||
@ -60,14 +61,14 @@ export const VideoListItem = (
|
||||
<img className="w-[100px] h-[56px] object-cover" src={video.cover || ImageCover}/>
|
||||
{generating &&
|
||||
<div className={'absolute inset-0 bg-black/30 text-white flex items-center justify-center'}>
|
||||
<span className="ml-1">视频生成中</span>
|
||||
<span className="ml-1">{t('video.generating')}</span>
|
||||
</div>
|
||||
}
|
||||
{/* && active*/}
|
||||
{!generating && playing && <div className={'absolute rounded inset-0 bg-black/30 text-sm text-white flex items-center justify-center'}>
|
||||
<div className="text-center">
|
||||
<IconPlaying className="inline-block text-xl" />
|
||||
<div>播放中</div>
|
||||
<div>{t('video.playing')}</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
@ -107,7 +108,7 @@ export const VideoListItem = (
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={'你确定要删除此视频吗?'}
|
||||
title={t('video.delete_confirm_title')}
|
||||
// description={`删除后需从重新${type == 'create' ? '生成' : '推流'}`}
|
||||
onConfirm={onRemove}
|
||||
><button className="hover:text-blue-500"><IconDelete/></button></Popconfirm>}
|
||||
|
@ -6,6 +6,7 @@ import LangCN from './translations/zh-CN.json';
|
||||
console.log('AppConfig',AppMode)
|
||||
i18next.use(initReactI18next).init({
|
||||
debug: true,
|
||||
lng:'en-US',
|
||||
fallbackLng: 'en-US',
|
||||
resources: {
|
||||
'en-US': {translation:LangEN},
|
||||
|
@ -1,10 +1,152 @@
|
||||
{
|
||||
"AppTitle": "Digital Human Live",
|
||||
"AppTitle": "Metahuman Streaming platform",
|
||||
"Hello": "Hello",
|
||||
"login": {
|
||||
"text": "Login"
|
||||
"close": "Close",
|
||||
"confirm": {
|
||||
"push_title": "Push Notice",
|
||||
"push_video": "Are you sure editing selected news?",
|
||||
"title": "Notice"
|
||||
},
|
||||
"notice": {
|
||||
"title": "操作提示"
|
||||
"delete_batch": "Delete Select",
|
||||
"delete_failed": "Delete failed",
|
||||
"delete_success": "Delete success",
|
||||
"download": "Download",
|
||||
"generating": {
|
||||
"title": "Preview - Click video item to play"
|
||||
},
|
||||
"history": {
|
||||
"delete_confirm": "Are you sure you want to delete this video?",
|
||||
"push_success": "Streaming success",
|
||||
"search_key": "Please enter keywords for video title",
|
||||
"text": "History video"
|
||||
},
|
||||
"live": {
|
||||
"duration": "Duration",
|
||||
"edit_locked": "Disable to sort",
|
||||
"edit_unlock": "Unlock",
|
||||
"play_first": "Play first video",
|
||||
"playlist_count": "Current playlist total has {{count}} items",
|
||||
"title": "Live"
|
||||
},
|
||||
"login": {
|
||||
"code_sending": "Sending...",
|
||||
"invalid_username_or_pwd": "Invalid phone number or code",
|
||||
"loading": "Login...",
|
||||
"password": "Enter the verification code",
|
||||
"send_sms_code": "Send code",
|
||||
"text": "Sign in",
|
||||
"title": "Sign in",
|
||||
"username": "Please enter your phone number",
|
||||
"welcome": "Welcome"
|
||||
},
|
||||
"nav": {
|
||||
"editing": "Editing",
|
||||
"generating": "Generating",
|
||||
"live": "Streaming",
|
||||
"materials": "News Materials"
|
||||
},
|
||||
"news": {
|
||||
"delete_confirm": "Are you sure you want to delete this item?",
|
||||
"delete_confirm_count": "Are you sure you want to delete these {{count}} items?",
|
||||
"delete_description": "This item will be deleted.<br/>It can be recovered from the “news” page. ",
|
||||
"delete_empty": "Please select the items to delete",
|
||||
"download_empty": "Please select the news to download",
|
||||
"download_failed": "Download failed!",
|
||||
"edit_form_search": "Please enter keywords for news title",
|
||||
"editing": "Editing",
|
||||
"filter_all": "All",
|
||||
"filter_source": "News source",
|
||||
"generate_video": "Generating",
|
||||
"get_detail": "Get news details",
|
||||
"get_detail_error": "Get new details failed",
|
||||
"image_count": "image",
|
||||
"materials": {
|
||||
"title": "News Materials"
|
||||
},
|
||||
"news_all_source": "source",
|
||||
"push_empty": "please select the news to edit",
|
||||
"push_failed": "Failed to editing",
|
||||
"push_stream_empty": "please select the news to streaming",
|
||||
"push_stream_success": "Success",
|
||||
"push_streaming": "Pushing...",
|
||||
"push_success": "Push success",
|
||||
"push_to_edit": "Editing",
|
||||
"pushed": "Pushed",
|
||||
"search_key_title": "Please enter keywords",
|
||||
"source": "source",
|
||||
"title": "News content",
|
||||
"title_image_count": "Number of pictures",
|
||||
"title_operate": "Operation",
|
||||
"title_time": "time",
|
||||
"title_word_count": "Wordcount",
|
||||
"word_count": "word",
|
||||
|
||||
"edit_digital_text": "MetaHuman materiel",
|
||||
"edit_other_text": "Other media materiel",
|
||||
"edit_generate_video_again": "Regenerate",
|
||||
"edit_generate_again": "Regenerate",
|
||||
"edit_generate_video": "Video generation",
|
||||
"edit_save_failed": "Save failed!",
|
||||
|
||||
"edit_notice_keep_1": "Keep at least one content block",
|
||||
"edit_notice_enter_text": "Please enter content",
|
||||
"edit_notice_enter_article_title": "Please enter title",
|
||||
"edit_notice_enter_article_title1": "Please enter news title",
|
||||
"edit_notice_enter_article_content": "Please enter content",
|
||||
|
||||
"edit_add_group": "Add Group",
|
||||
"edit_delete_group": "Delete Group",
|
||||
"edit_delete_group_confirm": "Are you sure delete the group?",
|
||||
"delete_the_picture": "Are you sure delete the picture?"
|
||||
},
|
||||
"upload": {
|
||||
"upload_failed": "Upload failed",
|
||||
"delete_confirm": "Are you sure delete the picture?",
|
||||
"upload_image": "Upload Picture"
|
||||
},
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"select": {
|
||||
"pushed": "Pushed: {{count}}",
|
||||
"select_all": "Select all",
|
||||
"selected": "Selected",
|
||||
"selected_some": "Selected: {{count}}",
|
||||
"text": "Select",
|
||||
"total": "Total: {{count}}"
|
||||
},
|
||||
"time_filter": {
|
||||
"all": "All time",
|
||||
"last_week": "Last week",
|
||||
"past_24_hour": "Past 24 hour",
|
||||
"past_30_min": "Past 30 min",
|
||||
"past_4_hour": "Past 4 hour",
|
||||
"past_hour": "Past 1 hour"
|
||||
},
|
||||
"user": {
|
||||
"logout": "Logout"
|
||||
},
|
||||
"video": {
|
||||
"delete_confirm_title": "Are you sure you want to delete this video?",
|
||||
"delete_confirm": "These videos will be deleted.<br.>They can be recovered from the “news” page. ",
|
||||
"delete_description": "Are you sure you want to delete these {{count}} videos?",
|
||||
"delete_empty": "Select the video you want to delete",
|
||||
"download": "Download",
|
||||
"push_confirm": "Are you sure you want to streaming these video?",
|
||||
"push_empty": "Select the video you want to streaming",
|
||||
"push_failed": "some video streaming failed!",
|
||||
"push_success": "Streaming success,please goto Streaming!",
|
||||
"push_to_live": "Streaming",
|
||||
"sort_modify_confirm": "Are you change video sequence?",
|
||||
"sort_modify_failed": "Video sequence change failed",
|
||||
"sort_modify_live_success": "Video sequence changed",
|
||||
"sort_modify_rollback": "Exit and video sequence restored!",
|
||||
"sort_modify_success": "Video sequence changed",
|
||||
"title": "Title",
|
||||
"title_generated_time": "Time stamp",
|
||||
"title_operation": "Operation",
|
||||
"title_thumb": "Cover",
|
||||
"playing": "Playing",
|
||||
"generating": "Generating"
|
||||
}
|
||||
}
|
@ -1,10 +1,151 @@
|
||||
{
|
||||
"AppTitle": "数字人直播",
|
||||
"Hello": "你好",
|
||||
"login": {
|
||||
"text": "登录"
|
||||
"close": "关闭",
|
||||
"confirm": {
|
||||
"push_title": "推流提示",
|
||||
"push_video": "是否确定一键推流选中新闻视频?",
|
||||
"title": "提示"
|
||||
},
|
||||
"notice": {
|
||||
"title": "Notice"
|
||||
"delete_batch": "批量删除",
|
||||
"delete_failed": "删除失败",
|
||||
"delete_success": "删除成功",
|
||||
"download": "下载",
|
||||
"generating": {
|
||||
"title": "预览视频 - 点击视频列表播放"
|
||||
},
|
||||
"history": {
|
||||
"delete_confirm": "是否要删除该视频",
|
||||
"push_success": "一键推流成功,已推流至数字人直播间,请查看!",
|
||||
"search_key": "请输入视频标题关键字进行信息",
|
||||
"text": "历史视频"
|
||||
},
|
||||
"live": {
|
||||
"duration": "时长",
|
||||
"edit_locked": "锁定状态不可排序",
|
||||
"edit_unlock": "已解锁",
|
||||
"play_first": "即将播放第一条视频",
|
||||
"playlist_count": "当前播放列表共 {{count}} 条",
|
||||
"title": "直播界面"
|
||||
},
|
||||
"login": {
|
||||
"code_sending": "发送中",
|
||||
"invalid_username_or_pwd": "账号或密码错误",
|
||||
"loading": "登录中...",
|
||||
"password": "请输入验证码",
|
||||
"send_sms_code": "获取验证码",
|
||||
"text": "立即登录",
|
||||
"title": "登录",
|
||||
"username": "请输入账号",
|
||||
"welcome": "欢迎登录"
|
||||
},
|
||||
"nav": {
|
||||
"editing": "新闻编辑",
|
||||
"generating": "视频生成",
|
||||
"live": "数字人直播间",
|
||||
"materials": "新闻素材"
|
||||
},
|
||||
"news": {
|
||||
"delete_confirm": "你确定要删除吗?",
|
||||
"delete_confirm_count": "你确定要删除选择的 {{count}} 条新闻吗?",
|
||||
"delete_description": "删除后需从新闻素材中重新选择",
|
||||
"delete_empty": "请选择要删除的新闻",
|
||||
"download_empty": "请选择要下载的新闻",
|
||||
"download_failed": "下载新闻失败,请重试!",
|
||||
"edit_form_search": "请输入新闻标题关键词进行搜索",
|
||||
"editing": "新闻编辑",
|
||||
"filter_all": "全部",
|
||||
"filter_source": "新闻来源",
|
||||
"generate_video": "生成视频",
|
||||
"get_detail": "获取新闻详情",
|
||||
"get_detail_error": "获取新闻详情失败",
|
||||
"image_count": "图片数",
|
||||
"materials": {
|
||||
"title": "新闻素材"
|
||||
},
|
||||
"news_all_source": "全部来源",
|
||||
"push_empty": "请选择要推入编辑的新闻",
|
||||
"push_failed": "推送失败",
|
||||
"push_stream_empty": "请选择要开播的新闻",
|
||||
"push_stream_success": "推流成功",
|
||||
"push_streaming": "推流中...",
|
||||
"push_success": "推送成功",
|
||||
"push_to_edit": "推入编辑",
|
||||
"pushed": "已推送",
|
||||
"search_key_title": "请输入新闻标题关键词进行搜索",
|
||||
"source": "来源",
|
||||
"title": "标题",
|
||||
"title_image_count": "图片数",
|
||||
"title_operate": "操作",
|
||||
"title_time": "时间",
|
||||
"title_word_count": "字数",
|
||||
"word_count": "字数",
|
||||
|
||||
"edit_digital_text": "数字人主播台编辑区",
|
||||
"edit_other_text": "素材融合呈现编辑区",
|
||||
"edit_generate_video_again": "重新生成",
|
||||
"edit_generate_again": "重新生成",
|
||||
"edit_generate_video": "生成视频",
|
||||
"edit_save_failed": "保存失败,请重试!",
|
||||
|
||||
"edit_notice_keep_1": "至少保留一个内容块",
|
||||
"edit_notice_enter_text": "请先添加内容",
|
||||
"edit_notice_enter_article_title": "请输入文章标题",
|
||||
"edit_notice_enter_article_title1": "请输入标题内容",
|
||||
"edit_notice_enter_article_content": "请输入正文文本内容",
|
||||
|
||||
"edit_add_group": "新增分组",
|
||||
"edit_delete_group": "删除此分组",
|
||||
"edit_delete_group_confirm": "请确认删除此分组?",
|
||||
"delete_the_picture": "请确认删除此图片"
|
||||
},
|
||||
"upload": {
|
||||
"upload_failed": "上传图片失败,请重试",
|
||||
"delete_confirm": "请确认删除此图片?",
|
||||
"upload_image": "上传图片"
|
||||
},
|
||||
"delete": "删除",
|
||||
"cancel": "取消",
|
||||
"confirm": "确定",
|
||||
"select": {
|
||||
"pushed": "已推送: {{count}} 条",
|
||||
"select_all": "全选",
|
||||
"selected": "已选",
|
||||
"selected_some": "已选 {{count}} 条",
|
||||
"text": "选择",
|
||||
"total": "总共 {{count}} 条"
|
||||
},
|
||||
"time_filter": {
|
||||
"all": "所有时间",
|
||||
"last_week": "近一周",
|
||||
"past_24_hour": "一天内",
|
||||
"past_30_min": "半小时内",
|
||||
"past_4_hour": "四小时内",
|
||||
"past_hour": "一小时内"
|
||||
},
|
||||
"user": {
|
||||
"logout": "退出登录"
|
||||
},
|
||||
"video": {
|
||||
"delete_confirm_title": "你确定要删除此视频吗 ",
|
||||
"delete_confirm": "删除后需重新生成视频",
|
||||
"delete_description": "已选择{{count}}条,确定要全部删除吗?",
|
||||
"delete_empty": "请选择要删除的视频",
|
||||
"download": "下载视频",
|
||||
"push_confirm": "是否确定一键推流选中新闻视频?",
|
||||
"push_empty": "请选择要推流的新闻视频",
|
||||
"push_failed": "选择视频中有部分视频还在生成中无法推送,推流成功视频前往数字人直播间页面查看!",
|
||||
"push_success": "一键推流成功,已推流至数字人直播间,请前往数字人直播间页面查看!",
|
||||
"push_to_live": "一键推流",
|
||||
"sort_modify_confirm": "是否采纳移动视频位置操作?",
|
||||
"sort_modify_failed": "调整视频顺序失败,请重试!",
|
||||
"sort_modify_live_success": "已完成直播队列的修改",
|
||||
"sort_modify_rollback": "退出并恢复之前的直播队列!",
|
||||
"sort_modify_success": "调整视频顺序成功",
|
||||
"title": "标题",
|
||||
"title_generated_time": "生成时间",
|
||||
"title_operation": "操作",
|
||||
"title_thumb": "缩略图",
|
||||
"playing": "播放中"
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import ReactDOM from 'react-dom/client'
|
||||
|
||||
import '@/i18n/config.ts'
|
||||
import App from './App.tsx'
|
||||
import '@/assets/index.scss'
|
||||
|
||||
|
@ -3,6 +3,7 @@ import {useSetState} from "ahooks";
|
||||
import {SearchOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import TimeSelect from "@/components/form/time-select.tsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
onSearch?: (params: VideoSearchParams) => void;
|
||||
@ -11,6 +12,7 @@ type Props = {
|
||||
}
|
||||
|
||||
export default function SearchForm({onSearch}: Props) {
|
||||
const {t} = useTranslation()
|
||||
const [state, setState] = useSetState<{
|
||||
pushing?: boolean;
|
||||
time_flag: number;
|
||||
@ -44,7 +46,7 @@ export default function SearchForm({onSearch}: Props) {
|
||||
onPressEnter={() => onFinish(state)}
|
||||
onBlur={() => onFinish(state)}
|
||||
allowClear
|
||||
placeholder={'请输入视频标题关键字进行信息'}
|
||||
placeholder={t("history.search_key")}
|
||||
/>
|
||||
<TimeSelect
|
||||
className="w-[120px] ml-1"
|
||||
|
@ -5,6 +5,7 @@ import {Player} from "@/components/video/player.tsx";
|
||||
|
||||
import {push2room} from "@/service/api/video.ts";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
video?: VideoInfo;
|
||||
@ -12,6 +13,7 @@ type Props = {
|
||||
onClose?: () => void
|
||||
}
|
||||
export default function VideoDetail({video, onClose,autoPlay}: Props) {
|
||||
const {t} = useTranslation()
|
||||
const [state, setState] = useSetState({
|
||||
exporting: false,
|
||||
pushing: false,
|
||||
@ -22,7 +24,7 @@ export default function VideoDetail({video, onClose,autoPlay}: Props) {
|
||||
if (state.pushing) return
|
||||
setState({pushing: true})
|
||||
push2room([video.id]).then(() => {
|
||||
showToast('一键推流成功,已推流至数字人直播间,请查看!', 'success')
|
||||
showToast(t('history.push_success'), 'success')
|
||||
}).catch(showErrorToast).finally(() => {
|
||||
setState({pushing: false})
|
||||
})
|
||||
@ -51,11 +53,11 @@ export default function VideoDetail({video, onClose,autoPlay}: Props) {
|
||||
</div>
|
||||
<div className="flex justify-end modal-control-footer">
|
||||
<div className="flex gap-4">
|
||||
<button disabled={state.pushing} className="text-gray-400 hover:text-gray-800 " type="text" onClick={pushToRoom}>一键推流</button>
|
||||
<button disabled={state.pushing} className="text-gray-400 hover:text-gray-800 " type="button" onClick={pushToRoom}>{t('video.push_to_live')}</button>
|
||||
<button disabled={state.exporting} className="text-gray-400 hover:text-gray-800 " onClick={downloadVideo}
|
||||
type="text">下载视频
|
||||
type="button">{t('video.download')}
|
||||
</button>
|
||||
<button onClick={onClose} type="text" className="text-gray-800 hover:text-blue-500">关闭</button>
|
||||
<button onClick={onClose} type="button" className="text-gray-800 hover:text-blue-500">{t('close')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -11,13 +11,14 @@ import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infini
|
||||
import ButtonBatch from "@/components/button-batch.tsx";
|
||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import {IconArrowRight, IconDelete} from "@/components/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const DEFAULT_PAGE_LIMIT = {
|
||||
|
||||
page: 1,
|
||||
limit: 12
|
||||
}
|
||||
export default function LibraryIndex() {
|
||||
const {t} = useTranslation()
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
const [params, setParams] = useState<VideoSearchParams>({
|
||||
@ -49,8 +50,8 @@ export default function LibraryIndex() {
|
||||
|
||||
const handleRemove = (video: VideoInfo) => {
|
||||
modal.confirm({
|
||||
title: '删除提示',
|
||||
content: '是否要删除该视频',
|
||||
title: t('confirm.title'),
|
||||
content: t('history.delete_confirm'),
|
||||
onOk: () => {
|
||||
console.log('OK', video);
|
||||
}
|
||||
@ -59,8 +60,8 @@ export default function LibraryIndex() {
|
||||
const handleLive = async () => {
|
||||
if (checkedIdArray.length == 0) return;
|
||||
modal.confirm({
|
||||
title: '推流提示',
|
||||
content: '是否确定一键推流选中新闻视频?',
|
||||
title: t('confirm.push_title'),
|
||||
content: t('confirm.push_video'),
|
||||
onOk: () => {
|
||||
console.log('OK');
|
||||
}
|
||||
@ -71,6 +72,7 @@ export default function LibraryIndex() {
|
||||
autoPlay: boolean
|
||||
}>()
|
||||
const handleAllCheckedChange = (checked: boolean) => {
|
||||
if (!data) return;
|
||||
setCheckedIdArray(checked ? data.list.map(v => v.id) : [])
|
||||
setState({
|
||||
checkedAll: !state.checkedAll
|
||||
@ -105,13 +107,13 @@ export default function LibraryIndex() {
|
||||
<div className="pl-[70px]"></div>
|
||||
<div className="flex items-center">
|
||||
<Space className="text-gray-400">
|
||||
<span>总共 {data?.list.length || 0} 条</span>
|
||||
<span>已推送: {state.pushedCount} 条</span>
|
||||
<span className={'text-blue-500'}>已选: {checkedIdArray.length} 条</span>
|
||||
<span>{t('select.total',{count:data?.list.length || 0})}</span>
|
||||
<span>{t('select.pushed',{count:state.pushedCount})}</span>
|
||||
<span className={'text-blue-500'}>{t('select.selected_some',{count:checkedIdArray.length})}</span>
|
||||
</Space>
|
||||
<button className="hover:text-blue-300 text-gray-400 ml-2"
|
||||
onClick={() => handleAllCheckedChange(checkedIdArray.length != data?.list.length)}>
|
||||
<span className="text-sm mr-2">全选</span>
|
||||
<span className="text-sm mr-2">{t("select.select_all")}</span>
|
||||
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
|
||||
</button>
|
||||
<Checkbox checked={checkedIdArray.length == data?.list.length}
|
||||
@ -157,17 +159,19 @@ export default function LibraryIndex() {
|
||||
onSuccess={refresh}
|
||||
className='bg-gray-300 hover:bg-gray-400 text-white'
|
||||
icon={<IconDelete className=""/>}
|
||||
title={`你确定要删除选择的 ${checkedIdArray.length} 条视频吗?`}
|
||||
confirmMessage={'删除后需重新生成视频'}
|
||||
title={t('video.delete_description',{count:checkedIdArray.length})}
|
||||
emptyMessage={t('video.delete_empty')}
|
||||
confirmMessage={t('video.delete_confirm')}
|
||||
onProcess={deleteHistories}
|
||||
>批量删除</ButtonBatch>}
|
||||
>{t('delete_batch')}</ButtonBatch>}
|
||||
{checkedIdArray?.length > 0 && <ButtonBatch
|
||||
selected={checkedIdArray}
|
||||
onSuccess={refresh}
|
||||
className='bg-[#4096ff] hover:bg-blue-600 text-white'
|
||||
icon={<IconArrowRight className={'text-white'}/>}
|
||||
onProcess={push2room}
|
||||
>一键推流</ButtonBatch>}
|
||||
emptyMessage={t('video.push_empty')}
|
||||
>{t('video.push_to_live')}</ButtonBatch>}
|
||||
</div>
|
||||
</>)
|
||||
}
|
17
src/pages/live-player/index.tsx
Normal file
17
src/pages/live-player/index.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import {useMount} from "ahooks";
|
||||
import {getLiveUrl} from "@/service/api/live.ts";
|
||||
import React, {useState} from "react";
|
||||
import {Player} from "@/components/video/player.tsx";
|
||||
import './style.scss'
|
||||
|
||||
export default function LivePlayer() {
|
||||
const [liveUrl, setLiveUrl] = useState<string>('http://fm.live.starbitech.com/fm/prod_fm.flv')
|
||||
useMount(async ()=>{
|
||||
getLiveUrl().then((ret)=>{
|
||||
setLiveUrl(ret.flv_url)
|
||||
})
|
||||
})
|
||||
return <div className="live-player-wrapper ">
|
||||
<Player showControls url={liveUrl} className={'h-screen'} />
|
||||
</div>
|
||||
}
|
9
src/pages/live-player/style.scss
Normal file
9
src/pages/live-player/style.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.live-player-wrapper {
|
||||
@apply relative m-auto h-screen w-screen max-w-[480px] content-center relative;
|
||||
.player-container{
|
||||
@apply bg-gray-500 w-full h-screen ;
|
||||
}
|
||||
video {
|
||||
height: 100vh !important;
|
||||
}
|
||||
}
|
@ -15,10 +15,11 @@ import {Player, PlayerInstance} from "@/components/video/player.tsx";
|
||||
import {IconDelete, IconLocked, IconUnlock} from "@/components/icons";
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const cache: { flvPlayer?: FlvJs.Player, timerPlayNext?: any, timerLoadState?: any, prevUrl?: string } = {}
|
||||
export default function LiveIndex() {
|
||||
|
||||
const {t} = useTranslation()
|
||||
const player = useRef<PlayerInstance | null>(null)
|
||||
|
||||
const [videoData, setVideoData] = useState<LiveVideoInfo[]>([])
|
||||
@ -65,7 +66,7 @@ export default function LiveIndex() {
|
||||
const _activeIndex = index != undefined && index > -1 ? index : (endToFirst ? 0 : activeIndex.current + 1)
|
||||
setState({activeIndex: _activeIndex})
|
||||
if (endToFirst) {
|
||||
showToast('即将播放第一条视频');
|
||||
showToast(t("live.play_first"));
|
||||
}
|
||||
// 找到对应video item 并显示在视图可见区域
|
||||
showVideoItem(_activeIndex)
|
||||
@ -143,7 +144,7 @@ export default function LiveIndex() {
|
||||
|
||||
const processDeleteVideo = async (ids: Id[]) => {
|
||||
deleteByIds(ids).then(() => {
|
||||
showToast('删除成功!', 'success')
|
||||
showToast(t('delete_success'), 'success')
|
||||
loadList()
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
@ -158,20 +159,20 @@ export default function LiveIndex() {
|
||||
return;
|
||||
}
|
||||
modal.confirm({
|
||||
title: '提示',
|
||||
content: '是否采纳移动视频位置操作?',
|
||||
title: t('confirm.title'),
|
||||
content: t('video.sort_modify_confirm'),
|
||||
centered: true,
|
||||
onOk: () => {
|
||||
//showToast('编辑成功!!!', 'info');
|
||||
modifyOrder(videoData.map(s => s.id)).then(() => {
|
||||
showToast('已完成直播队列的修改!', 'success')
|
||||
showToast(t('video.sort_modify_live_success'), 'success')
|
||||
setEditable(false)
|
||||
}).catch(() => {
|
||||
showToast('调整视频顺序失败,请重试!', 'warning')
|
||||
showToast(t('video.sort_modify_failed'), 'warning')
|
||||
})
|
||||
},
|
||||
onCancel: () => {
|
||||
showToast('退出并恢复之前的直播队列!', 'info');
|
||||
showToast(t('video.sort_modify_rollback'), 'info');
|
||||
loadList()
|
||||
setEditable(false)
|
||||
}
|
||||
@ -210,7 +211,7 @@ export default function LiveIndex() {
|
||||
<div className="flex">
|
||||
<div className="video-player-container mr-16 flex items-center">
|
||||
<div>
|
||||
<div className="text-center text-base text-gray-400">直播界面</div>
|
||||
<div className="text-center text-base text-gray-400">{t('live.title')}</div>
|
||||
<div className="video-player flex justify-center flex-1 mt-1">
|
||||
<div className="live-player relative rounded overflow-hidden w-[360px] h-[636px]"
|
||||
style={{backgroundColor: 'hsl(210, 100%, 48%)'}}>
|
||||
@ -224,7 +225,7 @@ export default function LiveIndex() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center text-sm mt-4 text-gray-400">
|
||||
<span>视频时长: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)}</span>
|
||||
<span>{t('live.duration')}: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -233,14 +234,14 @@ export default function LiveIndex() {
|
||||
<div>
|
||||
<Space>
|
||||
{/*<span className={"text-blue-500"}>视频正在播放{state.activeIndex == -1 ? '' : `到 ${state.activeIndex + 1} 条`}</span>*/}
|
||||
<span>当前播放列表共 {videoData.length} 条</span>
|
||||
<span>{t('live.playlist_count',{count:videoData.length})}</span>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className={'flex items-center text-gray-400 cursor-pointer select-none'}
|
||||
onClick={handleConfirm}>
|
||||
<span>{editable ? '已解锁' : '锁定状态不可排序'}</span>
|
||||
<span>{editable ? t('live.edit_unlock') : t('live.edit_locked')}</span>
|
||||
<span className="ml-2 text-sm">
|
||||
{editable ? <IconUnlock/> : <IconLocked/>}
|
||||
</span>
|
||||
@ -248,7 +249,7 @@ export default function LiveIndex() {
|
||||
<div className="check-all ml-10">
|
||||
<button className="hover:text-blue-300 text-gray-400"
|
||||
onClick={handleAllCheckedChange}>
|
||||
<span className="text-sm mr-2 whitespace-nowrap">全选</span>
|
||||
<span className="text-sm mr-2 whitespace-nowrap">{t('select.select_all')}</span>
|
||||
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
|
||||
</button>
|
||||
<Checkbox checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
|
||||
@ -258,10 +259,10 @@ export default function LiveIndex() {
|
||||
<div className="list-header">
|
||||
<div className="list-row header-row">
|
||||
<div className="col number">No.</div>
|
||||
<div className="col cover">缩略图</div>
|
||||
<div className="col title">标题</div>
|
||||
<div className="col generated-time">生成时间</div>
|
||||
<div className="col operation"></div>
|
||||
<div className="col cover">{t('video.title_thumb')}</div>
|
||||
<div className="col title">{t('video.title')}</div>
|
||||
<div className="col generated-time">{t('video.title_generated_time')}</div>
|
||||
<div className="col operation">{t('video.title_operation')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
@ -322,12 +323,12 @@ export default function LiveIndex() {
|
||||
{checkedIdArray.length > 0 && <ButtonBatch
|
||||
className='bg-gray-300 hover:bg-gray-400 text-white'
|
||||
selected={checkedIdArray}
|
||||
emptyMessage={`请选择要删除的视频`}
|
||||
confirmMessage={`是否删除当前的${checkedIdArray.length}条视频?`}
|
||||
emptyMessage={t('video.delete_empty')}
|
||||
confirmMessage={t('video.delete_description',{count:checkedIdArray.length})}
|
||||
onSuccess={loadList}
|
||||
onProcess={processDeleteVideo}
|
||||
>
|
||||
<span className={'text'}>批量删除</span>
|
||||
<span className={'text'}>{t('delete_batch')}</span>
|
||||
<IconDelete/>
|
||||
</ButtonBatch>}
|
||||
</div>
|
||||
|
@ -3,17 +3,20 @@ import {showToast} from "@/components/message.ts";
|
||||
import React, {useState} from "react";
|
||||
import {IconDelete, IconWarningCircle} from "@/components/icons";
|
||||
import {deleteByIds} from "@/service/api/article.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {divide} from "lodash";
|
||||
|
||||
export default function ButtonDeleteBatch(props: { ids: Id[];onSuccess?: () => void; }) {
|
||||
const {t} = useTranslation()
|
||||
const {modal} = App.useApp();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const handlePush = () => {
|
||||
setLoading(true)
|
||||
deleteByIds(props.ids).then(() => {
|
||||
props.onSuccess?.();
|
||||
showToast('删除成功', 'success')
|
||||
showToast(t('delete_success'), 'success')
|
||||
}).catch(() => {
|
||||
showToast('删除失败', 'error')
|
||||
showToast(t('delete_failed'), 'error')
|
||||
}).finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
@ -21,14 +24,14 @@ export default function ButtonDeleteBatch(props: { ids: Id[];onSuccess?: () => v
|
||||
const onPushClick = () => {
|
||||
if(loading) return;
|
||||
if (props.ids.length === 0) {
|
||||
showToast('请选择要删除的新闻', 'warning')
|
||||
showToast(t('news.delete_empty'), 'warning')
|
||||
return
|
||||
}
|
||||
modal.confirm({
|
||||
wrapClassName:'root-modal-confirm',
|
||||
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
|
||||
title: `你确定要删除选择的 ${props.ids.length} 条新闻吗?`,
|
||||
content: '删除后需从新闻素材中重新选择',
|
||||
title: t('news.delete_confirm_count',{count:props.ids.length}),
|
||||
content: <span dangerouslySetInnerHTML={{__html:t('news.delete_description')}}></span>,
|
||||
onOk: handlePush,
|
||||
centered: true
|
||||
})
|
||||
@ -40,7 +43,7 @@ export default function ButtonDeleteBatch(props: { ids: Id[];onSuccess?: () => v
|
||||
onClick={onPushClick}
|
||||
className='bg-gray-300 hover:bg-gray-400 text-white'
|
||||
>
|
||||
<span className={'text'}>批量删除</span>
|
||||
<span className={'text'}>{t('delete_batch')}</span>
|
||||
<IconDelete className=""/>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@ import {getById} from "@/service/api/news.ts";
|
||||
|
||||
import {showToast} from "@/components/message.ts";
|
||||
import {IconDownload} from "@/components/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
|
||||
/**
|
||||
@ -63,11 +64,12 @@ async function downloadAsZip(list: NewsInfo[]) {
|
||||
}
|
||||
|
||||
export default function ButtonNewsDownload(props: { ids: Id[] }) {
|
||||
const {t} = useTranslation()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const onDownloadClick = async (ids: Id[]) => {
|
||||
if(loading) return;
|
||||
if (props.ids.length === 0) {
|
||||
showToast('请选择要下载的新闻', 'warning')
|
||||
showToast(t('news.download_empty'), 'warning')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
@ -75,7 +77,7 @@ export default function ButtonNewsDownload(props: { ids: Id[] }) {
|
||||
const list = await getAllNewsContent(ids)
|
||||
await downloadAsZip(list)
|
||||
} catch (e) {
|
||||
showToast('下载新闻失败,请重试!', 'error')
|
||||
showToast(t('news.download_failed'), 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@ -86,7 +88,7 @@ export default function ButtonNewsDownload(props: { ids: Id[] }) {
|
||||
className={'btn-action bg-[#eef5ff] text-gray-800 hover:bg-[#d2e3ff]'}
|
||||
onClick={() => onDownloadClick(props.ids)}
|
||||
>
|
||||
<span className="text">下载</span>
|
||||
<span className="text">{t('download')}</span>
|
||||
<IconDownload />
|
||||
</button>
|
||||
)
|
||||
|
@ -4,20 +4,22 @@ import {push2article} from "@/service/api/news.ts";
|
||||
import {IconArrowRight} from "@/components/icons";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useIndexArrayCache} from "@/hooks/useCache.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export default function ButtonPushNews2Article(props: { ids: Id[]; }) {
|
||||
// const {modal} = App.useApp();
|
||||
const {t}= useTranslation()
|
||||
const [loading,setLoading] = useState(false)
|
||||
const navigate = useNavigate();
|
||||
const {set} = useIndexArrayCache();
|
||||
const handlePush = () => {
|
||||
setLoading(true)
|
||||
push2article(props.ids).then(() => {
|
||||
showToast('推送成功', 'success')
|
||||
showToast(t('news.push_success'), 'success')
|
||||
set([])
|
||||
navigate('/edit')
|
||||
}).catch(() => {
|
||||
showToast('推送失败', 'error')
|
||||
showToast(t('news.push_failed'), 'error')
|
||||
}).finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
@ -25,7 +27,7 @@ export default function ButtonPushNews2Article(props: { ids: Id[]; }) {
|
||||
const onPushClick = () => {
|
||||
if(loading) return;
|
||||
if (props.ids.length === 0) {
|
||||
showToast('请选择要推入编辑的新闻', 'warning')
|
||||
showToast(t('news.push_empty'), 'warning')
|
||||
return
|
||||
}
|
||||
handlePush();
|
||||
@ -42,7 +44,7 @@ export default function ButtonPushNews2Article(props: { ids: Id[]; }) {
|
||||
onClick={onPushClick}
|
||||
className='bg-[#4096ff] hover:bg-blue-600 text-white'
|
||||
>
|
||||
<span className={'text'}>推入编辑</span>
|
||||
<span className={'text'}>{t('news.push_to_edit')}</span>
|
||||
<IconArrowRight className={'text-white'} />
|
||||
</button>
|
||||
)
|
||||
|
@ -3,16 +3,18 @@ import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {push2video} from "@/service/api/article.ts";
|
||||
import {IconArrowRight} from "@/components/icons";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
|
||||
export default function ButtonPush2Video(props: { ids: Id[]; onSuccess?: () => void; }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const {t} = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const handlePush = () => {
|
||||
setLoading(true)
|
||||
push2video(props.ids).then(() => {
|
||||
showToast('推流成功', 'success')
|
||||
navigate('/create?state=push-success',{
|
||||
showToast(t('news.push_stream_success'), 'success')
|
||||
navigate('/create?state=push-success', {
|
||||
state: 'push-success'
|
||||
})
|
||||
// props.onSuccess?.()
|
||||
@ -23,7 +25,7 @@ export default function ButtonPush2Video(props: { ids: Id[]; onSuccess?: () => v
|
||||
const onPushClick = () => {
|
||||
if (loading) return;
|
||||
if (props.ids.length === 0) {
|
||||
showToast('请选择要开播的新闻', 'warning')
|
||||
showToast(t('news.push_stream_empty'), 'warning')
|
||||
return
|
||||
}
|
||||
// Modal.confirm({
|
||||
@ -40,7 +42,7 @@ export default function ButtonPush2Video(props: { ids: Id[]; onSuccess?: () => v
|
||||
className='bg-[#4096ff] hover:bg-blue-600 text-white'
|
||||
onClick={onPushClick}
|
||||
>
|
||||
<span className={'text'}>{loading?'推送中...':'生成视频'}</span>
|
||||
<span className={'text'}>{loading ? t('news.push_streaming') : t('news.generate_video')}</span>
|
||||
<IconArrowRight className={'text-white'}/>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -4,20 +4,22 @@ import React, {useEffect, useState} from "react";
|
||||
import {useSetState} from "ahooks";
|
||||
import useArticleTags from "@/hooks/useArticleTags.ts";
|
||||
import TagSelect from "@/components/form/tag-select.tsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export default function EditSearchForm(props: {
|
||||
onSubmit: (values: ApiArticleSearchParams) => void;
|
||||
defaultParams?: Partial<ApiArticleSearchParams>;
|
||||
}) {
|
||||
const {t} = useTranslation()
|
||||
const articleTags = useArticleTags()
|
||||
const [tags, _setTags] = useState<Id[][]>([]);
|
||||
const [prevSearchName, setPrevSearchName] = useState<string>(props.defaultParams?.title||'')
|
||||
const [prevSearchName, setPrevSearchName] = useState<string>(props.defaultParams?.title || '')
|
||||
const [params, setParams] = useSetState<ApiArticleSearchParams>({
|
||||
pagination: {limit: 10, page: 1},
|
||||
title:props.defaultParams?.title||''
|
||||
title: props.defaultParams?.title || ''
|
||||
});
|
||||
|
||||
const handleSubmit = (_tags?:Id[][],from?:'input') => {
|
||||
const handleSubmit = (_tags?: Id[][], from?: 'input') => {
|
||||
if (from == 'input' && (params.title == prevSearchName || (!params.title && !prevSearchName))) return
|
||||
params.title = prevSearchName;
|
||||
setParams({title: prevSearchName})
|
||||
@ -42,21 +44,21 @@ export default function EditSearchForm(props: {
|
||||
}
|
||||
})
|
||||
}
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
const {defaultParams} = props;
|
||||
if(!defaultParams){
|
||||
if (!defaultParams) {
|
||||
return;
|
||||
}
|
||||
const tags:Id[][] = []
|
||||
const tags: Id[][] = []
|
||||
|
||||
if(defaultParams.tags){
|
||||
defaultParams.tags.forEach(it=>{
|
||||
if (defaultParams.tags) {
|
||||
defaultParams.tags.forEach(it => {
|
||||
tags.push([it.level1, it.level2])
|
||||
})
|
||||
_setTags(tags)
|
||||
}
|
||||
},[articleTags])
|
||||
const setTags = (_tags: Id[][])=>{
|
||||
}, [articleTags])
|
||||
const setTags = (_tags: Id[][]) => {
|
||||
console.log(_tags)
|
||||
|
||||
_setTags(_tags)
|
||||
@ -70,9 +72,9 @@ export default function EditSearchForm(props: {
|
||||
onChange={e => setPrevSearchName(e.target.value)}
|
||||
type="text" className="rounded-3xl px-3 w-[270px]"
|
||||
prefix={<SearchOutlined/>}
|
||||
placeholder="请输入新闻标题关键词进行搜索"
|
||||
onPressEnter={()=>handleSubmit(undefined,'input')}
|
||||
onBlur={()=>handleSubmit(undefined,'input')}
|
||||
placeholder={t('news.edit_form_search')}
|
||||
onPressEnter={() => handleSubmit(undefined, 'input')}
|
||||
onBlur={() => handleSubmit(undefined, 'input')}
|
||||
/>
|
||||
{/*<span className="ml-5 text-sm">来源</span>*/}
|
||||
{/*<ArticleCascader*/}
|
||||
|
@ -9,6 +9,7 @@ import TimeSelect from "@/components/form/time-select.tsx";
|
||||
|
||||
import styles from './style.module.scss'
|
||||
import {IconPin} from "@/components/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type SearchPanelProps = {
|
||||
onSearch?: (params: ApiArticleSearchParams) => void;
|
||||
@ -24,6 +25,7 @@ const DEFAULT_STATE = {
|
||||
}
|
||||
export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) {
|
||||
const tags = useArticleTags();
|
||||
const {t} = useTranslation()
|
||||
const [params, setParams] = useSetState<ApiArticleSearchParams>({
|
||||
pagination,
|
||||
time_flag:1,
|
||||
@ -146,13 +148,13 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
value={prevSearchName}
|
||||
onChange={e => setPrevSearchName(e.target.value)}
|
||||
className="w-[270px] rounded-3xl"
|
||||
placeholder={'请输入新闻标题关键词进行搜索'}
|
||||
placeholder={t('news.search_key_title')}
|
||||
onPressEnter={onFinish}
|
||||
onBlur={onFinish}
|
||||
prefix={<SearchOutlined/>}
|
||||
/>
|
||||
<TimeSelect
|
||||
className="w-[120px] ml-1"
|
||||
className="w-[140px] ml-1"
|
||||
value={typeof(params.time_flag) != "undefined" ? params.time_flag : 1}
|
||||
onChange={handleTimeFilter}
|
||||
/>
|
||||
@ -167,7 +169,7 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
onClick={() => {
|
||||
handleFilter({tag_level_1_id: -1, tag_level_2_id: -1})
|
||||
setSubOptions([])
|
||||
}}>全部
|
||||
}}>{t('news.filter_all')}
|
||||
</div>
|
||||
{pinnedList.filter(s => (Number(s.value) !== 999999)).map(it => (
|
||||
<span
|
||||
@ -194,7 +196,7 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
{/* 固定新闻来源 */}
|
||||
<div className={clsx(styles.pinnedManagePanel)}>
|
||||
<div className="header flex justify-between">
|
||||
<div className="title font-bold">新闻来源</div>
|
||||
<div className="title font-bold">{t('news.filter_source')}</div>
|
||||
<div className={'cursor-pointer block hover:text-blue-500'} onClick={setFalse}>
|
||||
<UpOutlined style={{fontSize: 20}}/>
|
||||
</div>
|
||||
|
@ -56,7 +56,7 @@
|
||||
}
|
||||
}
|
||||
.title{
|
||||
@apply flex-1 pl-0 justify-start;
|
||||
@apply flex-1 pl-0;
|
||||
&:after{
|
||||
display: none;
|
||||
}
|
||||
@ -64,7 +64,11 @@
|
||||
.source{
|
||||
width: 180px;
|
||||
}
|
||||
.count-picture,.count-words{
|
||||
.count-picture{
|
||||
width: 160px;
|
||||
text-align: center;
|
||||
}
|
||||
.count-words{
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -15,11 +15,13 @@ import {clsx} from "clsx";
|
||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import ButtonDeleteBatch from "@/pages/news/components/button-delete-batch.tsx";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const FilterCache: Partial<ApiArticleSearchParams> = {
|
||||
tags: [],
|
||||
}
|
||||
export default function NewEdit() {
|
||||
const {t} = useTranslation()
|
||||
const [state, setState] = useState<{
|
||||
checkAll?: boolean;
|
||||
showToTop?: boolean;
|
||||
@ -68,7 +70,7 @@ export default function NewEdit() {
|
||||
const handleDelete = (id) => {
|
||||
deleteByIds([id]).then(() => {
|
||||
refresh()
|
||||
showToast('删除成功', 'success')
|
||||
showToast(t('delete_success'), 'success')
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
|
||||
@ -80,15 +82,15 @@ export default function NewEdit() {
|
||||
</div>
|
||||
|
||||
<div className="news-list-container mt-2">
|
||||
<div className="controls flex justify-end mb-3 gap-2">
|
||||
<Space>
|
||||
<span>总共 {data?.list?.length || 0} 条</span>
|
||||
<span className={'text-blue-500'}>已选 {selectedRowKeys.length} 条</span>
|
||||
<div className="controls flex justify-end mb-3 gap-5">
|
||||
<Space size={20}>
|
||||
<span>{t('select.total',{count:data?.list?.length || 0})}</span>
|
||||
<span className={'text-blue-500'}>{t('select.selected_some',{count:selectedRowKeys.length})}</span>
|
||||
</Space>
|
||||
<div>
|
||||
<span className={'inline-block cursor-pointer mr-2'} onClick={() => {
|
||||
handleCheckAll(!state.checkAll)
|
||||
}}>全选</span>
|
||||
}}>{t('select.select_all')}</span>
|
||||
<Checkbox checked={state.checkAll && (!data?.list || selectedRowKeys.length == data?.list?.length)}
|
||||
onChange={e => {
|
||||
handleCheckAll(e.target.checked)
|
||||
@ -97,12 +99,12 @@ export default function NewEdit() {
|
||||
</div>
|
||||
<div className={styles.newListTable}>
|
||||
<div className="header row flex">
|
||||
<div className="col title">标题</div>
|
||||
<div className="col source source">来源</div>
|
||||
<div className="col count-picture">图片数</div>
|
||||
<div className="col count-words">字数</div>
|
||||
<div className="col time">时间</div>
|
||||
<div className="col operations">操作</div>
|
||||
<div className="col title">{t('news.title')}</div>
|
||||
<div className="col source">{t('news.source')}</div>
|
||||
<div className="col count-picture">{t('news.title_image_count')}</div>
|
||||
<div className="col count-words">{t('news.title_word_count')}</div>
|
||||
<div className="col time">{t('news.title_time')}</div>
|
||||
<div className="col operations">{t('news.title_operate')}</div>
|
||||
</div>
|
||||
<InfiniteScroller ref={scrollerRef} onCallback={(page) => {
|
||||
setParams(prev => ({
|
||||
@ -142,8 +144,8 @@ export default function NewEdit() {
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={'你确定要删除吗?'}
|
||||
description={'删除后需从新闻素材中重新选择'}
|
||||
title={t('news.delete_confirm')}
|
||||
description={<span dangerouslySetInnerHTML={{__html:t('news.delete_description')}}></span>}
|
||||
onConfirm={() => {
|
||||
handleDelete(item.id)
|
||||
}}
|
||||
|
@ -13,11 +13,13 @@ import ButtonNewsDownload from "@/pages/news/components/button-news-download.tsx
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import {useIndexArrayCache} from "@/hooks/useCache.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const FilterCache: Partial<ApiArticleSearchParams> = {
|
||||
time_flag: 1,
|
||||
}
|
||||
export default function NewsIndex() {
|
||||
const {t} = useTranslation()
|
||||
const [params, setParams] = useState<ApiArticleSearchParams>({
|
||||
pagination: {page: 1, limit: 12},
|
||||
...FilterCache
|
||||
@ -55,12 +57,12 @@ export default function NewsIndex() {
|
||||
})
|
||||
|
||||
const handleViewNewsDetail = (id: number) => {
|
||||
const {update, close} = showLoading('获取新闻详情...')
|
||||
const {update, close} = showLoading(`${t('news.get_detail')}...`)
|
||||
getById(id).then(res => {
|
||||
close()
|
||||
setActiveNews({...res, id})
|
||||
}).catch(() => {
|
||||
update('获取新闻详情失败', 'info')
|
||||
update(t('news.get_detail_error'), 'info')
|
||||
})
|
||||
}
|
||||
|
||||
@ -113,21 +115,21 @@ export default function NewsIndex() {
|
||||
<Checkbox
|
||||
checked={checkedId.includes(activeNews!.id)}
|
||||
onChange={() => handleCheckChange(activeNews!.id)}
|
||||
><span className="ml-[-4px]">选择</span></Checkbox>
|
||||
><span className="ml-[-4px]">{t('select.text')}</span></Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>}
|
||||
<div className="news-list-container">
|
||||
<div className="controls flex justify-end mb-3 gap-2">
|
||||
<Space>
|
||||
<span>总共 {data?.list?.length || 0} 条</span>
|
||||
<span className={'text-blue-500'}>已选 {checkedId.length} 条</span>
|
||||
<div className="controls flex justify-end mb-3 gap-5">
|
||||
<Space size={20}>
|
||||
<span>{t('select.total',{count:data?.list?.length || 0})}</span>
|
||||
<span className={'text-blue-500'}>{t('select.selected_some',{count:checkedId.length})}</span>
|
||||
</Space>
|
||||
<div>
|
||||
<span className={'inline-block cursor-pointer mr-2'} onClick={() => {
|
||||
handleCheckAll(!state.checkAll)
|
||||
}}>全选</span>
|
||||
}}>{t('select.select_all')}</span>
|
||||
<Checkbox checked={state.checkAll && checkedId.length == currentEnabledList.length} onChange={e => {
|
||||
handleCheckAll(e.target.checked)
|
||||
}}></Checkbox>
|
||||
@ -169,15 +171,15 @@ export default function NewsIndex() {
|
||||
</div>}
|
||||
</div>
|
||||
<div className="info text-gray-400 mt-4 text-sm">
|
||||
<div className="line-clamp-1">来源: <span>{item.data_source_name}</span></div>
|
||||
<div className="line-clamp-1">{t('news.source')}: <span>{item.data_source_name}</span></div>
|
||||
<div className="extras flex items-center justify-between gap-3">
|
||||
<div><span>{formatTime(item.publish_time, 'min')}</span></div>
|
||||
<div><span>图片数: {item.img_num}</span></div>
|
||||
<div><span>字数: {item.content_word_count}</span></div>
|
||||
<div><span>{t('news.image_count')}: {item.img_num}</span></div>
|
||||
<div><span>{t('news.word_count')}: {item.content_word_count}</span></div>
|
||||
<div
|
||||
className={` mt-1`}>
|
||||
{item.internal_article_id > 0 ?
|
||||
<span className={"inline-block text-gray-600"}>已推送</span> :
|
||||
<span className={"inline-block text-gray-600"}>{t('news.pushed')}</span> :
|
||||
<Checkbox checked={checkedId.includes(item.id)} onChange={() => {
|
||||
handleCheckChange(item.id)
|
||||
}}/>}
|
||||
|
@ -7,6 +7,7 @@ import styles from './../style.module.scss'
|
||||
|
||||
import useAuth from "@/hooks/useAuth.ts";
|
||||
import {useSmsCode} from "@/components/form/sms-code.tsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type FieldType = {
|
||||
username?: string;
|
||||
@ -15,6 +16,7 @@ type FieldType = {
|
||||
|
||||
|
||||
export default function FormLogin() {
|
||||
const {t} = useTranslation()
|
||||
const [disabled, setDisabled] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string>()
|
||||
@ -26,7 +28,7 @@ export default function FormLogin() {
|
||||
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
|
||||
if(disabled || loading) return
|
||||
if (!values.username || !/^1\d{10}$/.test(values.username)) {
|
||||
setError('账号或密码错误')
|
||||
setError(t("login.invalid_username_or_pwd"))
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
@ -38,7 +40,7 @@ export default function FormLogin() {
|
||||
};
|
||||
|
||||
return (<div className="form">
|
||||
<div className={'text-center text-xl pb-6 pt-8'}>欢迎登录</div>
|
||||
<div className={'text-center text-xl pb-6 pt-8'}>{t("login.welcome")}</div>
|
||||
<Form<FieldType>
|
||||
name="basic"
|
||||
style={{maxWidth: 600}}
|
||||
@ -53,18 +55,18 @@ export default function FormLogin() {
|
||||
>
|
||||
<Form.Item<FieldType> name="username">
|
||||
<div className={styles.loginBox}>
|
||||
<Input size={'large'} variant={'borderless'} placeholder="请输入账号"/>
|
||||
<Input size={'large'} variant={'borderless'} placeholder={t("login.username")}/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item name="password">
|
||||
|
||||
<div className={styles.loginBox}>
|
||||
<Input style={{borderRadius: 20}} size={'large'} variant={'borderless'}
|
||||
placeholder="请输入验证码"/>
|
||||
placeholder={t("login.password")}/>
|
||||
<span
|
||||
className={clsx(`text-nowrap mr-1 text-sm ${countdown > 0 || sending || !phone ? 'text-gray-400 cursor-not-allowed' : 'text-blue-500 cursor-pointer'}`)}
|
||||
onClick={() => sendCode(phone)}>
|
||||
{sending ? '发送中...' : (countdown > 0 ? `${Math.ceil(countdown / 1000)} s` : '获取验证码')}
|
||||
{sending ? `${t('login.code_sending')}...` : (countdown > 0 ? `${Math.ceil(countdown / 1000)} s` : t('login.send_sms_code'))}
|
||||
</span>
|
||||
</div>
|
||||
</Form.Item>
|
||||
@ -75,7 +77,7 @@ export default function FormLogin() {
|
||||
<div className="absolute text-red-500 text-center inset-x-0" style={{top: -34}}>{error}</div>
|
||||
<Button loading={loading} type="primary" size={'large'} htmlType="submit"
|
||||
block shape={'round'}>
|
||||
{loading ? '登录中...' : '立即登录'}
|
||||
{loading ? t("login.loading") : t('login.text')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
@ -3,10 +3,12 @@ import React, {useState} from "react";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {push2room, VideoStatus} from "@/service/api/video.ts";
|
||||
import {IconArrowRight, IconWarningCircle} from "@/components/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
|
||||
export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];onSuccess?:()=>void; }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const {t} = useTranslation()
|
||||
const handlePush = () => {
|
||||
setLoading(true)
|
||||
// 只需要已经生成视频的数据id
|
||||
@ -14,9 +16,9 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on
|
||||
push2room(vids).then(() => {
|
||||
props.onSuccess?.()
|
||||
if(props.ids.length == vids.length){
|
||||
showToast('一键推流成功,已推流至数字人直播间,请前往数字人直播间页面查看!', 'success')
|
||||
showToast(t("video.push_success"), 'success')
|
||||
}else{
|
||||
showToast('选择视频中有部分视频还在生成中无法推送,推流成功视频前往数字人直播间页面查看!', 'success')
|
||||
showToast(t("video.push_failed"), 'success')
|
||||
}
|
||||
}).catch(showErrorToast).finally(() => {
|
||||
setLoading(false)
|
||||
@ -25,14 +27,14 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on
|
||||
const onPushClick = () => {
|
||||
if(loading) return;
|
||||
if (props.ids.length === 0) {
|
||||
showToast('请选择要推流的新闻', 'warning')
|
||||
showToast(t("video.push_empty"), 'warning')
|
||||
return
|
||||
}
|
||||
Modal.confirm({
|
||||
wrapClassName:'root-modal-confirm',
|
||||
title: '操作提示',
|
||||
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
|
||||
content: '是否确定一键推流选中新闻视频??',
|
||||
content: t("video.push_confirm"),
|
||||
onOk: handlePush
|
||||
})
|
||||
}
|
||||
@ -44,7 +46,7 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on
|
||||
className='bg-[#4096ff] hover:bg-blue-600 text-white'
|
||||
onClick={onPushClick}
|
||||
>
|
||||
<span className={'text'}>一键推流</span>
|
||||
<span className={'text'}>{t("video.push_to_live")}</span>
|
||||
<IconArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -2,11 +2,11 @@ import {Checkbox, Empty, Space} from "antd";
|
||||
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {DndContext} from "@dnd-kit/core";
|
||||
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
|
||||
import {useSetState, useTimeout} from "ahooks";
|
||||
import {useSetState} from "ahooks";
|
||||
|
||||
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
||||
import {deleteByIds, getList, modifyOrder, VideoStatus} from "@/service/api/video.ts";
|
||||
import {deleteFromList, getList, modifyOrder, VideoStatus} from "@/service/api/video.ts";
|
||||
import {formatDuration} from "@/util/strings.ts";
|
||||
import ButtonBatch from "@/components/button-batch.tsx";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
@ -16,8 +16,10 @@ import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import {IconDelete} from "@/components/icons";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export default function VideoIndex() {
|
||||
const {t} = useTranslation()
|
||||
const [editId, setEditId] = useState(-1)
|
||||
const loc = useLocation()
|
||||
const [videoData, setVideoData] = useState<VideoInfo[]>([])
|
||||
@ -55,7 +57,7 @@ export default function VideoIndex() {
|
||||
// 判断是否有生成中的视频
|
||||
if (list.filter(s => s.status == VideoStatus.Generating).length > 0) {
|
||||
// 每5s重新获取一次最新数据
|
||||
setTimer(()=>setTimeout(() => loadList(false), 5000) as number);
|
||||
setTimer(()=>setTimeout(() => loadList(false), 5000) as any);
|
||||
}
|
||||
}).catch(showErrorToast)
|
||||
.finally(()=>{
|
||||
@ -96,10 +98,10 @@ export default function VideoIndex() {
|
||||
const handleModifySort = (items: VideoInfo[]) => {
|
||||
|
||||
modifyOrder(items.map(s => s.id)).then(() => {
|
||||
showToast('调整视频顺序成功!', 'success')
|
||||
showToast(t('video.sort_modify_success'), 'success')
|
||||
}).catch(() => {
|
||||
loadList();
|
||||
showToast('调整视频顺序失败,请重试!', 'warning')
|
||||
showToast(t('video.sort_modify_failed'), 'warning')
|
||||
})
|
||||
|
||||
return ()=>{
|
||||
@ -131,8 +133,8 @@ export default function VideoIndex() {
|
||||
}
|
||||
}, [videoData, scrollerRef])
|
||||
const processDeleteVideo = async (ids: Id[]) => {
|
||||
deleteByIds(ids).then(() => {
|
||||
showToast('删除成功!', 'success')
|
||||
deleteFromList(ids).then(() => {
|
||||
showToast(t('delete_success'), 'success')
|
||||
loadList()
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
@ -142,7 +144,7 @@ export default function VideoIndex() {
|
||||
<div className="flex">
|
||||
<div className="video-player-container mr-16 w-[360px] flex items-center">
|
||||
<div>
|
||||
<div className="text-center text-base text-gray-400">预览视频 - 点击视频列表播放</div>
|
||||
<div className="text-center text-base text-gray-400">{t("generating.title")}</div>
|
||||
<div className="video-player flex items-center mt-2">
|
||||
<div className=" w-[360px] h-[636px] rounded overflow-hidden">
|
||||
<Player
|
||||
@ -170,12 +172,12 @@ export default function VideoIndex() {
|
||||
<div className="pl-[70px]"></div>
|
||||
<div className="flex items-center">
|
||||
<Space>
|
||||
<span>总共 {videoData.length || 0} 条</span>
|
||||
<span className={'text-blue-500'}>已选 {checkedIdArray.length} 条</span>
|
||||
<span>{t('select.selected_some',{count:videoData.length || 0})}</span>
|
||||
<span className={'text-blue-500'}>{t('select.selected_some',{count:checkedIdArray.length})}</span>
|
||||
</Space>
|
||||
<button className="hover:text-blue-300 text-gray-400 ml-2"
|
||||
onClick={handleAllCheckedChange}>
|
||||
<span className="text-sm mr-2">全选</span>
|
||||
<span className="text-sm mr-2">{t("select.select_all")}</span>
|
||||
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
|
||||
</button>
|
||||
<Checkbox checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
|
||||
@ -185,10 +187,10 @@ export default function VideoIndex() {
|
||||
<div className="list-header">
|
||||
<div className="list-row header-row">
|
||||
<div className="col number">No.</div>
|
||||
<div className="col cover">缩略图</div>
|
||||
<div className="col title">标题</div>
|
||||
<div className="col generated-time">生成时间</div>
|
||||
<div className="col operation"></div>
|
||||
<div className="col cover">{t('video.title_thumb')}</div>
|
||||
<div className="col title">{t('video.title')}</div>
|
||||
<div className="col generated-time">{t('video.title_generated_time')}</div>
|
||||
<div className="col operation">{t('video.title_operation')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<InfiniteScroller loading={state.loading} ref={scrollerRef} onScroll={top => setState({showToTop: top > 30})}>
|
||||
@ -256,18 +258,18 @@ export default function VideoIndex() {
|
||||
<div className="page-action">
|
||||
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
|
||||
{checkedIdArray.length > 0 && <ButtonBatch
|
||||
onProcess={deleteByIds}
|
||||
onProcess={deleteFromList}
|
||||
selected={checkedIdArray}
|
||||
emptyMessage={`请选择要删除的新闻视频`}
|
||||
title={`已选择${checkedIdArray.length}条,确定要全部删除吗?`}
|
||||
emptyMessage={t('video.delete_empty')}
|
||||
title={t('video.delete_description',{count:checkedIdArray.length})}
|
||||
className='bg-gray-300 hover:bg-gray-400 text-white'
|
||||
confirmMessage={`删除后需重新生成视频`}
|
||||
confirmMessage={t('video.delete_confirm')}
|
||||
onSuccess={() => {
|
||||
showToast('删除成功!', 'success')
|
||||
showToast(t('delete_success'), 'success')
|
||||
loadList()
|
||||
}}
|
||||
>
|
||||
<span className="text">批量删除</span>
|
||||
<span className="text">{t('delete_batch')}</span>
|
||||
<IconDelete/>
|
||||
</ButtonBatch>}
|
||||
<ButtonPush2Room ids={checkedIdArray} list={videoData} onSuccess={loadList}/>
|
||||
|
@ -29,12 +29,15 @@ const router = createBrowserRouter([
|
||||
|
||||
// future={{v7_startTransition: true,v7_relativeSplatPath: true}}
|
||||
const AppRouter = () => {
|
||||
const {t} = useTranslation();
|
||||
const {t,i18n:langConfig} = useTranslation();
|
||||
const {i18n} = useConfig();
|
||||
useEffect(() => {
|
||||
if (i18n && i18n == 'zh-CN') {
|
||||
dayjs.locale('zh-cn');
|
||||
}else{
|
||||
dayjs.locale('en')
|
||||
}
|
||||
langConfig.changeLanguage(i18n).then(()=>console.log('change lang to ',i18n))
|
||||
}, [i18n])
|
||||
|
||||
return (<ConfigProvider
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Outlet, useLocation, useNavigate} from "react-router-dom";
|
||||
import {Divider, Dropdown, MenuProps} from "antd";
|
||||
import {Button, Divider, Dropdown, MenuProps} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
|
||||
import AuthGuard from "@/routes/layout/auth-guard.tsx";
|
||||
@ -13,6 +13,7 @@ import {hidePhone} from "@/util/strings.ts";
|
||||
import {defaultCache} from "@/hooks/useCache.ts";
|
||||
import {IconVideo} from "@/components/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import useConfig from "@/hooks/useConfig.ts";
|
||||
|
||||
|
||||
type LayoutProps = {
|
||||
@ -31,7 +32,7 @@ const NavigationUserContainer = () => {
|
||||
key: 'profile',
|
||||
label: <div className="nav-item" onClick={() => navigate('/history')}>
|
||||
<IconVideo />
|
||||
<span className={"nav-text"}>视频库</span>
|
||||
<span className={"nav-text"}>{t('history.text')}</span>
|
||||
</div>,
|
||||
},
|
||||
// {
|
||||
@ -43,7 +44,7 @@ const NavigationUserContainer = () => {
|
||||
className={`flex items-center rounded-3xl ${user ? 'bg-[#e3eeff]' : 'bg-primary-blue'} p-1 pr-2 cursor-pointer rounded`}>
|
||||
<UserAvatar className="user-avatar size-7"/>
|
||||
{user ? <span className={"username ml-2 text-sm"}>{hidePhone(user.nickname)}</span> : (
|
||||
<span className="text-sm mx-2 text-white">{t('login.text')}</span>
|
||||
<span className="text-sm mx-2 text-white">{t('login.title')}</span>
|
||||
)}
|
||||
</div>)
|
||||
return (<div className={"flex items-center justify-between gap-2 ml-10"}>
|
||||
@ -65,7 +66,7 @@ const NavigationUserContainer = () => {
|
||||
</div>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<div className="logout">
|
||||
<div onClick={handleLogout}>退出登录</div>
|
||||
<div onClick={handleLogout}>{t('user.logout')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -76,6 +77,7 @@ const NavigationUserContainer = () => {
|
||||
</div>)
|
||||
}
|
||||
export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
|
||||
const {i18n,onChangeLocalization} = useConfig();
|
||||
return (<div className={'dashboard-layout min-h-screen'}>
|
||||
<div className="min-h-screen w-full">
|
||||
<div className="app-header">
|
||||
@ -84,6 +86,13 @@ export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
|
||||
</div>
|
||||
<DashboardNavigation/>
|
||||
<div className="flex items-center">
|
||||
{
|
||||
i18n == 'zh-CN'?(
|
||||
<Button className="ml-2" onClick={()=>onChangeLocalization('en-US')}>Change To EN</Button>
|
||||
):(
|
||||
<Button className="ml-2" onClick={()=>onChangeLocalization('zh-CN')}>显示中文</Button>
|
||||
)
|
||||
}
|
||||
<NavigationUserContainer/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,42 +2,46 @@ import {clsx} from "clsx";
|
||||
import {NavLink} from "react-router-dom";
|
||||
import {IconNavigationArrow} from "@/components/icons";
|
||||
import useAuth from "@/hooks/useAuth.ts";
|
||||
import {useMemo} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
|
||||
const NavItems = [
|
||||
{
|
||||
key: 'news',
|
||||
name: '新闻素材',
|
||||
icon: 'news',
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
name: '新闻编辑',
|
||||
icon: 'e',
|
||||
path: '/edit'
|
||||
},
|
||||
{
|
||||
key: 'create',
|
||||
name: '视频生成',
|
||||
icon: 'ai',
|
||||
path: '/create'
|
||||
},
|
||||
// {
|
||||
// key: 'library',
|
||||
// name: '视频库',
|
||||
// icon: '+',
|
||||
// path:'/library'
|
||||
// },
|
||||
{
|
||||
key: 'live',
|
||||
name: '数字人直播间',
|
||||
icon: 'v',
|
||||
path: '/live'
|
||||
}
|
||||
]
|
||||
|
||||
export function DashboardNavigation() {
|
||||
const {t,i18n} = useTranslation()
|
||||
const {user} = useAuth()
|
||||
const NavItems = useMemo(()=>([
|
||||
{
|
||||
key: 'news',
|
||||
name: t('nav.materials'),
|
||||
icon: 'news',
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
name: t('nav.editing'),
|
||||
icon: 'e',
|
||||
path: '/edit'
|
||||
},
|
||||
{
|
||||
key: 'create',
|
||||
name: t('nav.generating'),
|
||||
icon: 'ai',
|
||||
path: '/create'
|
||||
},
|
||||
// {
|
||||
// key: 'library',
|
||||
// name: '视频库',
|
||||
// icon: '+',
|
||||
// path:'/library'
|
||||
// },
|
||||
{
|
||||
key: 'live',
|
||||
name: t('nav.live'),
|
||||
icon: 'v',
|
||||
path: '/live'
|
||||
}
|
||||
]),[i18n.language])
|
||||
return (<div className={'flex app-main-navigation'}>
|
||||
{NavItems.map((it, idx) => (<div key={idx} className={"flex items-center"}>
|
||||
{user ? <NavLink to={it.path} className={clsx('nav-item cursor-pointer items-center')}>
|
||||
|
@ -20,9 +20,10 @@ export function getById(id: Id) {
|
||||
return post<ArticleDetail>({url: '/article/detail/' + id})
|
||||
}
|
||||
|
||||
export function save(title: string, content_group: BlockContent[][], id?: number) {
|
||||
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
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {post} from "@/service/request.ts";
|
||||
import {get, post} from "@/service/request.ts";
|
||||
|
||||
export function playState() {
|
||||
return post<LiveState>({url: '/room/playing'})
|
||||
@ -15,3 +15,10 @@ export function modifyOrder(ids: Id[]) {
|
||||
export function deleteByIds(ids: Id[]) {
|
||||
return post('/room/remove', {ids})
|
||||
}
|
||||
|
||||
export function getLiveUrl() {
|
||||
return get<{flv_url:string}>({
|
||||
url: '/tencent/get_pull_url',
|
||||
baseURL: '/api/v1'
|
||||
})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import {post} from "@/service/request.ts";
|
||||
import {get, post} from "@/service/request.ts";
|
||||
|
||||
export function getList() {
|
||||
return post<DataList<VideoInfo>>('/video/list')
|
||||
@ -7,12 +7,7 @@ export function search(params:VideoSearchParams) {
|
||||
return post<DataList<VideoInfo>>('/video/search',params)
|
||||
}
|
||||
export function deleteHistories(ids: Id[]) {
|
||||
console.log('deleteHistories',ids)
|
||||
return new Promise<number>((resolve)=>{
|
||||
setTimeout(()=>{
|
||||
resolve(1)
|
||||
},2000)
|
||||
})
|
||||
return post('/video/history/remove', {ids})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -21,11 +16,12 @@ export function deleteHistories(ids: Id[]) {
|
||||
* @param content_group
|
||||
* @param article_id
|
||||
*/
|
||||
export function regenerate(title: string, content_group: BlockContent[][], article_id?: Id) {
|
||||
export function regenerate(title: string, metahuman_text: string, content_group: BlockContent[][], article_id?: Id) {
|
||||
return post<{ content: string }>({
|
||||
url: '/video/regenerate',
|
||||
data: {
|
||||
title,
|
||||
metahuman_text,
|
||||
content_group,
|
||||
article_id
|
||||
}
|
||||
@ -36,8 +32,8 @@ export function getById(id: Id) {
|
||||
return post<VideoInfo>({url: '/video/detail/' + id})
|
||||
}
|
||||
|
||||
export function deleteByIds(ids: Id[]) {
|
||||
return post('/video/remove', {ids})
|
||||
export function deleteFromList(ids: Id[]) {
|
||||
return post('/video/outside', {ids})
|
||||
}
|
||||
|
||||
|
||||
|
@ -71,12 +71,13 @@ export function post<T>(params: RequestOption | string, _data?: AllType) {
|
||||
})
|
||||
}
|
||||
|
||||
export function get<T>(params: RequestOption) {
|
||||
if (params.data) {
|
||||
params.url += (params.url.indexOf('?') === -1 ? '?' : '&') + stringify(params.data)
|
||||
export function get<T>(params: RequestOption|string) {
|
||||
const options = typeof params === 'string' ? {url: params} : params;
|
||||
if (options.data) {
|
||||
options.url += (options.url.indexOf('?') === -1 ? '?' : '&') + stringify(options.data)
|
||||
}
|
||||
return request<T>({
|
||||
...params,
|
||||
...options,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
1
src/types/core.d.ts
vendored
1
src/types/core.d.ts
vendored
@ -31,6 +31,7 @@ declare interface ArticleContentGroup {
|
||||
declare interface ArticleDetail {
|
||||
id: number;
|
||||
title: string;
|
||||
metahuman_text: string;
|
||||
content_group: BlockContent[][]
|
||||
}
|
||||
|
||||
|
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -12,6 +12,7 @@ declare const AppConfig: {
|
||||
// 登录用户信息 key
|
||||
AUTHED_PERSON_DATA_KEY: string;
|
||||
API_PREFIX: string;
|
||||
ONLY_LIVE: string;
|
||||
};
|
||||
declare const AppMode: 'test' | 'production' | 'development';
|
||||
|
||||
|
@ -21,6 +21,7 @@ export default defineConfig(({mode}) => {
|
||||
API_PREFIX: process.env.APP_API_PREFIX || '/mgmt/v1/metahuman',
|
||||
AUTH_TOKEN_KEY: process.env.AUTH_TOKEN_KEY || AUTH_TOKEN_KEY,
|
||||
AUTHED_PERSON_DATA_KEY: process.env.AUTHED_PERSON_DATA_KEY || 'digital-person-user-info',
|
||||
ONLY_LIVE: process.env.ONLY_LIVE || 'no',
|
||||
}),
|
||||
AppMode: JSON.stringify(mode),
|
||||
AppBuildVersion: JSON.stringify(AppPackage.name + '-' + AppPackage.version + '-' + dayjs().format('YYYYMMDDHH_mmss'))
|
||||
@ -46,7 +47,7 @@ export default defineConfig(({mode}) => {
|
||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/api': {
|
||||
target: `http://${devServerHost}`,
|
||||
target: `${devServerHost}`,
|
||||
changeOrigin: true,
|
||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user