Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
0a4bb5426e | |||
6fc064fbc8 | |||
611a00a550 | |||
0ccbfb5f5a | |||
74f37055bc | |||
d270d615a2 | |||
b7b15e7471 | |||
a2b5df22f8 | |||
116c171249 | |||
c8e5d8a6ab | |||
99323df02b | |||
42e2d3fcc0 | |||
cbd476d1e2 | |||
1be407d34e | |||
605a769b89 | |||
4e23bb623f | |||
cea77ea231 | |||
be34a8bc9b | |||
fdb125c7ba | |||
bcbdac6673 | |||
0520cb8e1d | |||
4dee84a459 | |||
3d47964580 | |||
17c9fa6c10 | |||
500c849140 | |||
64ee960846 | |||
e61bfcc26c | |||
de7088f642 |
12
.prettierignore
Normal file
12
.prettierignore
Normal file
@ -0,0 +1,12 @@
|
||||
/node_modules
|
||||
package*.json
|
||||
.gitignore
|
||||
*.local
|
||||
*_local
|
||||
__test__
|
||||
.ide
|
||||
.vscode
|
||||
.idea
|
||||
test
|
||||
dist
|
||||
public
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
}
|
16
package.json
16
package.json
@ -19,9 +19,9 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"ahooks": "^3.8.1",
|
||||
"antd": "^5.22.5",
|
||||
"axios": "^1.7.7",
|
||||
@ -30,7 +30,7 @@
|
||||
"dayjs": "^1.11.11",
|
||||
"file-saver": "^2.0.5",
|
||||
"flv.js": "^1.6.2",
|
||||
"i18next": "^24.2.1",
|
||||
"i18next": "^25.0.1",
|
||||
"jszip": "^3.10.1",
|
||||
"qs": "^6.12.1",
|
||||
"react": "^18.3.1",
|
||||
@ -51,15 +51,15 @@
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"postcss": "^8.4.40",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.2"
|
||||
},
|
||||
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import AppRouter from "@/routes";
|
||||
import {ConfigProvider} from "@/contexts/config";
|
||||
import {AuthProvider} from "@/contexts/auth";
|
||||
import LivePlayer from "@/pages/live-player";
|
||||
import React from 'react';
|
||||
// import LivePlayer from "@/pages/live-player";
|
||||
|
||||
console.log(`APP-BUILD-AT: ${AppBuildVersion}`)
|
||||
|
||||
const LivePlayer = React.lazy(() => import('@/pages/live-player'));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider>
|
||||
|
@ -170,7 +170,7 @@
|
||||
@apply text-sm;
|
||||
background: none;
|
||||
.col{
|
||||
@apply text-sm text-gray-800;
|
||||
@apply text-base text-gray-800;
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
@ -180,7 +180,7 @@
|
||||
}
|
||||
|
||||
.col {
|
||||
@apply flex items-center relative pl-4 text-center justify-center;
|
||||
@apply flex items-center relative pl-4 text-center justify-center text-sm;
|
||||
height: 60px;
|
||||
|
||||
&:after {
|
||||
@ -209,7 +209,7 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply flex-1 text-base;
|
||||
@apply flex-1;
|
||||
}
|
||||
|
||||
.generated-time {
|
||||
@ -218,7 +218,7 @@
|
||||
|
||||
.operation {
|
||||
@apply flex items-center ml-2 text-lg text-gray-400 justify-center;
|
||||
width: 150px;
|
||||
width: 180px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@ -230,34 +230,7 @@
|
||||
//max-height: calc(100vh - var(--app-header-header) - 200px);
|
||||
//overflow: auto;
|
||||
}
|
||||
.root-modal-confirm{
|
||||
z-index: calc(var(--header-z-index) + 1) !important;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
//anticon anticon-exclamation-circle
|
||||
.ant-modal-confirm-title{
|
||||
font-size: 20px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.ant-modal-confirm-content{
|
||||
margin-top: 10px;
|
||||
margin-left: -30px;
|
||||
}
|
||||
.icon-warning{
|
||||
|
||||
}
|
||||
.ant-modal-confirm-btns{
|
||||
@apply mt-8;
|
||||
button{
|
||||
@apply rounded-2xl py-4 px-8;
|
||||
}
|
||||
.ant-btn-default{
|
||||
@apply bg-white shadow-none text-popconfirm-btn-cancel border border-popconfirm-btn-cancel hover:border-popconfirm-btn-cancel hover:text-popconfirm-btn-cancel hover:bg-white hover:bg-popconfirm-btn-cancel/10;
|
||||
}
|
||||
.ant-btn-primary{
|
||||
@apply bg-white shadow-none text-popconfirm-bg border border-popconfirm-bg hover:text-popconfirm-bg hover:bg-white hover:bg-popconfirm-btn-primary-hover/10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-player {
|
||||
.video-js {
|
||||
@ -290,7 +263,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.w-min-60px{
|
||||
min-width: 60px;
|
||||
}
|
||||
.list-scroller-container {
|
||||
overflow: auto;
|
||||
margin-right: -20px;
|
||||
@ -342,35 +317,74 @@
|
||||
}
|
||||
.popconfirm-main{
|
||||
.ant-popover-inner{
|
||||
@apply bg-white px-6 py-6 rounded-xl;
|
||||
border-radius: 4px;
|
||||
padding: 20px 24px;
|
||||
min-width: 360px;
|
||||
background-color: #f2f2f2;
|
||||
box-shadow: 0 0 10px rgba(25, 25, 25, 0.1);
|
||||
}
|
||||
.icon-warning{
|
||||
@apply text-red-500;
|
||||
font-size: 20px;
|
||||
transform: translateY(5px);
|
||||
margin-right: 10px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.ant-popconfirm-message{
|
||||
.ant-popconfirm-title{
|
||||
@apply text-xl font-bold;
|
||||
@apply text-xl;
|
||||
font-weight: 400;
|
||||
}
|
||||
.ant-popconfirm-description{
|
||||
@apply mt-4 text-gray-400 text-sm;
|
||||
margin-left: -30px;
|
||||
@apply mt-2 text-gray-600 text-sm;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
.ant-popconfirm-buttons{
|
||||
@apply mt-8;
|
||||
@apply mt-6;
|
||||
button{
|
||||
@apply rounded-2xl py-4 px-8;
|
||||
font-size: 14px;
|
||||
line-height: 1.5714285714285714;
|
||||
height: 32px;
|
||||
padding: 4px 15px;
|
||||
border-radius: 4px;
|
||||
min-width: 88px;
|
||||
}
|
||||
.ant-btn-default{
|
||||
@apply bg-white shadow-none text-popconfirm-btn-cancel border border-popconfirm-btn-cancel hover:border-popconfirm-btn-cancel hover:text-popconfirm-btn-cancel hover:bg-white hover:bg-popconfirm-btn-cancel/10;
|
||||
//.ant-btn-default{
|
||||
// @apply bg-white shadow-none text-popconfirm-btn-cancel border border-popconfirm-btn-cancel hover:border-popconfirm-btn-cancel hover:text-popconfirm-btn-cancel hover:bg-white hover:bg-popconfirm-btn-cancel/10;
|
||||
//}
|
||||
//.ant-btn-primary{
|
||||
// @apply bg-white shadow-none text-popconfirm-bg border border-popconfirm-bg hover:text-popconfirm-bg hover:bg-white hover:bg-popconfirm-btn-primary-hover/10;
|
||||
//}
|
||||
}
|
||||
.ant-btn-primary{
|
||||
@apply bg-white shadow-none text-popconfirm-bg border border-popconfirm-bg hover:text-popconfirm-bg hover:bg-white hover:bg-popconfirm-btn-primary-hover/10;
|
||||
}
|
||||
.root-modal-confirm{
|
||||
z-index: calc(var(--header-z-index) + 1) !important;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
//anticon anticon-exclamation-circle
|
||||
.icon-warning{
|
||||
@apply text-red-500;
|
||||
font-size: 20px;
|
||||
//transform: translateY(5px);
|
||||
margin-right: 20px;
|
||||
}
|
||||
.ant-modal-confirm-title{
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
.ant-modal-confirm-content{
|
||||
margin-top: 10px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.ant-modal-confirm-btns{
|
||||
@apply mt-6;
|
||||
button{
|
||||
font-size: 14px;
|
||||
line-height: 1.5714285714285714;
|
||||
height: 32px;
|
||||
padding: 4px 15px;
|
||||
border-radius: 4px;
|
||||
min-width: 88px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -391,7 +405,11 @@
|
||||
.ant-message {
|
||||
z-index: var(--message-z-index);
|
||||
}
|
||||
|
||||
.background-template-popover{
|
||||
.ant-popover-inner{
|
||||
background-color: #E9E9E9;
|
||||
}
|
||||
}
|
||||
.ant-modal-root {
|
||||
.ant-modal-mask {
|
||||
@apply bg-black/20;
|
||||
@ -412,7 +430,7 @@
|
||||
}
|
||||
|
||||
.ant-modal-confirm-btns {
|
||||
margin-top: 40px;
|
||||
@apply mt-6;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -429,24 +447,48 @@
|
||||
}
|
||||
|
||||
.article-title {
|
||||
@apply px-6 pt-10 pb-6;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.09);
|
||||
}
|
||||
|
||||
.article-body {
|
||||
@apply p-6
|
||||
@apply p-6 pt-1;
|
||||
}
|
||||
|
||||
.modal-control-footer {
|
||||
@apply p-6
|
||||
}
|
||||
|
||||
.hot-news-list{
|
||||
@apply focus-within:bg-[#e6ebf1] focus-within:border-gray-100;
|
||||
}
|
||||
.input-box {
|
||||
// focus-within:shadow
|
||||
@apply bg-[#f8f8f8] border border-transparent w-full px-4 py-2 focus-within:bg-[#f0f0f0] focus-within:border-gray-300;
|
||||
@apply text-base bg-[#f8f8f8] border border-transparent w-full px-3 focus-within:bg-[#f3f3f3] focus-within:border-gray-100;
|
||||
border-radius: 8px;
|
||||
color:#3d3d3d;
|
||||
}
|
||||
.main-human-text{
|
||||
@apply focus-within:bg-[#e6ebf1] focus-within:border-gray-100;
|
||||
}
|
||||
.main-human-text-input{
|
||||
// focus-within:shadow
|
||||
@apply text-base bg-[#f8f8f8] border border-transparent w-full p-2;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-language{
|
||||
@apply relative text-gray-500 p-1.5 hover:bg-[#e3eeff] hover:text-gray-600 rounded cursor-pointer text-xl;
|
||||
}
|
||||
@keyframes animation_loading {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.icon-generating{
|
||||
animation: animation_loading 6s linear infinite;
|
||||
}
|
||||
// 全局按钮
|
||||
.page-action {
|
||||
@apply fixed right-10 bottom-10 flex flex-col gap-4 z-10;
|
||||
@ -456,6 +498,7 @@
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
padding-left: 15px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
4
src/assets/images/error/error_img.svg
Normal file
4
src/assets/images/error/error_img.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3726" width="256" height="256">
|
||||
<path d="M896 213.333333v281.173334l-97.706667-98.133334c-16.64-16.64-43.946667-16.64-60.586666 0L597.333333 537.173333 456.96 396.8a42.496 42.496 0 0 0-60.16 0L256 537.173333 128 408.746667V213.333333c0-46.933333 38.4-85.333333 85.333333-85.333333h597.333334c46.933333 0 85.333333 38.4 85.333333 85.333333z m-128 273.92l128 128.426667V810.666667c0 46.933333-38.4 85.333333-85.333333 85.333333H213.333333c-46.933333 0-85.333333-38.4-85.333333-85.333333v-280.746667l97.706667 97.706667c16.64 16.64 43.52 16.64 60.16 0l140.8-140.8 140.373333 140.373333c16.64 16.64 43.52 16.64 60.16 0l140.8-139.946667z"
|
||||
fill="#e6e6e6" />
|
||||
</svg>
|
After Width: | Height: | Size: 770 B |
BIN
src/assets/images/error/ic_broken_image.png
Normal file
BIN
src/assets/images/error/ic_broken_image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
61
src/components/article/HotNews.tsx
Normal file
61
src/components/article/HotNews.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import styles from './article.module.scss'
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Input, Switch} from "antd";
|
||||
import {useMemo} from "react";
|
||||
|
||||
type HotNewsProps = {
|
||||
news: string[];
|
||||
mode: string;
|
||||
onValueChange: (values: {
|
||||
news: string[],
|
||||
mode: string
|
||||
}) => void;
|
||||
}
|
||||
|
||||
|
||||
function HotNews({news, mode, onValueChange}: HotNewsProps) {
|
||||
const {t,i18n} = useTranslation()
|
||||
const demoPlaceholderList = useMemo(()=>{
|
||||
return i18n.language == 'zh-CN' ? [
|
||||
'例:韩正会见英国汇丰集团主席',
|
||||
'例: 丁薛祥出席全国高校毕业生等青年就业创业工作视频...',
|
||||
'例:俄称乌方再度袭击俄能源设施 乌称击退俄军进攻',
|
||||
] : [
|
||||
'eg.China\'s tax policies invigorate private economy',
|
||||
'eg.China\'s central bank conducts reverse repos Wednesday',
|
||||
'eg.China intensifies law enforcement in cyberspace',
|
||||
]
|
||||
},[i18n.language])
|
||||
const handleValueChange = (value: string, index: number) => {
|
||||
const values = [...news]
|
||||
values[index] = value
|
||||
onValueChange({news: values, mode})
|
||||
}
|
||||
return (
|
||||
<div className={`${styles.hotNews} mt-3`}>
|
||||
<div className="flex justify-between">
|
||||
<div className="area-title">
|
||||
<span className="title">{t("modal.hot_news.title")}</span>
|
||||
</div>
|
||||
<div className="mode">
|
||||
<span className="mr-2">{mode == 'auto' ? t("modal.hot_news.edit_auto") : t("modal.hot_news.edit_manual")}</span>
|
||||
<Switch size="small" checked={mode != 'auto'} onChange={checked => {
|
||||
onValueChange({news, mode: checked ? 'manual' : 'auto'})
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
{mode != 'auto' && <div className="hot-news-list panel-body p-3 ">
|
||||
{news.map((item, index) => <div key={index} className={`hot-news-item bg-gray-50 ${index == 0?'':'mt-3'} rounded-xl`}>
|
||||
<Input
|
||||
variant={"borderless"}
|
||||
readOnly={mode == 'auto'}
|
||||
placeholder={mode != 'auto' ? demoPlaceholderList[index] : ''}
|
||||
value={item}
|
||||
onChange={e => handleValueChange(e.target.value, index)}/>
|
||||
</div>)}
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HotNews
|
@ -46,7 +46,7 @@
|
||||
@apply flex gap-4;
|
||||
:global{
|
||||
.area-title{
|
||||
@apply text-gray-400 text-sm text-gray-800;
|
||||
@apply text-gray-400 text-base text-gray-800;
|
||||
}
|
||||
.digital-person{
|
||||
width: 450px;
|
||||
@ -133,3 +133,9 @@
|
||||
.textarea {
|
||||
@apply border-0
|
||||
}
|
||||
|
||||
// hot news
|
||||
.hotNews{
|
||||
.title{}
|
||||
|
||||
}
|
BIN
src/components/article/assets/bg1.jpg
Normal file
BIN
src/components/article/assets/bg1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
src/components/article/assets/bg2.jpg
Normal file
BIN
src/components/article/assets/bg2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
@ -8,6 +8,7 @@ import ImageList from "@/components/article/list.tsx";
|
||||
import { BlockText} from "./item.tsx";
|
||||
import styles from './article.module.scss'
|
||||
import {useTranslation} from "react-i18next";
|
||||
import ModalWarning from "@/components/icons/ModalWarning.tsx";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
@ -86,8 +87,9 @@ export default function ArticleBlock(
|
||||
rootClassName={'popconfirm-main'}
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={<div style={{minWidth: 150}}><span>{t('news.edit_delete_group_confirm')}</span></div>}
|
||||
icon={<ModalWarning.Icon />}
|
||||
title={<ModalWarning.Title />}
|
||||
description={<div style={{minWidth: 150}}><span>{t('news.edit_delete_group_confirm')}</span></div>}
|
||||
onConfirm={onRemove}
|
||||
okText={t('delete')}
|
||||
cancelText={t('cancel')}
|
||||
|
@ -1,17 +1,25 @@
|
||||
import {Modal} from "antd";
|
||||
import ArticleGroup from "@/components/article/group.tsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useSetState} from "ahooks";
|
||||
import * as article from "@/service/api/article.ts";
|
||||
import {regenerate} from "@/service/api/video.ts";
|
||||
import {push2video} from "@/service/api/article.ts";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import { Modal, App, Radio, Popover } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSetState } from 'ahooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
import * as article from '@/service/api/article.ts';
|
||||
import { regenerate } from '@/service/api/video.ts';
|
||||
import { push2video } from '@/service/api/article.ts';
|
||||
import { showErrorToast, showToast } from '@/components/message.ts';
|
||||
import ArticleGroup, { HotNewsData } from '@/components/article/group.tsx';
|
||||
import type { HookAPI as ModalHookAPI } from 'antd/es/modal/useModal';
|
||||
import { IconWarningCircle } from '@/components/icons';
|
||||
|
||||
import Bg1 from './assets/bg1.jpg'
|
||||
import Bg2 from './assets/bg2.jpg'
|
||||
|
||||
type Props = {
|
||||
id?: number;
|
||||
type: 'news' | 'video';
|
||||
onClose?: (saved?: boolean) => void;
|
||||
onRefresh?: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
@ -20,15 +28,15 @@ const DEFAULT_STATE = {
|
||||
msgTitle: '',
|
||||
msgGroup: '',
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
|
||||
function pushBlocksToGroup(blocks: BlockContent[], groups: BlockContent[][]) {
|
||||
const lastGroup = groups[groups.length - 1]
|
||||
const lastGroup = groups[groups.length - 1];
|
||||
if (lastGroup && lastGroup.filter(s => s.type == 'text').length == 0) {
|
||||
// 如果上一个group中没有文本则直接合并
|
||||
lastGroup.push(...blocks)
|
||||
lastGroup.push(...blocks);
|
||||
} else {
|
||||
groups.push(blocks)
|
||||
groups.push(blocks);
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,132 +47,217 @@ function rebuildGroups(groups: BlockContent[][]) {
|
||||
if (!blocks) return;
|
||||
blocks = blocks.filter(s => !!s).sort((a, b) => {
|
||||
if (a.type == 'text' && b.type == 'text') return 1;
|
||||
return a.type == 'text' ? -1 : 1
|
||||
})
|
||||
return a.type == 'text' ? -1 : 1;
|
||||
});
|
||||
if (blocks.length == 1) {
|
||||
if (index == 0) _groups.push(blocks)
|
||||
else pushBlocksToGroup(blocks, _groups)
|
||||
if (index == 0) _groups.push(blocks);
|
||||
else pushBlocksToGroup(blocks, _groups);
|
||||
} else {
|
||||
if (index == 0) {
|
||||
_groups.push([blocks[0]])
|
||||
_groups.push(blocks.slice(1))
|
||||
_groups.push([blocks[0]]);
|
||||
_groups.push(blocks.slice(1));
|
||||
} else {
|
||||
pushBlocksToGroup(blocks, _groups)
|
||||
pushBlocksToGroup(blocks, _groups);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (_groups.length < 2) {
|
||||
Array(2 - _groups.length).fill([{type: 'text', content: ''}]).forEach((it) => {
|
||||
_groups.push(it)
|
||||
})
|
||||
Array(2 - _groups.length).fill([{ type: 'text', content: '' }]).forEach((it) => {
|
||||
_groups.push(it);
|
||||
});
|
||||
}
|
||||
// console.log('rebuildGroups', _groups)
|
||||
return _groups;
|
||||
|
||||
|
||||
}
|
||||
|
||||
function groupHasImageAndText(blocks: BlockContent[]) {
|
||||
return blocks.some(s=>s.type == 'image' && s.content.trim().length > 0) && blocks.some(s=>s.type == 'text' && s.content.trim().length > 0)
|
||||
return blocks.some(s => s.type == 'image' && s.content.trim().length > 0) && blocks.some(s => s.type == 'text' && s.content.trim().length > 0);
|
||||
}
|
||||
|
||||
// 验证分组数据是否合法
|
||||
function checkGroupsValid(groups: BlockContent[][]) {
|
||||
function checkGroupsValid(_groups: BlockContent[][]) {
|
||||
const groups = _groups.filter((_, index) => {
|
||||
if (index == 0) return true;
|
||||
return _.length > 1 || (_.length == 1 && _[0].content.trim().length > 0);
|
||||
});
|
||||
if (groups.length == 1) return true;
|
||||
for (let index = 1;index< groups.length; index ++) {
|
||||
if(!groupHasImageAndText(groups[index])) return false;
|
||||
for (let index = 1; index < groups.length; index++) {
|
||||
if (!groupHasImageAndText(groups[index])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default function ArticleEditModal(props: Props) {
|
||||
const {t} = useTranslation()
|
||||
const [groups, setGroups] = useState<BlockContent[][]>([]);
|
||||
const [title, setTitle] = useState('')
|
||||
function checkHotNewsValid(hotNews: HotNewsData, modal: ModalHookAPI, t: TFunction<'translation', undefined>) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
|
||||
// 验证热点新闻数据是否正确
|
||||
if (hotNews.mode == 'manual' && hotNews.list.filter(s => s.trim().length > 0).length < 3) {
|
||||
modal.warning({
|
||||
wrapClassName: 'root-modal-confirm',
|
||||
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle /></span>,
|
||||
title: t('modal.hot_news.empty_notice_title'),
|
||||
content: <span dangerouslySetInnerHTML={{ __html: t('modal.hot_news.empty_notice_message') }}></span>,
|
||||
centered: true,
|
||||
onOk: () => {
|
||||
resolve(false);
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
export default function ArticleEditModal(props: Props) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { modal } = App.useApp();
|
||||
const [groups, setGroups] = useState<BlockContent[][]>([]);
|
||||
const [title, setTitle] = useState('');
|
||||
const [tag, setTag] = useState('');
|
||||
const [articleTemplateInfo, setArticleTemplateInfo] = useState<ArticleTemplateInfo>({
|
||||
select:'',
|
||||
options:[]
|
||||
});
|
||||
const [hotNews, setHotNews] = useState<HotNewsData>({
|
||||
list: ['', '', ''],
|
||||
mode: 'auto'
|
||||
});
|
||||
const [state, setState] = useSetState({
|
||||
...DEFAULT_STATE,
|
||||
generating:false
|
||||
})
|
||||
generating: false,
|
||||
pushed: false
|
||||
});
|
||||
|
||||
// 保存数据
|
||||
const handleSave = () => {
|
||||
setState({error: ''})
|
||||
const handleSave = async () => {
|
||||
setState({ error: '' });
|
||||
if (!title) {
|
||||
// setState({msgTitle: '请输入标题内容'});
|
||||
return;
|
||||
}
|
||||
if(i18n.language == 'zh-CN' && tag.length > 4 ){
|
||||
// 获取图文设置不正确的数据
|
||||
setState({ msgGroup: t('news.edit.tag_length_error') });
|
||||
return;
|
||||
}
|
||||
if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
|
||||
setState({msgGroup: t('news.edit_empty_human_content')});
|
||||
setState({ msgGroup: t('news.edit_empty_human_content') });
|
||||
return;
|
||||
}
|
||||
// 验证图文都存在时,文图是否匹配
|
||||
if(!checkGroupsValid(groups)) {
|
||||
if (!checkGroupsValid(groups)) {
|
||||
// 获取图文设置不正确的数据
|
||||
setState({msgGroup: t('news.edit_empty_group_content')});
|
||||
setState({ msgGroup: t('news.edit_empty_group_content') });
|
||||
return;
|
||||
}
|
||||
const hotNewsValid = await checkHotNewsValid(hotNews, modal, t);
|
||||
if (!hotNewsValid) return;
|
||||
// if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
|
||||
// // setState({msgGroup: '请输入正文文本内容'});
|
||||
// return;
|
||||
// }
|
||||
const save = props.type == 'news' ? article.save : regenerate
|
||||
setState({loading: true})
|
||||
save(title, groups[0][0].content, groups.slice(1), props.id && props.id > 0 ? props.id : undefined).then(() => {
|
||||
props.onClose?.(true)
|
||||
const save = props.type == 'news' ? article.save : regenerate;
|
||||
setState({ loading: true });
|
||||
save({
|
||||
title,
|
||||
metahuman_text: groups[0][0].content,
|
||||
content_group: groups.slice(1),
|
||||
hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list,
|
||||
video_tag:tag,
|
||||
background: articleTemplateInfo.select,
|
||||
id: props.id && props.id > 0 ? props.id : undefined
|
||||
}).then(() => {
|
||||
props.onClose?.(true);
|
||||
}).catch(e => {
|
||||
setState({error: e.message || t('news.edit_save_failed')})
|
||||
setState({ error: e.message || t('news.edit_save_failed') });
|
||||
}).finally(() => {
|
||||
setState({loading: false})
|
||||
setState({ loading: false });
|
||||
});
|
||||
}
|
||||
const handlePush2Video = async () =>{
|
||||
};
|
||||
const handlePush2Video = async () => {
|
||||
if (state.pushed) return;
|
||||
if (!title) {
|
||||
// setState({msgTitle: '请输入标题内容'});
|
||||
return;
|
||||
}
|
||||
if(i18n.language == 'zh-CN' && tag.length > 4 ){
|
||||
// 获取图文设置不正确的数据
|
||||
setState({ msgGroup: t('news.edit.tag_length_error') });
|
||||
return;
|
||||
}
|
||||
if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
|
||||
setState({msgGroup: t('news.edit_empty_human_content')});
|
||||
setState({ msgGroup: t('news.edit_empty_human_content') });
|
||||
return;
|
||||
}
|
||||
// 验证图文都存在时,文图是否匹配
|
||||
if(!checkGroupsValid(groups)) {
|
||||
if (!checkGroupsValid(groups)) {
|
||||
// 获取图文设置不正确的数据
|
||||
setState({msgGroup: t('news.edit_empty_group_content')});
|
||||
setState({ msgGroup: t('news.edit_empty_group_content') });
|
||||
return;
|
||||
}
|
||||
if(!props.id || state.generating) return;
|
||||
setState({generating:true})
|
||||
await article.save(title, groups[0][0].content, groups.slice(1), props.id)
|
||||
if (!props.id || state.generating) return;
|
||||
const hotNewsValid = await checkHotNewsValid(hotNews, modal, t);
|
||||
if (!hotNewsValid) return;
|
||||
setState({ generating: true });
|
||||
await article.save({
|
||||
title,
|
||||
metahuman_text: groups[0][0].content,
|
||||
content_group: groups.slice(1),
|
||||
hot_news: hotNews.mode == 'auto' ? [''] : hotNews.list,
|
||||
video_tag:tag,
|
||||
background: articleTemplateInfo.select,
|
||||
id: props.id
|
||||
});
|
||||
push2video([props.id]).then(() => {
|
||||
showToast(t('news.push_stream_success'), 'success')
|
||||
showToast(t('news.push_stream_success'), 'success');
|
||||
setState({ pushed: true });
|
||||
props.onClose?.(true);
|
||||
// props.onRefresh?.();
|
||||
// navigate('/create?state=push-success',{
|
||||
// state: 'push-success'
|
||||
// })
|
||||
// props.onSuccess?.()
|
||||
}).catch(showErrorToast).finally(()=>{
|
||||
setState({generating:false})
|
||||
})
|
||||
}
|
||||
}).catch(showErrorToast).finally(() => {
|
||||
setState({ generating: false });
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
setState({...DEFAULT_STATE})
|
||||
setState({ ...DEFAULT_STATE });
|
||||
if (typeof (props.id) != 'undefined') {
|
||||
// 如果传入了id则获取数据
|
||||
if (props.id > 0) {
|
||||
article.getById(props.id).then(res => {
|
||||
setGroups(rebuildGroups([[{content: res.metahuman_text, type: "text"}], ...res.content_group]))
|
||||
setTitle(res.title)
|
||||
})
|
||||
if (res.hot_news) {
|
||||
const len = res.hot_news.length;
|
||||
const list = len >= 3 ? res.hot_news : res.hot_news.concat(Array(3 - len).fill(''));
|
||||
const mode = res.hot_news && res.hot_news.filter(s => s.length > 0).length == 3 ? 'manual' : 'auto';
|
||||
setHotNews({
|
||||
list,
|
||||
mode
|
||||
});
|
||||
}
|
||||
setGroups(rebuildGroups([[{ content: res.metahuman_text, type: 'text' }], ...res.content_group]));
|
||||
setTitle(res.title);
|
||||
setTag(res.video_tag)
|
||||
setArticleTemplateInfo(res.template_info)
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
setGroups([])
|
||||
setTitle('')
|
||||
setGroups([]);
|
||||
setTitle('');
|
||||
}
|
||||
}
|
||||
}, [props.id])
|
||||
}, [props.id]);
|
||||
|
||||
return (<Modal
|
||||
title={null}
|
||||
centered={true}
|
||||
rootClassName={"article-edit-modal"}
|
||||
rootClassName={'article-edit-modal'}
|
||||
open={props.id != undefined && props.id >= 0}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
@ -172,35 +265,80 @@ export default function ArticleEditModal(props: Props) {
|
||||
footer={null}
|
||||
closeIcon={null}
|
||||
onCancel={() => props.onClose?.()}
|
||||
okButtonProps={{loading: state.loading}}
|
||||
okButtonProps={{ loading: state.loading }}
|
||||
onOk={handleSave}
|
||||
okText={props.type == 'news' ? t('confirm_text') : t('news.edit_generate_video_again')}
|
||||
>
|
||||
<div className="article-title mt-5">
|
||||
<input className={'input-box text-lg'} value={title} onChange={e => {
|
||||
setTitle(e.target.value)
|
||||
setState({msgTitle: e.target.value ? '' : t('news.edit_notice_enter_article_title1')})
|
||||
}} placeholder={t('news.edit_notice_enter_article_title')}/>
|
||||
<div className="text-red-500">{state.msgTitle}</div>
|
||||
<div className="mt-5 px-6 pt-10">
|
||||
<div className="flex items-center pb-3 article-title">
|
||||
<span className="mr-2 text-lg">{t('news.title')}</span>
|
||||
<input className={'input-box text-lg flex-1 py-2'} value={title} onChange={e => {
|
||||
setTitle(e.target.value);
|
||||
setState({ msgTitle: e.target.value ? '' : t('news.edit_notice_enter_article_title1') });
|
||||
}} placeholder={t('news.edit_notice_enter_article_title')} />
|
||||
</div>
|
||||
<div className="text-red-500 mt-2">{state.msgTitle}</div>
|
||||
</div>
|
||||
<div className="article-body">
|
||||
<div className="box">
|
||||
<div className="box text-base">
|
||||
<ArticleGroup
|
||||
errorMessage={state.msgGroup} editable groups={groups}
|
||||
onChange={list => {
|
||||
setGroups(() => list)
|
||||
setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? t('news.edit_empty_human_content') : ''});
|
||||
errorMessage={state.msgGroup}
|
||||
editable
|
||||
groups={groups}
|
||||
hotNews={hotNews}
|
||||
onChange={(list, hotNews) => {
|
||||
setHotNews(hotNews);
|
||||
setGroups(() => list);
|
||||
setState({ msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? t('news.edit_empty_human_content') : '' });
|
||||
}}
|
||||
/>
|
||||
<div className="text-red-500">{state.msgGroup}</div>
|
||||
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>
|
||||
{state.error && <div className="text-red-500">{state.error}</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
|
||||
value={articleTemplateInfo.select}
|
||||
onChange={e=>{
|
||||
setArticleTemplateInfo(prev=>(
|
||||
{
|
||||
...prev,
|
||||
select: e.target.value
|
||||
}
|
||||
))
|
||||
}}>
|
||||
{articleTemplateInfo.options.map((opt,idx)=>(
|
||||
<Popover
|
||||
rootClassName="background-template-popover"
|
||||
key={idx} placement="bottomLeft" arrow={false}
|
||||
content={<img className="w-[150px] rounded" src={opt.background} />}
|
||||
>
|
||||
<Radio value={opt.template_id}>{t('news.edit.bg')}{idx + 1}</Radio></Popover>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
<div className="text-red-500 mt-2">{state.msgGroup}</div>
|
||||
</div>
|
||||
{state.error && <div className="text-red-500 mt-2">{state.error}</div>}
|
||||
</div>
|
||||
<div className="modal-control-footer flex justify-end">
|
||||
<div className="flex gap-10 ">
|
||||
{props.type == 'news' && props.id ? <button className="text-gray-400 hover:text-gray-800" onClick={handlePush2Video}>{t('news.edit_generate_video')}{state.generating?'推送中...':''}</button> : null}
|
||||
{props.type == 'news' && props.id ? <button
|
||||
className="text-gray-400 hover:text-gray-800"
|
||||
onClick={handlePush2Video}
|
||||
>
|
||||
{t('news.edit_generate_video')}{state.pushed ? `${i18n.language == 'zh-CN' ? '中' : ''}...` : (state.generating ? `${i18n.language == 'zh-CN' ? '推送中' : 'Pushing'}...` : '')}
|
||||
</button> : null}
|
||||
<button className="text-gray-400 hover:text-gray-800" onClick={() => props.onClose?.()}>{t('cancel')}</button>
|
||||
<button onClick={handleSave} className="text-gray-800 hover:text-blue-500">{props.type == 'news' ? t('confirm_text') : t('news.edit_generate_again')}</button>
|
||||
<button onClick={handleSave}
|
||||
className="text-gray-800 hover:text-blue-500">{props.type == 'news' ? t('news.save_text') : t('news.edit_generate_again')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>);
|
||||
|
@ -6,23 +6,30 @@ import {showToast} from "@/components/message.ts";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {IconAdd} from "@/components/icons";
|
||||
import HotNews from "@/components/article/HotNews.tsx";
|
||||
|
||||
export type HotNewsData = {
|
||||
list: string[];
|
||||
mode: string
|
||||
}
|
||||
type Props = {
|
||||
groups: BlockContent[][];
|
||||
editable?: boolean;
|
||||
onChange?: (groups: BlockContent[][]) => void;
|
||||
onChange?: (groups: BlockContent[][], hotNews: HotNewsData) => void;
|
||||
errorMessage?: string;
|
||||
hotNews: HotNewsData;
|
||||
leftPanelHeader?: React.ReactNode;
|
||||
}
|
||||
|
||||
|
||||
export default function ArticleGroup({groups, editable, onChange, errorMessage}: Props) {
|
||||
const {t,i18n} = useTranslation()
|
||||
export default function ArticleGroup({groups, editable, onChange, errorMessage, hotNews, leftPanelHeader}: Props) {
|
||||
const {t, i18n} = useTranslation()
|
||||
// const groups = rebuildGroups(_groups)
|
||||
/**
|
||||
* 添加一个组
|
||||
* @param insertIndex 插入的位置,-1表示插入到末尾
|
||||
*/
|
||||
const handleAddGroup = (insertIndex: number,checkId:number) => {
|
||||
const handleAddGroup = (insertIndex: number, checkId: number) => {
|
||||
// && insertIndex !== 1
|
||||
if (checkId > 0 && checkId < groups.length) {
|
||||
//const index = insertIndex == -1 || insertIndex >= groups.length ? groups.length - 1 : insertIndex - 1
|
||||
@ -41,28 +48,31 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
|
||||
} else {
|
||||
_groups.splice(insertIndex, 0, newGroup)
|
||||
}
|
||||
onChange?.(_groups)
|
||||
onChange?.(_groups, hotNews)
|
||||
}
|
||||
|
||||
const handleDigitalPersonContentChange = (content:string) => {
|
||||
const handleDigitalPersonContentChange = (content: string) => {
|
||||
groups[0] = [{type: 'text', content}]
|
||||
onChange?.([...groups])
|
||||
onChange?.([...groups], hotNews)
|
||||
}
|
||||
|
||||
return <div className={styles.group}>
|
||||
<div className={'panel digital-person'}>
|
||||
<div className={'panel digital-person h-[544px]'}>
|
||||
{leftPanelHeader}
|
||||
<div className="area-title">
|
||||
<span className="">{t('news.edit_digital_text')}</span>
|
||||
{i18n.language == 'zh-CN' && <span className="text-gray-400">(出现数字人形象)</span>}
|
||||
</div>
|
||||
<div className="panel-body p-3">
|
||||
<div className="panel-body p-3 flex-1 main-human-text">
|
||||
{/* value={groups || groups[0][0].content}*/}
|
||||
<div className="h-[486px] pt-2 rounded-xl overflow-hidden bg-gray-50">
|
||||
{editable ? <div className="relative">
|
||||
<div className={`h-full rounded-xl overflow-hidden bg-gray-50`}>
|
||||
<div className="human-tts h-full">
|
||||
{editable ? <div className="relative h-full">
|
||||
<Input.TextArea
|
||||
placeholder={t('news.edit_notice_enter_text')}
|
||||
className="main-human-text-input"
|
||||
value={groups && groups.length > 0 ? groups[0][0].content : ''}
|
||||
autoSize={{minRows: 20, maxRows: 21}}
|
||||
autoSize={{maxRows: hotNews.mode == 'auto'?15:8}}
|
||||
variant={"borderless"}
|
||||
onChange={e => {
|
||||
handleDigitalPersonContentChange(e.target.value)
|
||||
@ -72,6 +82,16 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hot-news-container">
|
||||
<HotNews
|
||||
news={hotNews.list} mode={hotNews.mode}
|
||||
onValueChange={(hotNews) => {
|
||||
onChange?.([...groups], {
|
||||
list:hotNews.news,mode: hotNews.mode
|
||||
})
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"panel groups-list flex-1"}>
|
||||
<div className={"area-title"}>
|
||||
<span className="">{t('news.edit_other_text')}</span>
|
||||
@ -81,9 +101,12 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
|
||||
<div className="panel-body py-3">
|
||||
<div className="max-h-[485px] overflow-auto py-4">
|
||||
|
||||
{editable && groups.length == 1 && <div className={`${styles.blockContainer} group`}><div className={'divider-container before'}><Divider>
|
||||
<span onClick={()=>handleAddGroup?.(1,1)} className="article-action-add" title={t('news.materials.add_group')}><IconAdd style={{fontSize: 24}}/></span>
|
||||
</Divider></div></div> }
|
||||
{editable && groups.length == 1 && <div className={`${styles.blockContainer} group`}>
|
||||
<div className={'divider-container before'}><Divider>
|
||||
<span onClick={() => handleAddGroup?.(1, 1)} className="article-action-add"
|
||||
title={t('news.materials.add_group')}><IconAdd style={{fontSize: 24}}/></span>
|
||||
</Divider></div>
|
||||
</div>}
|
||||
|
||||
{groups.map((g, index) => (
|
||||
index == 0 ? null : <ArticleBlock
|
||||
@ -92,20 +115,20 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
|
||||
blocks={g}
|
||||
onChange={(blocks) => {
|
||||
groups[index] = blocks
|
||||
onChange?.([...groups])
|
||||
onChange?.([...groups], hotNews)
|
||||
}}
|
||||
errorMessage={errorMessage}
|
||||
index={index}
|
||||
onAdd={(_index,checkIndex) => {
|
||||
handleAddGroup?.(_index ? _index :index + 1,checkIndex)
|
||||
onAdd={(_index, checkIndex) => {
|
||||
handleAddGroup?.(_index ? _index : index + 1, checkIndex)
|
||||
}}
|
||||
disableRemoveMessage={groups.length <= 1?t('news.edit_notice_keep_1'):''}
|
||||
disableRemoveMessage={groups.length <= 1 ? t('news.edit_notice_keep_1') : ''}
|
||||
onRemove={async () => {
|
||||
if (groups.length <= 1) {
|
||||
message.warning(t('news.edit_notice_keep_1'))
|
||||
return;
|
||||
}
|
||||
onChange?.(groups.filter((_, idx) => index !== idx))
|
||||
onChange?.(groups.filter((_, idx) => index !== idx), hotNews)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@ -113,7 +136,7 @@ export default function ArticleGroup({groups, editable, onChange, errorMessage}:
|
||||
</div>
|
||||
</div>
|
||||
{groups.length == 0 && editable &&
|
||||
<ArticleBlock editable onChange={blocks => onChange?.([blocks])} index={0}
|
||||
<ArticleBlock editable onChange={blocks => onChange?.([blocks],hotNews)} index={0}
|
||||
blocks={[{type: 'text', content: ''}]}/>}
|
||||
</div>
|
||||
}
|
@ -2,12 +2,14 @@ import React, {useState} from "react";
|
||||
import {Input, Popconfirm, Spin, Upload, UploadProps} from "antd";
|
||||
import {CloseOutlined} from "@ant-design/icons";
|
||||
import {clsx} from "clsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
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";
|
||||
import {IconAddImage} from "@/components/icons";
|
||||
import {ModalWarningIcon, ModalWarningTitle} from "@/components/icons/ModalWarning.tsx";
|
||||
import { BizError } from '@/service/types.ts';
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
@ -37,6 +39,10 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
|
||||
});
|
||||
const beforeUpload = async (file: any) => {
|
||||
try {
|
||||
// 判断文件类型
|
||||
if (!MimeTypes.includes(file.type)) {
|
||||
throw new Error('upload_file_type_error')
|
||||
}
|
||||
// 因为有超时问题,所以每次上传都重新获取参数
|
||||
Data.uploadConfig = await getOssPolicy();
|
||||
const suffix = file.name.slice(file.name.lastIndexOf('.'));
|
||||
@ -52,17 +58,22 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
|
||||
const onUploadChange = async (info) => {
|
||||
if (info.fileList.length == 0) return;
|
||||
const file = info.fileList[0];
|
||||
console.log('onChange', file);
|
||||
console.log('onUploadChange', file);
|
||||
if (file.status == 'done') {
|
||||
setLoading(-1)
|
||||
onChange?.({type: 'image', content: Data.uploadConfig?.host + '/' + file.url})
|
||||
setLoading(-1);
|
||||
onChange?.({ type: 'image', content: Data.uploadConfig?.host + '/' + file.url });
|
||||
} else if (file.status == 'error') {
|
||||
setLoading(-1)
|
||||
showToast(t('upload.upload_failed'), 'warning')
|
||||
|
||||
if (!MimeTypes.includes(file.type)) {
|
||||
showToast(t('upload.upload_file_type_error'), 'warning');
|
||||
return;
|
||||
}
|
||||
setLoading(-1);
|
||||
showToast(t('upload.upload_failed'), 'warning');
|
||||
} else if (file.status == 'uploading') {
|
||||
setLoading(file.percent)
|
||||
}
|
||||
setLoading(file.percent);
|
||||
}
|
||||
};
|
||||
//
|
||||
return <div className={styles.image}>
|
||||
{editable && onlyUpload ? <div className={'relative'}>
|
||||
@ -104,8 +115,9 @@ export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: Ima
|
||||
rootClassName={'popconfirm-main'}
|
||||
placement={'right'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={<div style={{minWidth: 150}}><span>{t('upload.delete_confirm')}</span></div>}
|
||||
icon={<ModalWarningIcon/>}
|
||||
title={<ModalWarningTitle/>}
|
||||
description={<div style={{minWidth: 150}}><span>{t('upload.delete_confirm')}</span></div>}
|
||||
onConfirm={onRemove}
|
||||
okText={t('delete')}
|
||||
cancelText={t('cancel')}
|
||||
|
@ -7,6 +7,7 @@ import {BizError} from "@/service/types.ts";
|
||||
import {IconWarningCircle} from "@/components/icons";
|
||||
import {LoadingOutlined} from "@ant-design/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import ModalWarning from "@/components/icons/ModalWarning.tsx";
|
||||
|
||||
type Props = {
|
||||
selected: any[],
|
||||
@ -57,10 +58,12 @@ export default function ButtonBatch(
|
||||
if(confirmMessage){
|
||||
modal.confirm({
|
||||
wrapClassName: 'root-modal-confirm',
|
||||
title: <span dangerouslySetInnerHTML={{__html:title || t('confirm.title')}}></span>,
|
||||
title: <ModalWarning.Title />,
|
||||
centered: true,
|
||||
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
|
||||
content: confirmMessage,
|
||||
icon: <ModalWarning.Icon />,
|
||||
content: <div>
|
||||
<div>{confirmMessage}</div>
|
||||
</div>,
|
||||
onOk: onBatchProcess
|
||||
})
|
||||
}else{
|
||||
|
17
src/components/icons/ModalWarning.tsx
Normal file
17
src/components/icons/ModalWarning.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import {IconWarningCircle} from "@/components/icons/index.tsx";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export function ModalWarningIcon({size=24}:{size?:number}) {
|
||||
return <IconWarningCircle
|
||||
style={{fontSize: size, color: 'rgba(250, 173, 20, 1)'}}/>
|
||||
}
|
||||
export function ModalWarningTitle(){
|
||||
const {t} = useTranslation()
|
||||
return <span className="text-base">{t('modal.warning')}</span>
|
||||
}
|
||||
const ModalWarning = {
|
||||
Icon: ModalWarningIcon,
|
||||
Title: ModalWarningTitle
|
||||
}
|
||||
export default ModalWarning
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import RecycleIndex from "@/pages/recycle";
|
||||
|
||||
type IconProps = { style?: React.CSSProperties; className?: string; }
|
||||
|
||||
@ -59,6 +60,43 @@ export const IconDownload = ({style, className}: IconProps) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const IconOrderFill = ({style, className}: IconProps) => (
|
||||
<svg
|
||||
className={`svg-icon ${className || ''} icon-download`} style={style}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" width="1em" height="1em" viewBox="0 0 22 22"
|
||||
>
|
||||
<path
|
||||
d="M1.15485 4.02687C0.839494 4.03116 0.539152 4.16202 0.322478 4.39157C0.105803 4.61897 -0.0100427 4.92575 0.000683739 5.2411C-0.00146155 5.88898 0.506973 6.42531 1.15485 6.45534H3.46533C3.78069 6.45105 4.08103 6.32019 4.29771 6.09064C4.51438 5.86324 4.63023 5.55646 4.6195 5.2411C4.62165 4.59323 4.11321 4.0569 3.46533 4.02687H1.15485ZM1.15485 9.98006C0.839494 9.98435 0.539152 10.1152 0.322478 10.3448C0.105803 10.5722 -0.0100427 10.8789 0.000683739 11.1943C-0.00146155 11.8422 0.506973 12.3785 1.15485 12.4085H3.46533C3.78069 12.4042 4.08103 12.2734 4.29771 12.0438C4.51438 11.8164 4.63023 11.5097 4.6195 11.1943C4.62165 10.5464 4.11321 10.0101 3.46533 9.98006H1.15485ZM1.15485 15.9912C0.532717 16.0555 0.0628972 16.5811 0.0628973 17.2054C0.0628973 17.8297 0.534862 18.3531 1.15485 18.4196H3.46533C4.08747 18.3531 4.55729 17.8297 4.55729 17.2054C4.55729 16.5811 4.08532 16.0577 3.46533 15.9912H1.15485ZM20.8186 0.0216038H3.40741C3.09205 0.0258944 2.79171 0.156757 2.57504 0.386304C2.35836 0.613705 2.24252 0.920482 2.25324 1.23584V2.81263H3.40741C4.69244 2.84481 5.71789 3.896 5.71789 5.18104C5.72862 5.80317 5.49049 6.40171 5.05714 6.84578C4.62379 7.28986 4.02955 7.543 3.40956 7.54944H2.25539V8.76368H3.40956C4.69459 8.79585 5.72004 9.84705 5.72004 11.1321C5.73076 11.7542 5.49264 12.3528 5.05929 12.7968C4.62594 13.2409 4.03169 13.494 3.4117 13.5005H2.25753V14.7147H3.4117C4.69244 14.749 5.71789 15.8002 5.71789 17.0853C5.72862 17.7074 5.49049 18.3059 5.05714 18.75C4.62379 19.1941 4.02955 19.4472 3.40956 19.4537H2.25539V20.7752C2.25324 21.4231 2.76168 21.9594 3.40956 21.9894H20.8208C21.1361 21.9851 21.4365 21.8543 21.6531 21.6247C21.8698 21.3973 21.9857 21.0905 21.9749 20.7752V1.06636C21.9234 0.467825 21.4172 0.0130226 20.8186 0.0216038ZM17.7015 8.87738C18.1799 8.87738 18.5682 9.26567 18.5682 9.74407C18.5682 10.2225 18.1799 10.6108 17.7015 10.6108H15.1014V11.6512H17.6994C18.1778 11.6512 18.5661 12.0395 18.5661 12.5179C18.5661 12.9963 18.1778 13.3846 17.6994 13.3846H15.1014V16.2743C15.1143 16.5919 14.9512 16.89 14.6788 17.0509C14.4063 17.214 14.0652 17.214 13.7928 17.0509C13.5203 16.8879 13.3573 16.5897 13.3702 16.2743V13.3954H10.7701C10.2917 13.3954 9.90336 13.0071 9.90336 12.5287C9.90336 12.0503 10.2917 11.662 10.7701 11.662H13.368V10.6215H10.7701C10.2917 10.6215 9.90336 10.2332 9.90336 9.7548C9.90336 9.2764 10.2917 8.8881 10.7701 8.8881H12.0401L10.1372 6.98094C9.90765 6.76641 9.81111 6.44461 9.88834 6.13998C9.96557 5.83535 10.2037 5.59722 10.5083 5.51999C10.813 5.44276 11.1348 5.53715 11.3493 5.7667L14.0052 8.42472L14.0631 8.48264H14.4106L14.4685 8.42472L17.1244 5.7667C17.4634 5.45349 17.989 5.46207 18.3151 5.7903C18.6411 6.11638 18.6497 6.64198 18.3365 6.98094L16.4336 8.87738H17.7015Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const IconRecycleFill = ({style, className}: IconProps) => (
|
||||
<svg
|
||||
className={`svg-icon ${className || ''} icon-download`} style={style}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" width="1em" height="1em" viewBox="0 0 22 22"
|
||||
>
|
||||
<path
|
||||
d="M21.1832 3.92852H17.262V2.35739C17.2638 1.73447 17.019 1.1363 16.5814 0.694283C16.1437 0.252265 15.549 0.00255176 14.9279 0H7.08268C6.45926 0.000364354 5.86146 0.248804 5.42051 0.690782C4.97956 1.13276 4.73149 1.73216 4.73076 2.35739V3.92852H0.816386C0.675283 3.92266 0.535222 3.95514 0.411003 4.02252C0.286784 4.0899 0.183019 4.18968 0.110673 4.31132C0.0382409 4.43311 0 4.57228 0 4.71409C0 4.8559 0.0382409 4.99507 0.110673 5.11686C0.183019 5.23849 0.286784 5.33828 0.411003 5.40566C0.535222 5.47304 0.675283 5.50551 0.816386 5.49966H21.1832C21.2888 5.50409 21.3943 5.48706 21.4932 5.44958C21.5921 5.4121 21.6824 5.35495 21.7587 5.28157C21.835 5.20818 21.8957 5.12008 21.9371 5.02256C21.9786 4.92504 22 4.82011 22 4.71409C22 4.60807 21.9786 4.50314 21.9371 4.40562C21.8957 4.3081 21.835 4.21999 21.7587 4.14661C21.6824 4.07323 21.5921 4.01608 21.4932 3.9786C21.3943 3.94112 21.2888 3.92409 21.1832 3.92852ZM18.0542 6.87801H3.95091C3.74286 6.87801 3.54331 6.9608 3.39607 7.10822C3.24883 7.25563 3.16593 7.45561 3.16556 7.66427V19.644C3.16665 20.2687 3.41469 20.8676 3.85531 21.3092C4.29592 21.7509 4.89317 21.9993 5.51611 22H16.4849C17.1078 21.9993 17.7051 21.7509 18.1457 21.3092C18.5863 20.8676 18.8343 20.2687 18.8354 19.644V7.69456C18.8366 7.4827 18.7554 7.27873 18.609 7.12599C18.4626 6.97326 18.2626 6.8838 18.0514 6.87664L18.0542 6.87801ZM5.66576 16.0845C5.60496 15.9767 5.56272 15.8594 5.54082 15.7375C5.53845 15.6251 5.56797 15.5143 5.62595 15.418C5.62595 15.418 6.26439 14.3082 6.27262 14.2972C6.28086 14.2862 5.66988 13.9309 5.66988 13.9309L7.75682 13.4614L8.65338 15.6976L8.06299 15.3561L7.20351 16.7468C7.04274 17.0229 6.94126 17.3297 6.90557 17.6474C6.87595 17.9301 6.9234 18.2157 7.04287 18.4736L5.66576 16.0845ZM7.70465 18.9073C7.60718 18.8801 7.51453 18.8378 7.43005 18.782C7.30763 18.6826 7.21302 18.5531 7.15545 18.4061C7.04694 18.1357 7.00915 17.8419 7.04568 17.5527C7.0822 17.2635 7.19183 16.9885 7.36415 16.7537H10.1307V18.9114H7.70465V18.9073ZM9.61309 12.8307L7.7527 11.7525L8.96642 9.63886C9.03647 9.56856 9.11766 9.5104 9.20669 9.46673C9.35308 9.41038 9.51138 9.39237 9.66664 9.41441C9.98643 9.46441 10.287 9.59945 10.5371 9.80547C10.7304 9.97913 10.8864 10.1904 10.9957 10.4265L9.61309 12.8307ZM11.0836 10.2805C10.9264 10.0033 10.7126 9.76262 10.4561 9.57414C10.2286 9.40715 9.96083 9.30401 9.68037 9.27533H12.4263C12.5495 9.2769 12.6716 9.29924 12.7874 9.34143C12.8859 9.39611 12.9669 9.47778 13.0208 9.57689C13.0208 9.57689 13.6593 10.6867 13.6634 10.7005C13.6634 10.6922 14.2799 10.359 14.2799 10.359L13.6346 12.4093L11.2524 12.0651L11.8469 11.7236L11.0836 10.2805ZM15.0295 18.5286C14.9669 18.6352 14.8871 18.7306 14.7934 18.8109C14.6971 18.8696 14.5861 18.8992 14.4735 18.8963H13.1829C13.1677 18.8963 13.1691 19.6027 13.1691 19.6027L11.7151 18.0205L13.2007 16.1203V16.8088L14.8304 16.8597C15.1488 16.8615 15.4641 16.7963 15.7558 16.6683C16.0132 16.554 16.2351 16.3723 16.3984 16.1423L15.0254 18.5273L15.0295 18.5286ZM16.2844 16.0666C16.0817 16.3194 15.8146 16.5126 15.5114 16.6256C15.2657 16.707 15.0058 16.737 14.7481 16.7138L13.3627 14.3137L15.2217 13.2341L16.4354 15.3464C16.4721 15.47 16.477 15.6008 16.4497 15.7267C16.4223 15.8527 16.3636 15.9696 16.2789 16.0666H16.2844Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const IconDownloadOutline = ({style, className}: IconProps)=>(
|
||||
<svg
|
||||
className={`svg-icon ${className || ''} icon-download`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" width="1em" height="1em" viewBox="0 0 22 22"
|
||||
>
|
||||
<path
|
||||
d="M9.94889 12.052V1.05935C9.94889 0.778394 10.0596 0.508944 10.2568 0.310277C10.4539 0.11161 10.7212 0 11 0C11.2788 0 11.5461 0.11161 11.7432 0.310277C11.9404 0.508944 12.0511 0.778394 12.0511 1.05935V12.052L13.9236 10.1648C14.0204 10.0634 14.1364 9.98239 14.2648 9.92663C14.3931 9.87087 14.5311 9.84146 14.6708 9.84011C14.8106 9.83876 14.9492 9.8655 15.0785 9.91876C15.2079 9.97203 15.3254 10.0508 15.4242 10.1503C15.523 10.2499 15.6011 10.3683 15.6539 10.4987C15.7068 10.6291 15.7333 10.7687 15.732 10.9096C15.7306 11.0504 15.7015 11.1895 15.6461 11.3188C15.5908 11.4482 15.5105 11.5651 15.4098 11.6627L11.7431 15.3581C11.546 15.5567 11.2787 15.6683 11 15.6683C10.7213 15.6683 10.454 15.5567 10.2569 15.3581L6.59022 11.6627C6.48954 11.5651 6.40919 11.4482 6.35387 11.3188C6.29854 11.1895 6.26936 11.0504 6.26802 10.9096C6.26668 10.7687 6.29321 10.6291 6.34606 10.4987C6.39892 10.3683 6.47703 10.2499 6.57583 10.1503C6.67464 10.0508 6.79215 9.97203 6.9215 9.91876C7.05085 9.8655 7.18944 9.83876 7.32916 9.84011C7.46888 9.84146 7.60694 9.87087 7.73525 9.92663C7.86356 9.98239 7.97955 10.0634 8.07644 10.1648L9.94889 12.052ZM17.5181 3.52296C17.194 3.52291 16.8832 3.39311 16.654 3.1621C16.4248 2.9311 16.2961 2.61782 16.2961 2.29115C16.2961 1.96449 16.4248 1.65121 16.654 1.4202C16.8832 1.1892 17.194 1.0594 17.5181 1.05935H19.5556C20.2039 1.05935 20.8256 1.31891 21.284 1.78092C21.7425 2.24294 22 2.86957 22 3.52296V19.5364C22 20.1898 21.7425 20.8164 21.284 21.2784C20.8256 21.7404 20.2039 22 19.5556 22H2.44444C1.79614 22 1.17438 21.7404 0.715961 21.2784C0.257539 20.8164 0 20.1898 0 19.5364V3.52296C0 2.86957 0.257539 2.24294 0.715961 1.78092C1.17438 1.31891 1.79614 1.05935 2.44444 1.05935H4.48189C4.64241 1.05933 4.80136 1.09117 4.94967 1.15306C5.09798 1.21495 5.23274 1.30568 5.34625 1.42007C5.45977 1.53446 5.54981 1.67026 5.61124 1.81972C5.67268 1.96918 5.7043 2.12937 5.7043 2.29115C5.7043 2.45293 5.67268 2.61313 5.61124 2.76259C5.54981 2.91205 5.45977 3.04785 5.34625 3.16224C5.23274 3.27662 5.09798 3.36735 4.94967 3.42925C4.80136 3.49114 4.64241 3.52298 4.48189 3.52296H2.44444V19.5364H19.5556V3.52296H17.5181Z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const IconPin = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-download`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" version="1.1" width="0.6em" height="1em" viewBox="0 0 12 21">
|
||||
@ -170,26 +208,26 @@ export const IconUnlock = ({style, className}: IconProps) => (
|
||||
)
|
||||
|
||||
export const IconPlaying = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
<svg className={`svg-icon ${className || ''} icon-playing`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 32 30" version="1.1">
|
||||
<path d="M1 11.7057V18.2943M7 6.76424V23.2358M13 1V29M19 7.22275V22.7772M25 11.1114V18.8886M31 13.3528V16.6472"
|
||||
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)
|
||||
export const IconGenerating = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
<svg className={`svg-icon ${className || ''} icon-generating`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 20 20" version="1.1">
|
||||
<path d="M3.463 2.43301C5.27751 0.860592 7.59897 -0.00342947 10 1.02307e-05C15.523 1.02307e-05 20 4.47701 20 10C20 12.136 19.33 14.116 18.19 15.74L15 10H18C18.0001 8.43163 17.5392 6.89781 16.6747 5.58927C15.8101 4.28072 14.5799 3.25517 13.1372 2.64013C11.6944 2.0251 10.1027 1.84771 8.55996 2.13003C7.0172 2.41234 5.59145 3.14191 4.46 4.22801L3.463 2.43301ZM16.537 17.567C14.7225 19.1394 12.401 20.0034 10 20C4.477 20 0 15.523 0 10C0 7.86401 0.67 5.88401 1.81 4.26001L5 10H2C1.99987 11.5684 2.46075 13.1022 3.32534 14.4108C4.18992 15.7193 5.42007 16.7449 6.86282 17.3599C8.30557 17.9749 9.89729 18.1523 11.44 17.87C12.9828 17.5877 14.4085 16.8581 15.54 15.772L16.537 17.567Z" fill="white"/>
|
||||
</svg>
|
||||
)
|
||||
export const IconGenerateFailed = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
<svg className={`svg-icon ${className || ''} icon-generate-fail`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 20 20" version="1.1">
|
||||
<path d="M18 0H2C0.9 0 0.00999999 0.9 0.00999999 2L0 20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0ZM11 12H9V10H11V12ZM11 8H9V4H11V8Z" fill="#FFA800"/>
|
||||
</svg>
|
||||
)
|
||||
export const IconRegenerate = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
<svg className={`svg-icon ${className || ''} icon-regenerate`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 24 24" version="1.1">
|
||||
<path d="M20.4728 3.525C19.3618 2.4074 18.0406 1.52056 16.5851 0.915578C15.1297 0.310592 13.5688 -0.000577199 11.9925 8.03759e-07C5.35835 8.03759e-07 0 5.37 0 12C0 18.63 5.35835 24 11.9925 24C17.591 24 22.2589 20.175 23.5947 15H20.4728C19.8545 16.7543 18.7067 18.2736 17.1878 19.3483C15.6688 20.4229 13.8536 21.0001 11.9925 21C7.02439 21 2.98687 16.965 2.98687 12C2.98687 7.035 7.02439 3 11.9925 3C14.4841 3 16.7054 4.035 18.3265 5.67L13.4934 10.5H24V8.03759e-07L20.4728 3.525Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
32
src/components/icons/language-switcher.tsx
Normal file
32
src/components/icons/language-switcher.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import useGlobalConfig from "@/hooks/useGlobalConfig.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React from "react";
|
||||
import {useSearchParams} from "react-router-dom";
|
||||
|
||||
function LanguageSwitcher() {
|
||||
const {i18n} = useTranslation();
|
||||
const [params] = useSearchParams();
|
||||
const {globalConfig} = useGlobalConfig()
|
||||
const handleChangeLang = async () => {
|
||||
const key = i18n.language == 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
await i18n.changeLanguage(key)
|
||||
globalConfig.i18n = key
|
||||
localStorage.setItem('ai-human-lang',key)
|
||||
}
|
||||
return (
|
||||
(params.get('lang') == 'yes' || AppConfig.APP_LANG == 'multiple') ?
|
||||
<div className="icon-language" onClick={handleChangeLang}>
|
||||
<span className="hover:bg-gray-200">
|
||||
<svg
|
||||
className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em">
|
||||
<path
|
||||
d="M757.205333 473.173333c5.333333 0 10.453333 2.090667 14.250667 5.717334a19.029333 19.029333 0 0 1 5.888 13.738666v58.154667h141.184c11.093333 0 20.138667 8.704 20.138667 19.413333v232.704a19.797333 19.797333 0 0 1-20.138667 19.413334h-141.184v96.981333a19.754667 19.754667 0 0 1-20.138667 19.370667H716.8a20.565333 20.565333 0 0 1-14.250667-5.674667 19.029333 19.029333 0 0 1-5.888-13.696v-96.981333h-141.141333a20.565333 20.565333 0 0 1-14.250667-5.674667 19.029333 19.029333 0 0 1-5.930666-13.738667v-232.704c0-5.12 2.133333-10.112 5.930666-13.738666a20.565333 20.565333 0 0 1 14.250667-5.674667h141.141333v-58.154667c0-5.162667 2.133333-10.112 5.888-13.738666a20.565333 20.565333 0 0 1 14.250667-5.674667h40.362667zM192.597333 628.394667c22.272 0 40.32 17.365333 40.32 38.826666v38.741334c0 40.618667 32.512 74.368 74.624 77.397333l6.058667 0.213333h80.64c21.930667 0.469333 39.424 17.706667 39.424 38.784 0 21.077333-17.493333 38.314667-39.424 38.784H313.6c-89.088 0-161.28-69.461333-161.28-155.178666v-38.741334c0-21.461333 18.005333-38.826667 40.277333-38.826666z m504.106667 0h-80.64v116.394666h80.64v-116.394666z m161.28 0h-80.64v116.394666h80.64v-116.394666zM320.170667 85.333333c8.234667 0 15.658667 4.778667 18.773333 12.202667H338.773333l161.322667 387.84c2.517333 5.973333 1.706667 12.8-2.005333 18.090667a20.394667 20.394667 0 0 1-16.725334 8.533333h-43.52a20.181333 20.181333 0 0 1-18.688-12.202667L375.850667 395.648H210.901333l-43.264 104.149333A20.181333 20.181333 0 0 1 148.906667 512H105.514667a20.394667 20.394667 0 0 1-16.725334-8.533333 18.773333 18.773333 0 0 1-2.005333-18.090667l161.28-387.84A20.181333 20.181333 0 0 1 266.88 85.333333h53.290667zM716.8 162.901333c42.794667 0 83.84 16.341333 114.090667 45.44a152.234667 152.234667 0 0 1 47.232 109.738667v38.741333c-0.469333 21.077333-18.389333 37.930667-40.32 37.930667s-39.808-16.853333-40.32-37.930667v-38.741333c0-20.608-8.490667-40.32-23.637334-54.869333a82.304 82.304 0 0 0-57.045333-22.741334h-80.64c-21.888-0.469333-39.424-17.706667-39.424-38.784 0-21.077333 17.493333-38.314667 39.424-38.784h80.64z m-423.424 34.304L243.2 318.037333h100.48L293.418667 197.205333z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div> : null
|
||||
)
|
||||
}
|
||||
|
||||
export default LanguageSwitcher
|
21
src/components/message/confirm.tsx
Normal file
21
src/components/message/confirm.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import {Popconfirm} from "antd";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import ModalWarning from "@/components/icons/ModalWarning.tsx";
|
||||
|
||||
export function DeleteItemPopoverConfirm({children,description,onConfirm}: {
|
||||
onConfirm: ((e?: React.MouseEvent<HTMLElement>) => void) | undefined
|
||||
description?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}){
|
||||
const {t} = useTranslation()
|
||||
return <Popconfirm
|
||||
rootClassName={'popconfirm-main'}
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<ModalWarning.Icon />}
|
||||
title={<ModalWarning.Title />}
|
||||
description={<div dangerouslySetInnerHTML={{__html:description || t('modal.delete_item_confirm')}}></div>}
|
||||
onConfirm={onConfirm}
|
||||
>{children}</Popconfirm>
|
||||
}
|
@ -2,6 +2,7 @@ import React, {CSSProperties, useCallback, useEffect, useImperativeHandle, useRe
|
||||
import {useInViewport, useScroll} from "ahooks";
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {Spin} from "antd";
|
||||
import {t} from "i18next";
|
||||
|
||||
export type InfiniteScrollerRef = {
|
||||
scrollToPosition: (top: number) => void
|
||||
@ -65,7 +66,7 @@ const InfiniteScroller = React.forwardRef<InfiniteScrollerRef, InfiniteScrollerP
|
||||
{props.loading && <div style={{minHeight:'30vh'}}></div>}
|
||||
{props?.pagination && props.pagination.total > props.pagination.limit * props.pagination.page && (props.loadingPlaceholder ||
|
||||
<div className="data-load-control-element py-10 text-center">
|
||||
<div className="loading-text">加载中...</div>
|
||||
<div className="loading-text">{t('loading')}</div>
|
||||
</div>)}
|
||||
{props?.empty && !props.loading && props.pagination?.total == 0 && <div className="flex justify-center text-center pt-20">
|
||||
<div className="rounded-lg px-4 py-10">
|
||||
|
135
src/components/video/Mp4Player.tsx
Normal file
135
src/components/video/Mp4Player.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
// import TCPlayer from 'tcplayer.js';
|
||||
|
||||
import flvPlayer, { VideoPlayer } from '@/components/video/VideoPlayer.ts';
|
||||
import {PlayerInstance} from "@/hooks/useCache.ts";
|
||||
|
||||
// import 'tcplayer.js/dist/tcplayer.min.css';
|
||||
|
||||
type State = {
|
||||
playing: boolean
|
||||
muted: boolean
|
||||
end?: boolean
|
||||
error?: boolean
|
||||
fullscreen: boolean
|
||||
progress: number
|
||||
playedSeconds: number
|
||||
duration: number
|
||||
}
|
||||
type StateUpdate = Partial<State> | ((prev: State) => Partial<State>)
|
||||
|
||||
type Props = {
|
||||
url?: string; cover?: string; showControls?: boolean; className?: string;
|
||||
poster?: string;
|
||||
onChange?: (state: State) => void;
|
||||
onProgress?: (current:number,duration:number) => void;
|
||||
onPause?: () => void;
|
||||
onPlay?: () => void;
|
||||
muted?: boolean;
|
||||
autoPlay?: boolean;
|
||||
}
|
||||
export type PlayerInstance = {
|
||||
play: (url: string, currentTime: number) => void;
|
||||
pause: () => void;
|
||||
getState: () => State;
|
||||
}
|
||||
|
||||
export const Mp4Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
|
||||
const [tcPlayer, setTcPlayer] = useState<VideoPlayer | null>(null)
|
||||
const [prevUrl, setPrevUrl] = useState<string | undefined>();
|
||||
|
||||
const [state, _setState] = useState<State>({
|
||||
playing: false,
|
||||
muted: false,
|
||||
// 是否全屏
|
||||
fullscreen: false,
|
||||
progress: 0,
|
||||
playedSeconds: 0,
|
||||
duration: 0
|
||||
})
|
||||
|
||||
const setState = (data: StateUpdate) => {
|
||||
_setState(prev => {
|
||||
const _state = typeof (data) === 'function' ? {...prev, ...data(prev)} : {...prev, ...data}
|
||||
props.onChange?.(_state)
|
||||
return _state
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
if(props.url && tcPlayer){
|
||||
tcPlayer.src(props.url)
|
||||
}
|
||||
},[props.url, tcPlayer])
|
||||
|
||||
useEffect(() => {
|
||||
const player = flvPlayer.newInstance({className:props.className});
|
||||
document.querySelector('.video-player-container-inner')!.appendChild(player.getVideo())
|
||||
// const player = TCPlayer(playerVideo, {
|
||||
// controls: props.showControls,
|
||||
// poster: props.poster,
|
||||
// autoplay: typeof(props.autoPlay) != 'undefined' ? props.autoPlay : true,
|
||||
// licenseUrl: AppConfig.TCPlayerLicense
|
||||
// }
|
||||
// ) as TCPlayerInstance;
|
||||
|
||||
player.on('pause', () => {
|
||||
setState({playing: false, end: false, error: false})
|
||||
props.onPause?.()
|
||||
})
|
||||
player.on('playing', () => {
|
||||
setState({playing: true, end: false, error: false})
|
||||
props.onPlay?.()
|
||||
})
|
||||
player.on('ended', () => {
|
||||
setState({end: true, playing: false, error: false})
|
||||
})
|
||||
player.on('timeupdate', () => {
|
||||
props.onProgress?.(player.currentTime(), player.duration())
|
||||
})
|
||||
player.on('error', () => {
|
||||
setState({end: false, playing: false, error: true})
|
||||
})
|
||||
setTcPlayer(() => player)
|
||||
return () => {
|
||||
console.log('destroy video')
|
||||
try{
|
||||
// player.unload();
|
||||
player.dispose();
|
||||
setTcPlayer(() => null)
|
||||
//Array.from(document.querySelectorAll('video')).forEach(v => v.pause())
|
||||
}catch (e){
|
||||
console.log(e)
|
||||
}
|
||||
//playerVideo.parentElement?.removeChild(playerVideo)
|
||||
}
|
||||
}, [])
|
||||
React.useImperativeHandle(ref, () => {
|
||||
return {
|
||||
pause(){
|
||||
if (!tcPlayer) return;
|
||||
tcPlayer.pause()
|
||||
},
|
||||
play: (url, currentTime = 0) => {
|
||||
// console.log('play', url, currentTime)
|
||||
url = url.replace('.flv','.mp4');
|
||||
if (!tcPlayer) return;
|
||||
const player = tcPlayer
|
||||
if (prevUrl == url) {
|
||||
player.currentTime(0)
|
||||
} else {
|
||||
player.src(url)
|
||||
}
|
||||
player.play()
|
||||
setPrevUrl(url)
|
||||
if (currentTime > 0) {
|
||||
player.currentTime(currentTime)
|
||||
}
|
||||
},
|
||||
getState: () => state
|
||||
}
|
||||
})
|
||||
|
||||
return <div className={`video-player relative ${props.className} video-player-container-inner`}>
|
||||
</div>
|
||||
})
|
165
src/components/video/VideoPlayer.ts
Normal file
165
src/components/video/VideoPlayer.ts
Normal file
@ -0,0 +1,165 @@
|
||||
type VideoPlayerEvents =
|
||||
'playing'
|
||||
| 'pause'
|
||||
| 'ended'
|
||||
| 'timeupdate'
|
||||
| 'error'
|
||||
| 'canplay'
|
||||
| 'durationchange'
|
||||
| 'progress'
|
||||
|
||||
type PlayerOptions = {
|
||||
enableLog?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
enum PlayState {
|
||||
playing = 'playing',
|
||||
pause = 'pause',
|
||||
ended = 'ended',
|
||||
error = 'error',
|
||||
}
|
||||
|
||||
export class VideoPlayer {
|
||||
private video?: HTMLVideoElement;
|
||||
private State: PlayState = PlayState.pause;
|
||||
private enable_log = true;
|
||||
private currentDuration = 0;
|
||||
|
||||
|
||||
newInstance(options?: PlayerOptions) {
|
||||
const { className, enableLog=true } = options || {};
|
||||
this.enable_log = enableLog || false;
|
||||
|
||||
if (this.video) {
|
||||
this.video.remove();
|
||||
}
|
||||
if (this.video) {
|
||||
this.video.pause();
|
||||
}
|
||||
// Create video element
|
||||
const playerVideo = document.createElement('video');
|
||||
const playerId = `player-container-${Date.now().toString(16)}`;
|
||||
playerVideo.setAttribute('id', playerId);
|
||||
playerVideo.setAttribute('preload', 'auto');
|
||||
playerVideo.setAttribute('playsInline', 'true');
|
||||
playerVideo.setAttribute('webkit-playsinline', 'true');
|
||||
if (className) playerVideo.setAttribute('className', className);
|
||||
playerVideo.classList.add('digital-video-player');
|
||||
playerVideo.addEventListener('durationchange', (e) => {
|
||||
this.currentDuration = Math.floor(this.video?.duration || 0);
|
||||
this.log('video duration change',e)
|
||||
});
|
||||
playerVideo.addEventListener('error', (e) => {
|
||||
this.log('video error:',e)
|
||||
});
|
||||
this.video = playerVideo;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志
|
||||
* @param args
|
||||
*/
|
||||
log(...args: any[]) {
|
||||
if (this.enable_log) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
getVideo() {
|
||||
return this.video!;
|
||||
}
|
||||
|
||||
// 暂停视频
|
||||
pause() {
|
||||
this.video?.pause();
|
||||
// 更新状态
|
||||
this.State = PlayState.pause;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听视频事件
|
||||
* @param event
|
||||
* @param callback
|
||||
*/
|
||||
on(event: VideoPlayerEvents, callback: () => void) {
|
||||
this.video?.addEventListener(event, callback);
|
||||
|
||||
}
|
||||
|
||||
off(event: VideoPlayerEvents, callback: () => void) {
|
||||
this.video?.removeEventListener(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或这是当前播放时间点
|
||||
* @param time
|
||||
*/
|
||||
currentTime(time?: number) {
|
||||
if (!this.video) return 0;
|
||||
if (typeof time === 'number' && this.video.currentTime !== time) {
|
||||
try {
|
||||
this.video.currentTime = time;
|
||||
} catch (err) {
|
||||
this.log('Failed to set currentTime', err);
|
||||
}
|
||||
}
|
||||
return this.video.currentTime || 0;
|
||||
}
|
||||
|
||||
src(url: string) {
|
||||
if (!this.video) return;
|
||||
this.video.src = url;
|
||||
this.video.load();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放
|
||||
*/
|
||||
play() {
|
||||
if (!this.video) return;
|
||||
this.video.play().then(() => {
|
||||
this.State = PlayState.playing;
|
||||
}).catch((err) => {
|
||||
this.State = PlayState.error;
|
||||
this.log('play error', err);
|
||||
});
|
||||
}
|
||||
|
||||
duration() {
|
||||
return this.currentDuration || this.video?.duration || 0;
|
||||
}
|
||||
dispose(){
|
||||
if(!this.video){
|
||||
return;
|
||||
}
|
||||
// 暂停
|
||||
this.pause();
|
||||
// 移除
|
||||
this.video.parentNode?.removeChild(this.video);
|
||||
this.video = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* player.on('pause', () => {
|
||||
* setState({playing: false, end: false, error: false})
|
||||
* })
|
||||
* player.on('playing', () => {
|
||||
* setState({playing: true, end: false, error: false})
|
||||
* })
|
||||
* player.on('ended', () => {
|
||||
* setState({end: true, playing: false, error: false})
|
||||
* })
|
||||
* player.on('timeupdate', () => {
|
||||
* props.onProgress?.(player.currentTime(), player.duration())
|
||||
* })
|
||||
* player.on('error', () => {
|
||||
* setState({end: false, playing: false, error: true})
|
||||
* })
|
||||
*/
|
||||
|
||||
const videoPlayer = new VideoPlayer();
|
||||
export default videoPlayer;
|
@ -1,11 +1,10 @@
|
||||
// import ReactPlayer from 'react-player'
|
||||
// import {PauseOutlined, PlayCircleOutlined, FullscreenOutlined, FullscreenExitOutlined} from "@ant-design/icons"
|
||||
// import {Progress} from "antd";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {PlayerInstance} from "@/hooks/useCache.ts";
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import TCPlayer from 'tcplayer.js';
|
||||
import 'tcplayer.js/dist/tcplayer.min.css';
|
||||
|
||||
import { PlayerInstance } from '@/hooks/useCache.ts';
|
||||
import videoPlayer from '@/components/video/VideoPlayer.ts';
|
||||
|
||||
type State = {
|
||||
playing: boolean
|
||||
muted: boolean
|
||||
@ -22,7 +21,7 @@ type Props = {
|
||||
url?: string; cover?: string; showControls?: boolean; className?: string;
|
||||
poster?: string;
|
||||
onChange?: (state: State) => void;
|
||||
onProgress?: (current:number,duration:number) => void;
|
||||
onProgress?: (current: number, duration: number) => void;
|
||||
muted?: boolean;
|
||||
autoPlay?: boolean;
|
||||
}
|
||||
@ -32,8 +31,9 @@ export type PlayerInstance = {
|
||||
getState: () => State;
|
||||
}
|
||||
export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
|
||||
const [tcPlayer, setTcPlayer] = useState<TCPlayer | null>(null)
|
||||
const [prevUrl, setPrevUrl] = useState<string | undefined>();
|
||||
const [tcPlayer, setTcPlayer] = useState<TCPlayerInstance | null>(null);
|
||||
|
||||
const [state, _setState] = useState<State>({
|
||||
playing: false,
|
||||
muted: false,
|
||||
@ -42,105 +42,85 @@ export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
|
||||
progress: 0,
|
||||
playedSeconds: 0,
|
||||
duration: 0
|
||||
})
|
||||
});
|
||||
|
||||
const setState = (data: StateUpdate) => {
|
||||
console.log('playstate change', data)
|
||||
console.log('playstate change', data);
|
||||
_setState(prev => {
|
||||
const _state = typeof (data) === 'function' ? {...prev, ...data(prev)} : {...prev, ...data}
|
||||
props.onChange?.(_state)
|
||||
return _state
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
if(props.url && tcPlayer){
|
||||
tcPlayer.src(props.url)
|
||||
}
|
||||
},[props.url, tcPlayer])
|
||||
const _state = typeof (data) === 'function' ? { ...prev, ...data(prev) } : { ...prev, ...data };
|
||||
props.onChange?.(_state);
|
||||
return _state;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(PlayerInstance.length != 0){
|
||||
PlayerInstance.forEach(player => player.pause())
|
||||
PlayerInstance.length = 0
|
||||
if (props.url && tcPlayer) {
|
||||
tcPlayer.src(props.url);
|
||||
}
|
||||
const playerVideo = document.createElement('video');
|
||||
const playerId = `player-container-${Date.now().toString(16)}`;
|
||||
playerVideo.setAttribute('id', playerId)
|
||||
playerVideo.setAttribute('preload', 'auto')
|
||||
playerVideo.setAttribute('playsInline', 'true')
|
||||
playerVideo.setAttribute('webkit-playsinline', 'true')
|
||||
if(props.className) playerVideo.setAttribute('className', props.className)
|
||||
playerVideo.classList.add('digital-video-player')
|
||||
PlayerInstance.push(playerVideo)
|
||||
document.querySelector('.video-player-container-inner')!.appendChild(playerVideo)
|
||||
}, [props.url, tcPlayer]);
|
||||
|
||||
const player = TCPlayer(playerId, {
|
||||
useEffect(() => {
|
||||
const playerVideo = videoPlayer.newInstance().getVideo();
|
||||
document.querySelector('.video-player-container-inner')!.appendChild(playerVideo);
|
||||
|
||||
const flvPlayer = TCPlayer(playerVideo, {
|
||||
//sources: [{src: props.url}],
|
||||
controls: props.showControls,
|
||||
// muted:props.muted,
|
||||
poster: props.poster,
|
||||
autoplay: typeof(props.autoPlay) != 'undefined' ? props.autoPlay : true,
|
||||
autoplay: typeof (props.autoPlay) != 'undefined' ? props.autoPlay : true,
|
||||
licenseUrl: 'https://license.vod2.myqcloud.com/license/v2/1328581896_1/v_cube.license'
|
||||
}
|
||||
)
|
||||
player.on('pause', () => {
|
||||
setState({playing: false, end: false, error: false})
|
||||
})
|
||||
player.on('playing', () => {
|
||||
setState({playing: true, end: false, error: false})
|
||||
})
|
||||
player.on('ended', () => {
|
||||
setState({end: true, playing: false, error: false})
|
||||
})
|
||||
player.on('timeupdate', () => {
|
||||
props.onProgress?.(player.currentTime(), player.duration())
|
||||
})
|
||||
player.on('error', () => {
|
||||
setState({end: false, playing: false, error: true})
|
||||
})
|
||||
setTcPlayer(() => player)
|
||||
);
|
||||
|
||||
flvPlayer.on('pause', () => {
|
||||
setState({ playing: false, end: false, error: false });
|
||||
});
|
||||
flvPlayer.on('playing', () => {
|
||||
setState({ playing: true, end: false, error: false });
|
||||
});
|
||||
flvPlayer.on('ended', () => {
|
||||
setState({ end: true, playing: false, error: false });
|
||||
});
|
||||
flvPlayer.on('timeupdate', () => {
|
||||
props.onProgress?.(flvPlayer.currentTime(), flvPlayer.duration());
|
||||
});
|
||||
flvPlayer.on('error', () => {
|
||||
setState({ end: false, playing: false, error: true });
|
||||
});
|
||||
setTcPlayer(() => flvPlayer);
|
||||
return () => {
|
||||
// if (tcPlayer) {
|
||||
// tcPlayer.pause()
|
||||
// tcPlayer.unload()
|
||||
// }else{
|
||||
// playerVideo.pause()
|
||||
// }
|
||||
console.log('destroy video')
|
||||
try{
|
||||
Array.from(document.querySelectorAll('video')).forEach(v => v.pause())
|
||||
}catch (e){
|
||||
console.log(e)
|
||||
}
|
||||
playerVideo.parentElement?.removeChild(playerVideo)
|
||||
}
|
||||
}, [])
|
||||
tcPlayer?.pause();
|
||||
console.log('destroy video');
|
||||
tcPlayer?.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useImperativeHandle(ref, () => {
|
||||
return {
|
||||
pause(){
|
||||
pause() {
|
||||
if (!tcPlayer) return;
|
||||
tcPlayer.pause()
|
||||
tcPlayer.pause();
|
||||
},
|
||||
play: (url, currentTime = 0) => {
|
||||
console.log('play', url, currentTime)
|
||||
console.log('play', url, currentTime);
|
||||
if (!tcPlayer) return;
|
||||
const player = tcPlayer
|
||||
const player = tcPlayer;
|
||||
if (prevUrl == url) {
|
||||
player.currentTime(0)
|
||||
player.currentTime(0);
|
||||
} else {
|
||||
player.src(url)
|
||||
player.src(url);
|
||||
}
|
||||
player.play()
|
||||
setPrevUrl(url)
|
||||
player.play();
|
||||
setPrevUrl(url);
|
||||
if (currentTime > 0) {
|
||||
player.currentTime(currentTime)
|
||||
player.currentTime(currentTime);
|
||||
}
|
||||
},
|
||||
getState: () => state
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
return <div className={`video-player relative ${props.className} video-player-container-inner`}>
|
||||
</div>
|
||||
})
|
||||
</div>;
|
||||
});
|
@ -5,7 +5,7 @@ import {Checkbox, Popconfirm} from "antd";
|
||||
|
||||
import ImageCover from '@/assets/images/cover.png'
|
||||
import {
|
||||
IconDelete,
|
||||
IconDelete, IconDownloadOutline,
|
||||
IconEdit,
|
||||
IconGenerateFailed,
|
||||
IconGenerating,
|
||||
@ -15,11 +15,16 @@ import {
|
||||
import {VideoStatus} from "@/service/api/video.ts";
|
||||
import {formatTime} from "@/util/strings.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {saveAs} from "file-saver";
|
||||
import {DeleteItemPopoverConfirm} from "@/components/message/confirm.tsx";
|
||||
import {showLoading, showToast} from "@/components/message.ts";
|
||||
|
||||
type Props = {
|
||||
video: VideoInfo | LiveVideoInfo,
|
||||
additionOperation?: React.ReactNode;
|
||||
additionOperationBefore?: React.ReactNode;
|
||||
additionOperationAfter?: React.ReactNode;
|
||||
editable?: boolean;
|
||||
downloadUrl?: string;
|
||||
sortable?: boolean;
|
||||
index?: number;
|
||||
checked?: boolean;
|
||||
@ -31,7 +36,7 @@ type Props = {
|
||||
onRegenerate?: () => void;
|
||||
hideCheckBox?: boolean;
|
||||
onItemClick?: () => void;
|
||||
onRemove?: (action?:'delete' | 'rollback') => void;
|
||||
onRemove?: (action?: 'delete' | 'rollback') => void;
|
||||
removeIcon?: React.ReactNode;
|
||||
id: number;
|
||||
className?: string;
|
||||
@ -40,58 +45,74 @@ type Props = {
|
||||
|
||||
export const VideoListItem = (
|
||||
{
|
||||
id, video, onRemove,removeIcon, checked,playing,
|
||||
onCheckedChange, onEdit, active, editable,
|
||||
className, sortable, type, index,onItemClick,
|
||||
additionOperation,onRegenerate,hideCheckBox
|
||||
id, video, onRemove, removeIcon, checked, playing,
|
||||
onCheckedChange, onEdit, active, editable, downloadUrl,
|
||||
className, sortable, type, index, onItemClick,
|
||||
additionOperationAfter, additionOperationBefore, onRegenerate, hideCheckBox
|
||||
}: Props) => {
|
||||
const {
|
||||
attributes, listeners,
|
||||
setNodeRef, transform
|
||||
} = useSortable({resizeObserverConfig: {}, id})
|
||||
|
||||
const {t} = useTranslation()
|
||||
const {t,i18n} = useTranslation()
|
||||
const [state, setState] = useSetState<{ checked?: boolean }>({})
|
||||
useEffect(() => {
|
||||
setState({checked})
|
||||
}, [checked])
|
||||
|
||||
const generating = (type == 'create' && video.status == VideoStatus.Generating)
|
||||
const failed = (type == 'create' && (video.status != VideoStatus.Generating && video.status != VideoStatus.Generated) )
|
||||
const failed = (type == 'create' && (video.status != VideoStatus.Generating && video.status != VideoStatus.Generated))
|
||||
const handleDownloadVideo = () => {
|
||||
if (downloadUrl && video.status == VideoStatus.Generated) {
|
||||
const ext = downloadUrl.substring(downloadUrl.lastIndexOf('.'))
|
||||
const loading = showLoading(t('downloading'))
|
||||
try{
|
||||
saveAs(downloadUrl, `${video.title || video.video_title}${ext}`)
|
||||
loading.close()
|
||||
}catch (e){
|
||||
loading.update(t('download_failed'),'error')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return <div
|
||||
className={`video-item ${className}`}
|
||||
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}
|
||||
>
|
||||
<div className={`list-row ${generating ? ' status-generating' : ''} ${failed ? 'status-generate-failed' : ''} ${active?'playing':''}`}>
|
||||
<div
|
||||
className={`list-row ${generating ? ' status-generating' : ''} ${failed ? 'status-generate-failed' : ''} ${active ? 'playing' : ''}`}>
|
||||
<div
|
||||
className="col number"
|
||||
{... (sortable && !generating?listeners:{})}
|
||||
{... (sortable && !generating?attributes:{})}
|
||||
{...(sortable && !generating ? listeners : {})}
|
||||
{...(sortable && !generating ? attributes : {})}
|
||||
>{index}</div>
|
||||
<div className="col cover cursor-pointer" onClick={onItemClick}>
|
||||
<div className="relative">
|
||||
<img className="w-[100px] h-[56px] object-cover" src={video.cover || ImageCover}/>
|
||||
<img className="w-[100px] h-[56px] object-cover border border-gray-200" src={video.cover || ImageCover}/>
|
||||
{generating &&
|
||||
<div className={'absolute rounded inset-0 bg-black/40 backdrop-blur-[1px] text-white flex items-center justify-center'}>
|
||||
<div
|
||||
className={'absolute rounded inset-0 bg-black/40 backdrop-blur-[1px] text-white flex items-center justify-center'}>
|
||||
<div className="text-center">
|
||||
<IconGenerating className="inline-block text-xl" />
|
||||
<IconGenerating className="inline-block text-xl"/>
|
||||
<div className="text-xs">{t('video.generating')}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{failed &&
|
||||
<div className={'absolute rounded inset-0 bg-black/40 backdrop-blur-[1px] text-white flex items-center justify-center'}>
|
||||
<div
|
||||
className={'absolute rounded inset-0 bg-black/40 backdrop-blur-[1px] text-white flex items-center justify-center'}>
|
||||
<div className="text-center">
|
||||
<IconGenerateFailed className="inline-block text-xl" />
|
||||
<IconGenerateFailed className="inline-block text-xl"/>
|
||||
<div className="text-xs">{t('video.generate_failed')}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{/* && active*/}
|
||||
{!generating && !failed && playing && <div className={'absolute rounded inset-0 backdrop-blur-[1px] bg-black/40 text-white flex items-center justify-center'}>
|
||||
{!generating && !failed && playing && <div
|
||||
className={'absolute rounded inset-0 backdrop-blur-[1px] bg-black/40 text-white flex items-center justify-center'}>
|
||||
<div className="text-center">
|
||||
<IconPlaying className="inline-block text-xl" />
|
||||
<IconPlaying className="inline-block text-xl"/>
|
||||
<div className="text-xs">{t('video.playing')}</div>
|
||||
</div>
|
||||
</div>}
|
||||
@ -99,8 +120,8 @@ export const VideoListItem = (
|
||||
</div>
|
||||
<div
|
||||
className="col title"
|
||||
{... (sortable && !generating?listeners:{})}
|
||||
{... (sortable && !generating?attributes:{})}
|
||||
{...(sortable && !generating ? listeners : {})}
|
||||
{...(sortable && !generating ? attributes : {})}
|
||||
>
|
||||
<div className="line-clamp-2">
|
||||
{video.title || video.video_title}
|
||||
@ -108,51 +129,63 @@ export const VideoListItem = (
|
||||
</div>
|
||||
<div
|
||||
className="col generated-time"
|
||||
{... (sortable && !generating?listeners:{})}
|
||||
{... (sortable && !generating?attributes:{})}
|
||||
>{video.ctime ? formatTime(video.ctime,'min') : '-'}</div>
|
||||
{...(sortable && !generating ? listeners : {})}
|
||||
{...(sortable && !generating ? attributes : {})}
|
||||
>{video.ctime ? formatTime(video.ctime, 'min') : '-'}</div>
|
||||
<div className="col operation">
|
||||
{/*{sortable && !generating && (!active ?*/}
|
||||
{/* <button className="hover:text-blue-500 cursor-move">*/}
|
||||
{/* <MenuOutlined/>*/}
|
||||
{/* </button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>)}*/}
|
||||
<div className={"flex items-center justify-start gap-6"}>
|
||||
<div className={"flex items-center justify-center gap-5"}>
|
||||
{downloadUrl && video.status == VideoStatus.Generated &&
|
||||
<button className="hover:text-blue-500" onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleDownloadVideo?.()
|
||||
}} style={{fontSize: '1.1em'}} title={i18n.language == 'zh-CN'?'下载':'Download'}>
|
||||
<IconDownloadOutline/>
|
||||
</button>}
|
||||
{additionOperationBefore}
|
||||
{editable && !generating && <>
|
||||
{onEdit &&
|
||||
<button className="hover:text-blue-500" onClick={e=>{
|
||||
{onEdit && <button
|
||||
className="hover:text-blue-500" onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onEdit?.()
|
||||
}} style={{fontSize: '1.1em'}}>
|
||||
}} style={{fontSize: '1.1em'}} title={i18n.language == 'zh-CN'?'修改':'Modify'}>
|
||||
<IconEdit/>
|
||||
</button>}
|
||||
{onRegenerate &&
|
||||
<button className="text-red-400 hover:text-blue-500" onClick={e=>{
|
||||
{onRegenerate && <button
|
||||
className="text-red-400 hover:text-blue-500" onClick={e => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onRegenerate?.()
|
||||
}} style={{fontSize: '1.1em'}}>
|
||||
}} style={{fontSize: '1.1em'}} title={i18n.language == 'zh-CN'?'重新生成':'Regenerate'}>
|
||||
<IconRegenerate/>
|
||||
</button>}
|
||||
|
||||
{onRemove && <Popconfirm
|
||||
rootClassName={'popconfirm-main'}
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={t('video.delete_confirm_title')}
|
||||
// description={`删除后需从重新${type == 'create' ? '生成' : '推流'}`}
|
||||
onConfirm={() => onRemove(failed ? 'rollback' : 'delete')}
|
||||
><button className="hover:text-blue-500">{removeIcon?removeIcon:(failed?<IconRollbackCircle />:<IconDelete/>)}</button></Popconfirm>}
|
||||
{hideCheckBox ? <span className={"inline-block w-[18px] h-1"}></span> : <Checkbox checked={state.checked} onChange={() => {
|
||||
{onRemove && !failed && <DeleteItemPopoverConfirm
|
||||
description={failed ? t('video.rollback_confirm_title') : undefined}
|
||||
onConfirm={() => onRemove(failed ? 'rollback' : 'delete')}>
|
||||
<button className="hover:text-blue-500" title={
|
||||
failed ? (i18n.language == 'zh-CN'?'重新生成':'Regenerate') : i18n.language == 'zh-CN'?'删除':'Delete'
|
||||
} style={{fontSize:20}}>
|
||||
{removeIcon ? removeIcon : (failed ?
|
||||
<IconRollbackCircle/> :
|
||||
<IconDelete/>)}
|
||||
</button>
|
||||
</DeleteItemPopoverConfirm>}
|
||||
{hideCheckBox ? <></> :
|
||||
<Checkbox checked={state.checked} onChange={() => {
|
||||
if (onCheckedChange) {
|
||||
onCheckedChange(!state.checked)
|
||||
} else {
|
||||
setState({checked: !state.checked})
|
||||
}
|
||||
}} />}
|
||||
}}/>}
|
||||
</>}
|
||||
{additionOperation}
|
||||
{additionOperationAfter}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,6 +5,10 @@ import {getAuthToken, setAuthToken} from "@/hooks/useAuth.ts";
|
||||
import {auth} from "@/service/api/user.ts";
|
||||
import {getAllCategory} from "@/service/api/article.ts";
|
||||
import {BizError} from "@/service/types.ts";
|
||||
import {getRemainingDuration} from "@/service/api/order.ts";
|
||||
import {Modal} from "antd";
|
||||
import ModalWarning from "@/components/icons/ModalWarning.tsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const UserRoleStorageKey = 'user-current-role';
|
||||
|
||||
@ -54,9 +58,9 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
|
||||
token
|
||||
}
|
||||
})
|
||||
}catch (e){
|
||||
} catch (e) {
|
||||
const err = e as BizError;
|
||||
if(err.code == 1001){
|
||||
if (err.code == 1001) {
|
||||
// token失效
|
||||
setAuthToken(null)
|
||||
dispatch({
|
||||
|
@ -1,15 +1,10 @@
|
||||
{
|
||||
"AppTitle": "AI Livesteam",
|
||||
"go_to_home": "Go to Homepage" ,
|
||||
"Hello": "Hello",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"service_error": "Service exception, please contact customer support.",
|
||||
"error_401": "You do not have permission to access this page",
|
||||
"error_403": "You do not have permission to access this page",
|
||||
"error_404": "Page not found",
|
||||
"error_500": "Service exception, please contact customer support.",
|
||||
"confirm": {
|
||||
"ok": "Confirm",
|
||||
"push_title": "Push Notice",
|
||||
"push_video": "Are you sure editing selected news?",
|
||||
"title": "Notice"
|
||||
@ -20,14 +15,21 @@
|
||||
"delete_failed": "Delete failed",
|
||||
"delete_success": "Delete success",
|
||||
"download": "Download",
|
||||
"download_fail": "Download Failed",
|
||||
"downloading": "Downloading...",
|
||||
"error_401": "You do not have permission to access this page",
|
||||
"error_403": "You do not have permission to access this page",
|
||||
"error_404": "Page not found",
|
||||
"error_500": "Service exception, please contact customer support.",
|
||||
"generating": {
|
||||
"title": "Preview - Click the video to play"
|
||||
},
|
||||
"go_to_home": "Go to Homepage",
|
||||
"history": {
|
||||
"delete_confirm": "Are you sure you want to delete this video?",
|
||||
"push_success": "Streaming success",
|
||||
"search_key": "Please enter title keywords",
|
||||
"text": "Video history"
|
||||
"text": "Recycle Bin"
|
||||
},
|
||||
"history.pushed": "Streaming: {{count}}",
|
||||
"live": {
|
||||
@ -38,6 +40,7 @@
|
||||
"playlist_count": "{{count}} videos in total",
|
||||
"title": "Livestream"
|
||||
},
|
||||
"loading": "Loading...",
|
||||
"login": {
|
||||
"code_sending": "Sending...",
|
||||
"invalid_username_or_pwd": "Invalid phone number or code",
|
||||
@ -49,6 +52,32 @@
|
||||
"username": "Please enter your phone number",
|
||||
"welcome": "Welcome"
|
||||
},
|
||||
"message": {
|
||||
"save_failed": "Save failed",
|
||||
"save_success": "Save success"
|
||||
},
|
||||
"modal": {
|
||||
"delete_item_confirm": "Are you sure you want to delete this item?",
|
||||
"hot_news": {
|
||||
"edit_auto": "Customize",
|
||||
"edit_manual": "Customize",
|
||||
"empty_notice_message": "Some live news items are incomplete. Please update them or disable the customization feature.",
|
||||
"empty_notice_title": "Warning",
|
||||
"title": "Live news"
|
||||
},
|
||||
"push_article": {
|
||||
"action_all": "Still generating",
|
||||
"action_cancel": "Cancel",
|
||||
"action_skip": "Skip the news",
|
||||
"content_error": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, and <span class=\"modal-count-warning\">{{error_count}}</span> metahuman contents are too short in these news below. Do you want to transfer them to videos?",
|
||||
"content_error_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected, and the metahuman content is too short in this news. Do you want to transfer it to a video?",
|
||||
"content_normal": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, Do you want to transfer them into videos?",
|
||||
"content_normal_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected. Do you want to transfer it to a video?",
|
||||
"empty_notice_title": "Warning",
|
||||
"error_title": "Abnormal news"
|
||||
},
|
||||
"warning": "Warning"
|
||||
},
|
||||
"nav": {
|
||||
"editing": "Editing",
|
||||
"generating": "Generating",
|
||||
@ -64,11 +93,17 @@
|
||||
"delete_the_picture": "Are you sure delete the picture?",
|
||||
"download_empty": "Please select the news to download",
|
||||
"download_failed": "Download failed!",
|
||||
"edit": {
|
||||
"bg": "Background",
|
||||
"tag": "Tag",
|
||||
"tag_length_error": "Video tag only limit 4 words ",
|
||||
"tag_placeholder": "Example: Enterprise dynamics"
|
||||
},
|
||||
"edit_add_group": "Add Group",
|
||||
"edit_delete_group": "Delete Group",
|
||||
"edit_delete_group_confirm": "Are you sure you want to delete the group?",
|
||||
"edit_digital_text": "Metahuman Material",
|
||||
"edit_empty_group_content": "Please other media material",
|
||||
"edit_empty_group_content": "To generate a fully Metahuman video, ensure that no other media materials are included. For all other cases, both text and images must be provided in the media material section.",
|
||||
"edit_empty_human_content": "Please enter meta human material",
|
||||
"edit_form_search": "Please enter title keywords",
|
||||
"edit_generate_again": "Regenerate",
|
||||
@ -89,8 +124,8 @@
|
||||
"get_detail_error": "Get new details failed",
|
||||
"image_count": "Images",
|
||||
"materials": {
|
||||
"title": "News Materials",
|
||||
"add_group": "Add Group"
|
||||
"add_group": "Add Group",
|
||||
"title": "News Materials"
|
||||
},
|
||||
"news_all_source": "All",
|
||||
"push_empty": "please select the news to edit",
|
||||
@ -101,15 +136,40 @@
|
||||
"push_success": "Push success",
|
||||
"push_to_edit": "Editing",
|
||||
"pushed": "Editing",
|
||||
"save_text": "Save",
|
||||
"search_key_title": "Please enter title keywords",
|
||||
"source": "Source",
|
||||
"title": "Content",
|
||||
"title": "Title",
|
||||
"title_image_count": "No. of images",
|
||||
"title_operate": "",
|
||||
"title_time": "Time stamp",
|
||||
"title_word_count": "Word count",
|
||||
"word_count": "Words"
|
||||
},
|
||||
"order": {
|
||||
"left_time": "Remaining time",
|
||||
"list": {
|
||||
"consume_time": "Duration",
|
||||
"cover": "Cover",
|
||||
"id": "No.",
|
||||
"operator": "User",
|
||||
"order_time": "Time stamp",
|
||||
"title": "Title"
|
||||
},
|
||||
"remaining_duration_warning": "Unable to generate videos due to insufficient remaining time?",
|
||||
"text": "Orders"
|
||||
},
|
||||
"page": {
|
||||
"size_10": "10 per page",
|
||||
"size_20": "20 per page",
|
||||
"size_30": "30 per page",
|
||||
"total_item": "{{total}} videos in total"
|
||||
},
|
||||
"recycle": {
|
||||
"remove_forever": "Remove Forever",
|
||||
"restore_video": "Restore"
|
||||
},
|
||||
"save_operation": "Save",
|
||||
"select": {
|
||||
"pushed": "Pushed: {{count}}",
|
||||
"select_all": "Select all",
|
||||
@ -118,6 +178,7 @@
|
||||
"text": "Select",
|
||||
"total": "Total: {{count}}"
|
||||
},
|
||||
"service_error": "Service exception, please contact customer support.",
|
||||
"time_filter": {
|
||||
"all": "All",
|
||||
"last_week": "Last week",
|
||||
@ -126,9 +187,11 @@
|
||||
"past_4_hour": "Past 4 hour",
|
||||
"past_hour": "Past 1 hour"
|
||||
},
|
||||
"title": "Title",
|
||||
"upload": {
|
||||
"delete_confirm": "Are you sure delete the picture?",
|
||||
"upload_failed": "Upload failed",
|
||||
"upload_file_type_error": "Only support upload image",
|
||||
"upload_image": "Upload Image"
|
||||
},
|
||||
"user": {
|
||||
@ -141,15 +204,21 @@
|
||||
"delete_description": "Are you sure you want to delete the video?",
|
||||
"delete_description_count": "Are you sure you want to delete these {{count}} videos?",
|
||||
"delete_empty": "Select the video you want to delete",
|
||||
"delete_forever_confirm": "Do you want to permutely delete it?",
|
||||
"delete_forever_confirm_count": "Do you want to permutely delete these videos?",
|
||||
"download": "Download",
|
||||
"generating": "Generating",
|
||||
"generate_failed": "Generate Failed",
|
||||
"generating": "Generating",
|
||||
"live_rollback_confirm_title": "Are you sure you want to rollback this video?",
|
||||
"playing": "Playing",
|
||||
"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",
|
||||
"restore_confirm": "Do you want to restore it to the generating page?",
|
||||
"restore_confirm_count": "Do you want to restore these videos to the generating page?",
|
||||
"rollback_confirm_title": "Are you sure you want to revert this video?",
|
||||
"sort_modify_confirm": "Are you change video sequence?",
|
||||
"sort_modify_failed": "Video sequence change failed",
|
||||
"sort_modify_live_success": "Video sequence changed",
|
||||
@ -159,18 +228,5 @@
|
||||
"title_generated_time": "Time stamp",
|
||||
"title_operation": "",
|
||||
"title_thumb": "Cover"
|
||||
},
|
||||
"modal": {
|
||||
"warning": "Warning",
|
||||
"push_article": {
|
||||
"content_normal": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, Do you want to transfer them into videos?",
|
||||
"content_normal_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected. Do you want to transfer it to a video?",
|
||||
"content_error": "<span class=\"modal-count-normal\">{{count}}</span> news are selected, and <span class=\"modal-count-warning\">{{error_count}}</span> metahuman contents are too short in these news below. Do you want to transfer them to videos?",
|
||||
"content_error_single": "<span class=\"modal-count-normal\">{{count}}</span> news is selected, and the metahuman content is too short in this news. Do you want to transfer it to a video?",
|
||||
"error_title": "Abnormal news",
|
||||
"action_cancel": "Cancel",
|
||||
"action_skip": "Skip the news",
|
||||
"action_all": "Still generating"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,10 @@
|
||||
{
|
||||
"AppTitle": "数字人直播",
|
||||
"go_to_home": "返回首页" ,
|
||||
"Hello": "你好",
|
||||
"cancel": "取消",
|
||||
"close": "关闭",
|
||||
"service_error": "新闻异常,无法生成,请咨询客服",
|
||||
"error_401": "您没有权限访问本页面",
|
||||
"error_403": "您没有权限访问本页面",
|
||||
"error_404": "访问的页面不存在",
|
||||
"error_500": "服务异常,请咨询客服.",
|
||||
"confirm": {
|
||||
"ok": "确定",
|
||||
"push_title": "推流提示",
|
||||
"push_video": "是否确定一键推流选中新闻视频?",
|
||||
"title": "提示"
|
||||
@ -20,14 +15,21 @@
|
||||
"delete_failed": "删除失败",
|
||||
"delete_success": "删除成功",
|
||||
"download": "下载",
|
||||
"download_fail": "下载失败",
|
||||
"downloading": "下载中...",
|
||||
"error_401": "您没有权限访问本页面",
|
||||
"error_403": "您没有权限访问本页面",
|
||||
"error_404": "访问的页面不存在",
|
||||
"error_500": "服务异常,请咨询客服.",
|
||||
"generating": {
|
||||
"title": "预览视频 - 点击视频列表播放"
|
||||
},
|
||||
"go_to_home": "返回首页",
|
||||
"history": {
|
||||
"delete_confirm": "是否要删除该视频",
|
||||
"push_success": "一键推流成功,已推流至数字人直播间,请查看!",
|
||||
"search_key": "请输入视频标题关键字进行信息",
|
||||
"text": "历史视频"
|
||||
"text": "回收站"
|
||||
},
|
||||
"history.pushed": "已推送 {{count}} 条",
|
||||
"live": {
|
||||
@ -38,6 +40,7 @@
|
||||
"playlist_count": "当前播放列表共 {{count}} 条",
|
||||
"title": "直播界面"
|
||||
},
|
||||
"loading": "加载中...",
|
||||
"login": {
|
||||
"code_sending": "发送中",
|
||||
"invalid_username_or_pwd": "账号或密码错误",
|
||||
@ -49,6 +52,32 @@
|
||||
"username": "请输入账号",
|
||||
"welcome": "欢迎登录"
|
||||
},
|
||||
"message": {
|
||||
"save_failed": "保存失败",
|
||||
"save_success": "保存成功"
|
||||
},
|
||||
"modal": {
|
||||
"delete_item_confirm": "您确定要删除吗?",
|
||||
"hot_news": {
|
||||
"edit_auto": "自定义",
|
||||
"edit_manual": "自定义",
|
||||
"empty_notice_message": "自定义的“新闻热点”尚未填写完毕,<br/>请填写全部热点,或开启智能填充",
|
||||
"empty_notice_title": "操作提示",
|
||||
"title": "视频下方热点(跑马灯)"
|
||||
},
|
||||
"push_article": {
|
||||
"action_all": "全部生成",
|
||||
"action_cancel": "全部取消",
|
||||
"action_skip": "跳过异常新闻",
|
||||
"content_error": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
|
||||
"content_error_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
|
||||
"content_normal": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否全部生成?",
|
||||
"content_normal_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否生成?",
|
||||
"empty_notice_title": "操作提示",
|
||||
"error_title": "异常新闻"
|
||||
},
|
||||
"warning": "操作提示"
|
||||
},
|
||||
"nav": {
|
||||
"editing": "新闻编辑",
|
||||
"generating": "视频生成",
|
||||
@ -56,7 +85,7 @@
|
||||
"materials": "新闻素材"
|
||||
},
|
||||
"news": {
|
||||
"delete_confirm": "你确定要删除吗?",
|
||||
"delete_confirm": "您确定要删除吗?",
|
||||
"delete_confirm_count": "你确定要删除选择的 {{count}} 条新闻吗?",
|
||||
"delete_description": "删除后需从新闻素材中重新选择",
|
||||
"delete_description_count": "删除后需从新闻素材中重新选择",
|
||||
@ -64,11 +93,17 @@
|
||||
"delete_the_picture": "请确认删除此图片",
|
||||
"download_empty": "请选择要下载的新闻",
|
||||
"download_failed": "下载新闻失败,请重试!",
|
||||
"edit": {
|
||||
"bg": "背景",
|
||||
"tag": "标签",
|
||||
"tag_length_error": "标签长度不能超过4个字",
|
||||
"tag_placeholder": "例:企业动态"
|
||||
},
|
||||
"edit_add_group": "新增分组",
|
||||
"edit_delete_group": "删除此分组",
|
||||
"edit_delete_group_confirm": "请确认删除此分组?",
|
||||
"edit_digital_text": "数字人主播台编辑区",
|
||||
"edit_empty_group_content": "素材区的文本和图片,均不得为空",
|
||||
"edit_empty_group_content": "如仅需数字人播报,请勿在素材融合区填写内容;如需展示图文信息,素材融合区的文本、图片均不得为空",
|
||||
"edit_empty_human_content": "请先填写数字人播报内容",
|
||||
"edit_form_search": "请输入新闻标题关键词进行搜索",
|
||||
"edit_generate_again": "重新生成",
|
||||
@ -89,8 +124,8 @@
|
||||
"get_detail_error": "获取新闻详情失败",
|
||||
"image_count": "图片数",
|
||||
"materials": {
|
||||
"title": "新闻素材",
|
||||
"add_group": "新增分组"
|
||||
"add_group": "新增分组",
|
||||
"title": "新闻素材"
|
||||
},
|
||||
"news_all_source": "全部来源",
|
||||
"push_empty": "请选择要推入编辑的新闻",
|
||||
@ -101,6 +136,7 @@
|
||||
"push_success": "推送成功",
|
||||
"push_to_edit": "推入编辑",
|
||||
"pushed": "已推送",
|
||||
"save_text": "保存",
|
||||
"search_key_title": "请输入新闻标题关键词进行搜索",
|
||||
"source": "来源",
|
||||
"title": "标题",
|
||||
@ -110,6 +146,30 @@
|
||||
"title_word_count": "字数",
|
||||
"word_count": "字数"
|
||||
},
|
||||
"order": {
|
||||
"left_time": "当前剩余时长",
|
||||
"list": {
|
||||
"consume_time": "消费时长",
|
||||
"cover": "缩略图",
|
||||
"id": "订单编号",
|
||||
"operator": "操作人",
|
||||
"order_time": "下单时间",
|
||||
"title": "标题"
|
||||
},
|
||||
"remaining_duration_warning": "视频生成剩余时长为零,将无法生成视频,请尽快充值额度。",
|
||||
"text": "订单记录"
|
||||
},
|
||||
"page": {
|
||||
"size_10": "10条/页",
|
||||
"size_20": "20条/页",
|
||||
"size_30": "30条/页",
|
||||
"total_item": "共计{{total}}条"
|
||||
},
|
||||
"recycle": {
|
||||
"remove_forever": "彻底删除",
|
||||
"restore_video": "还原视频"
|
||||
},
|
||||
"save_operation": "保存操作",
|
||||
"select": {
|
||||
"pushed": "已推送: {{count}} 条",
|
||||
"select_all": "全选",
|
||||
@ -118,6 +178,7 @@
|
||||
"text": "选择",
|
||||
"total": "总共 {{count}} 条"
|
||||
},
|
||||
"service_error": "新闻异常,无法生成,请咨询客服",
|
||||
"time_filter": {
|
||||
"all": "所有时间",
|
||||
"last_week": "近一周",
|
||||
@ -126,9 +187,11 @@
|
||||
"past_4_hour": "四小时内",
|
||||
"past_hour": "一小时内"
|
||||
},
|
||||
"title": "标题",
|
||||
"upload": {
|
||||
"delete_confirm": "请确认删除此图片?",
|
||||
"upload_failed": "上传图片失败,请重试",
|
||||
"upload_file_type_error": "仅支持上传图片",
|
||||
"upload_image": "上传图片"
|
||||
},
|
||||
"user": {
|
||||
@ -141,15 +204,21 @@
|
||||
"delete_description": "已选择{{count}}条,确定要全部删除吗?",
|
||||
"delete_description_count": "已选择{{count}}条,确定要全部删除吗?",
|
||||
"delete_empty": "请选择要删除的视频",
|
||||
"delete_forever_confirm": "是否彻底删除选中的视频? <br />这些视频将无法找回",
|
||||
"delete_forever_confirm_count": "是否彻底删除选中的视频? <br />这些视频将无法找回!",
|
||||
"download": "下载视频",
|
||||
"generating": "生成中",
|
||||
"generate_failed": "生成失败",
|
||||
"generating": "生成中",
|
||||
"live_rollback_confirm_title": "你确定要回退此视频吗 ",
|
||||
"playing": "播放中",
|
||||
"push_confirm": "是否确定一键推流选中新闻视频?",
|
||||
"push_empty": "请选择要推流的新闻视频",
|
||||
"push_failed": "选择视频中有部分视频还在生成中无法推送,推流成功视频前往数字人直播间页面查看!",
|
||||
"push_success": "一键推流成功,已推流至数字人直播间,请前往数字人直播间页面查看!",
|
||||
"push_to_live": "一键推流",
|
||||
"restore_confirm": "是否将选中视频,还原到视频生成页?",
|
||||
"restore_confirm_count": "是否将选中视频,还原到视频生成页",
|
||||
"rollback_confirm_title": "您确定要回退此视频吗?",
|
||||
"sort_modify_confirm": "是否采纳移动视频位置操作?",
|
||||
"sort_modify_failed": "调整视频顺序失败,请重试!",
|
||||
"sort_modify_live_success": "已完成直播队列的修改",
|
||||
@ -159,18 +228,5 @@
|
||||
"title_generated_time": "生成时间",
|
||||
"title_operation": "操作",
|
||||
"title_thumb": "缩略图"
|
||||
},
|
||||
"modal": {
|
||||
"warning": "操作提示",
|
||||
"push_article": {
|
||||
"content_normal": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否全部生成?",
|
||||
"content_normal_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,是否生成?",
|
||||
"content_error": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
|
||||
"content_error_single": "已选中<span class=\"modal-count-normal\">{{count}}</span>条新闻,<span class=\"modal-count-warning\">{{error_count}}</span>条新闻数字人播报字数过少,是否生成全部<span class=\"modal-count-normal\">{{count}}</span>条视频?",
|
||||
"error_title": "异常新闻",
|
||||
"action_cancel": "全部取消",
|
||||
"action_skip": "跳过异常新闻",
|
||||
"action_all": "全部生成"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import {useMount} from "ahooks";
|
||||
import {getLiveUrl} from "@/service/api/live.ts";
|
||||
import React, {useState} from "react";
|
||||
|
||||
import {getLiveUrl} from "@/service/api/live.ts";
|
||||
import {Player} from "@/components/video/player.tsx";
|
||||
import './style.scss'
|
||||
|
||||
|
@ -1,32 +1,36 @@
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {Checkbox, Empty, Modal, Space} from "antd";
|
||||
import {Checkbox, Empty, Popconfirm, Space} from "antd";
|
||||
import {SortableContext, arrayMove} from '@dnd-kit/sortable';
|
||||
import {DndContext} from "@dnd-kit/core";
|
||||
import FlvJs from "flv.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useSetState} from "ahooks";
|
||||
|
||||
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||
import {deleteByIds, getList, modifyOrder, playState} from "@/service/api/live.ts";
|
||||
|
||||
import {deleteByIds, getList, modifyOrder, playState, restoreByIds} from "@/service/api/live.ts";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import ButtonBatch from "@/components/button-batch.tsx";
|
||||
import FlvJs from "flv.js";
|
||||
import {formatDuration} from "@/util/strings.ts";
|
||||
import {useSetState} from "ahooks";
|
||||
import {Player, PlayerInstance} from "@/components/video/player.tsx";
|
||||
import {IconDelete, IconLocked, IconUnlock} from "@/components/icons";
|
||||
import {Mp4Player as Player, PlayerInstance} from "@/components/video/Mp4Player.tsx";
|
||||
import {IconDelete, IconLocked, IconRollbackCircle} 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";
|
||||
import {ModalWarningIcon, ModalWarningTitle} from "@/components/icons/ModalWarning.tsx";
|
||||
|
||||
import styles from "./style.module.scss"
|
||||
|
||||
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[]>([])
|
||||
const [modal, contextHolder] = Modal.useModal()
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
const [editable, setEditable] = useState<boolean>(false)
|
||||
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
|
||||
const [rollbackIds,setRollbackIds] = useState<Id[]>([])
|
||||
const [delIds,setDelIds] = useState<Id[]>([])
|
||||
|
||||
const [state, setState] = useSetState({
|
||||
playId:-1,
|
||||
@ -83,6 +87,8 @@ export default function LiveIndex() {
|
||||
const playedTime = (Date.now() / 1000 >> 0) - liveState.live_start_time
|
||||
if (playedTime < 0 || playedTime > duration) { // 已播放时间大于总时长了
|
||||
//initPlayingState() // 重新获取播放状态
|
||||
console.log('已播放时间大于总时长')
|
||||
cache.timerLoadState = setTimeout(initPlayingState, 5000)
|
||||
return;
|
||||
}
|
||||
player.current?.play(video.video_oss_url, playedTime)
|
||||
@ -93,6 +99,7 @@ export default function LiveIndex() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化播放状态
|
||||
const initPlayingState = () => {
|
||||
player.current?.pause();
|
||||
@ -152,44 +159,59 @@ export default function LiveIndex() {
|
||||
|
||||
// 删除视频
|
||||
const processDeleteVideo = async (ids: Id[]) => {
|
||||
deleteByIds(ids).then(() => {
|
||||
showToast(t('delete_success'), 'success')
|
||||
loadList()
|
||||
}).catch(showErrorToast)
|
||||
// 临时记录删除的id
|
||||
setDelIds(_=>[...ids,..._])
|
||||
// deleteByIds(ids).then(() => {
|
||||
// showToast(t('delete_success'), 'success')
|
||||
// loadList()
|
||||
// }).catch(showErrorToast)
|
||||
}
|
||||
const resetState = (editable: boolean)=>{
|
||||
setEditable(editable)
|
||||
setCheckedIdArray([])
|
||||
setRollbackIds(()=>[])
|
||||
setDelIds(()=>[])
|
||||
setState({checkedAll: false})
|
||||
}
|
||||
// 状态:锁定->解锁
|
||||
const handleSetEditable = ()=>{
|
||||
resetState(true)
|
||||
}
|
||||
//
|
||||
const handleConfirm = () => {
|
||||
const handleCancel = ()=>{
|
||||
resetState(false)
|
||||
}
|
||||
const handleRollback = (v:LiveVideoInfo)=>{
|
||||
setRollbackIds(_=>[v.id,..._])
|
||||
}
|
||||
const handleConfirm = async () => {
|
||||
if (!editable) {
|
||||
setEditable(true)
|
||||
return;
|
||||
}
|
||||
const newSort = videoData.map(s => s.id).join(',')
|
||||
if (newSort == state.originSort) {
|
||||
setEditable(false)
|
||||
return;
|
||||
const ids = videoData
|
||||
.filter(s=>!(delIds.includes(s.id) || rollbackIds.includes(s.id)))
|
||||
.map(s => s.id)
|
||||
try{
|
||||
// 删除
|
||||
if(delIds.length > 0) {
|
||||
await deleteByIds(delIds)
|
||||
}
|
||||
modal.confirm({
|
||||
title: t('confirm.title'),
|
||||
content: t('video.sort_modify_confirm'),
|
||||
centered: true,
|
||||
onOk: () => {
|
||||
//showToast('编辑成功!!!', 'info');
|
||||
modifyOrder(videoData.map(s => s.id)).then(() => {
|
||||
showToast(t('video.sort_modify_live_success'), 'success')
|
||||
setEditable(false)
|
||||
}).catch(() => {
|
||||
showToast(t('video.sort_modify_failed'), 'warning')
|
||||
})
|
||||
},
|
||||
onCancel: () => {
|
||||
showToast(t('video.sort_modify_rollback'), 'info');
|
||||
if(rollbackIds.length > 0) {
|
||||
await restoreByIds(rollbackIds)
|
||||
}
|
||||
// 调整排序
|
||||
await modifyOrder(ids);
|
||||
showToast(t('message.save_success'), 'success')
|
||||
}catch (e){
|
||||
console.log(e)
|
||||
showToast(t('message.save_failed'), 'error')
|
||||
}finally {
|
||||
loadList()
|
||||
setEditable(false)
|
||||
resetState(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleAllCheckedChange = () => {
|
||||
if(editable) return;
|
||||
if(!editable) return;
|
||||
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
|
||||
setState({
|
||||
checkedAll: !state.checkedAll
|
||||
@ -250,7 +272,7 @@ export default function LiveIndex() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="video-list-container video-list-sort-container flex flex-col flex-1 mt-2">
|
||||
<div className="live-control flex justify-between mb-1">
|
||||
<div className="live-control flex justify-between mb-1 h-[30px]">
|
||||
<div>
|
||||
<Space>
|
||||
{/*<span className={"text-blue-500"}>视频正在播放{state.activeIndex == -1 ? '' : `到 ${state.activeIndex + 1} 条`}</span>*/}
|
||||
@ -259,12 +281,14 @@ export default function LiveIndex() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className={'flex items-center text-gray-400 cursor-pointer select-none'}
|
||||
onClick={handleConfirm}>
|
||||
<span>{editable ? t('live.edit_unlock') : t('live.edit_locked')}</span>
|
||||
<span className="ml-2 text-sm">
|
||||
{editable ? <IconUnlock/> : <IconLocked/>}
|
||||
</span>
|
||||
<div className={'flex items-center text-gray-400 cursor-pointer select-none'}>
|
||||
{editable ? (<Space size={15}>
|
||||
<button className={styles.btnDefault} onClick={handleCancel}>{t('cancel')}</button>
|
||||
<button className={styles.btn} onClick={handleConfirm}>{t('save_operation')}</button>
|
||||
</Space>):(<div className="flex items-center " onClick={handleSetEditable}>
|
||||
{t('live.edit_locked')}
|
||||
<span className="ml-2 text-sm"><IconLocked/></span>
|
||||
</div>)}
|
||||
</div>
|
||||
<div className="check-all ml-10">
|
||||
<button disabled={editable} className={`${editable?'':'hover:text-blue-300'} text-gray-400`}
|
||||
@ -272,7 +296,7 @@ export default function LiveIndex() {
|
||||
<span className="text-sm mr-2 whitespace-nowrap">{t('select.select_all')}</span>
|
||||
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
|
||||
</button>
|
||||
<Checkbox disabled={editable} checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
|
||||
<Checkbox disabled={!editable} checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -308,7 +332,7 @@ export default function LiveIndex() {
|
||||
}
|
||||
}}>
|
||||
<SortableContext items={videoData}>
|
||||
{videoData.map((v, index) => (
|
||||
{videoData.filter(v=>(!(delIds.includes(v.id) || rollbackIds.includes(v.id)))).map((v, index) => (
|
||||
<VideoListItem
|
||||
video={v}
|
||||
index={index + 1}
|
||||
@ -327,8 +351,19 @@ export default function LiveIndex() {
|
||||
// })
|
||||
}}
|
||||
onRemove={() => processDeleteVideo([v.id])}
|
||||
editable={!editable && state.playId != v.id}
|
||||
editable={editable && state.playId != v.id}
|
||||
sortable={editable && state.playId != v.id}
|
||||
additionOperationBefore={<>
|
||||
{editable && state.playId != v.id && <Popconfirm
|
||||
rootClassName={'popconfirm-main'}
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<ModalWarningIcon/>}
|
||||
title={<ModalWarningTitle />}
|
||||
description={t('video.live_rollback_confirm_title')}
|
||||
onConfirm={() => handleRollback(v)}
|
||||
><button className="hover:text-blue-500"><IconRollbackCircle /></button></Popconfirm>}
|
||||
</>}
|
||||
/>))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
@ -356,6 +391,5 @@ export default function LiveIndex() {
|
||||
<IconDelete/>
|
||||
</ButtonBatch>}
|
||||
</div>
|
||||
{contextHolder}
|
||||
</div>)
|
||||
}
|
@ -1,3 +1,26 @@
|
||||
.videoListContainer{
|
||||
|
||||
}
|
||||
@mixin btnDefault{
|
||||
border-radius: 20px;
|
||||
padding: 2px 16px;
|
||||
height: auto;
|
||||
}
|
||||
.btn{
|
||||
@include btnDefault;
|
||||
background: #4096FF;
|
||||
color:#fff;
|
||||
border: 1px solid #4096FF;
|
||||
&:hover{
|
||||
background: #337acc;
|
||||
}
|
||||
}
|
||||
|
||||
.btnDefault{
|
||||
@include btnDefault;
|
||||
color:#00000099;
|
||||
border: 1px solid #00000099;
|
||||
&:hover{
|
||||
background: #00000011;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import {IconDelete, IconWarningCircle} from "@/components/icons";
|
||||
import {deleteByIds} from "@/service/api/article.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {divide} from "lodash";
|
||||
import ModalWarning from "@/components/icons/ModalWarning.tsx";
|
||||
|
||||
export default function ButtonDeleteBatch(props: { ids: Id[];onSuccess?: () => void; }) {
|
||||
const {t} = useTranslation()
|
||||
@ -29,9 +30,12 @@ export default function ButtonDeleteBatch(props: { ids: Id[];onSuccess?: () => v
|
||||
}
|
||||
modal.confirm({
|
||||
wrapClassName:'root-modal-confirm',
|
||||
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
|
||||
title: t(props.ids.length == 1 ?'news.delete_confirm':'news.delete_confirm_count',{count:props.ids.length}),
|
||||
content: <span dangerouslySetInnerHTML={{__html:props.ids.length == 1 ?t('news.delete_description') :t('news.delete_description_count')}}></span>,
|
||||
icon: <ModalWarning.Icon />,
|
||||
title: <ModalWarning.Title />,
|
||||
content: <div>
|
||||
<div>{t(props.ids.length == 1 ?'news.delete_confirm':'news.delete_confirm_count',{count:props.ids.length})}</div>
|
||||
<div><span dangerouslySetInnerHTML={{__html:props.ids.length == 1 ?t('news.delete_description') :t('news.delete_description_count')}}></span></div>
|
||||
</div>,
|
||||
onOk: handlePush,
|
||||
centered: true
|
||||
})
|
||||
|
@ -4,9 +4,10 @@ import {useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useSetState} from "ahooks";
|
||||
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {showToast} from "@/components/message.ts";
|
||||
import {push2video} from "@/service/api/article.ts";
|
||||
import {IconArrowRight, IconWarningCircle} from "@/components/icons";
|
||||
import {IconArrowRight} from "@/components/icons";
|
||||
import ModalWarning from "@/components/icons/ModalWarning.tsx";
|
||||
|
||||
export enum ProcessResult {
|
||||
All,
|
||||
@ -21,6 +22,7 @@ type PushVideoProps = {
|
||||
}
|
||||
export default function ButtonPush2Video(props: PushVideoProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
// const {modal} = App.useApp()
|
||||
const [state, setState] = useSetState<{
|
||||
modalVisible?: boolean;
|
||||
errorTitle?: string[];
|
||||
@ -30,13 +32,47 @@ export default function ButtonPush2Video(props: PushVideoProps) {
|
||||
})
|
||||
const {t} = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const handlePush = (action: ProcessResult) => {
|
||||
/**
|
||||
*
|
||||
* @deprecated 保存即判断,此时暂不提示了
|
||||
*/
|
||||
// const checkHotNewsValid = async ()=>{
|
||||
// return new Promise<string>((resolve)=>{
|
||||
// const manualErrorCount = props.articles?.filter(s=>{
|
||||
// return s.hot_news.replace(/,/ig,'').trim().length == 0
|
||||
// })?.length || 0
|
||||
// if(manualErrorCount == 0) {
|
||||
// resolve('default')
|
||||
// return;
|
||||
// }
|
||||
// modal.confirm({
|
||||
// wrapClassName: 'root-modal-confirm',
|
||||
// icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
|
||||
// title: t('modal.push_article.empty_notice_title'),
|
||||
// content: t('modal.push_article.empty_notice_message'),
|
||||
// centered:true,
|
||||
// onOk: () => {
|
||||
// resolve('auto')
|
||||
// },
|
||||
// onCancel: () => {
|
||||
// resolve('reject')
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
const handlePush = async (action: ProcessResult) => {
|
||||
const skip = action === ProcessResult.Skip && state.errorIds.length > 0
|
||||
const ids = !skip ? props.ids : props.ids.filter(id => !state.errorIds.includes(id));
|
||||
if(skip && (state.errorIds.length == props.ids.length || ids.length == 0)){
|
||||
if (skip && (state.errorIds.length == props.ids.length || ids.length == 0)) {
|
||||
setState({modalVisible: false})
|
||||
return;
|
||||
}
|
||||
//
|
||||
// const result = await checkHotNewsValid();
|
||||
// // TODO: 有热点新闻自动?
|
||||
// if(result == 'reject'){ // 有热点新闻未填写 但点击取消并终止后续操作
|
||||
// return;
|
||||
// }
|
||||
setLoading(true)
|
||||
push2video(ids).then(() => {
|
||||
setState({modalVisible: false})
|
||||
@ -49,7 +85,7 @@ export default function ButtonPush2Video(props: PushVideoProps) {
|
||||
state: 'push-success'
|
||||
})
|
||||
// props.onSuccess?.()
|
||||
}).catch(()=>{
|
||||
}).catch(() => {
|
||||
showToast(t('service_error'), 'error')
|
||||
//showErrorToast
|
||||
}).finally(() => {
|
||||
@ -98,8 +134,7 @@ export default function ButtonPush2Video(props: PushVideoProps) {
|
||||
width={440}
|
||||
>
|
||||
<div className="modal-title flex items-center">
|
||||
<div className="anticon anticon-exclamation-circle text-red-400 w-10"><IconWarningCircle
|
||||
style={{fontSize: 24, color: 'rgba(250, 173, 20, 1)'}}/></div>
|
||||
<div className="anticon anticon-exclamation-circle text-red-400 w-10"><ModalWarning.Icon/></div>
|
||||
<div className="text-base">{t('modal.warning')}</div>
|
||||
</div>
|
||||
<div className="confirm-message-wrapper flex mt-2">
|
||||
@ -117,7 +152,7 @@ export default function ButtonPush2Video(props: PushVideoProps) {
|
||||
<div className="error-list text-red-400 mt-6 w-[350px]">
|
||||
<div className="title">{t('modal.push_article.error_title')}:</div>
|
||||
<div className="max-h-[100px] overflow-auto" style={{lineHeight: '20px'}}>
|
||||
{state.errorTitle.map(s => <div
|
||||
{state.errorTitle.map((s, idx) => <div key={idx}
|
||||
className="error-item overflow-hidden pr-1 text-nowrap overflow-ellipsis">{s}</div>)}
|
||||
</div>
|
||||
</div>}
|
||||
@ -128,10 +163,15 @@ export default function ButtonPush2Video(props: PushVideoProps) {
|
||||
<Button disabled={loading} onClick={() => {
|
||||
setState({modalVisible: false})
|
||||
}}>{t('modal.push_article.action_cancel')}</Button>
|
||||
{state.errorIds?.length > 0 && <Button disabled={loading} type="primary"
|
||||
onClick={() => handlePush(ProcessResult.Skip)}>{t('modal.push_article.action_skip')}</Button>}
|
||||
<Button disabled={loading} type={state.errorIds.length == 0 ? 'primary' : 'default'}
|
||||
onClick={() => handlePush(ProcessResult.All)} >{t('modal.push_article.action_all')}</Button>
|
||||
{state.errorIds?.length > 0 && (
|
||||
<Button
|
||||
disabled={loading} type="primary"
|
||||
onClick={() => handlePush(ProcessResult.Skip)}
|
||||
>{t('modal.push_article.action_skip')}</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={loading} type={state.errorIds.length == 0 ? 'primary' : 'default'}
|
||||
onClick={() => handlePush(ProcessResult.All)}>{t('modal.push_article.action_all')}</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Input} from "antd";
|
||||
import {useBoolean, useLocalStorageState, useSetState,useClickAway} from "ahooks";
|
||||
import {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {useLocalStorageState, useSetState, useClickAway} from "ahooks";
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {clsx} from "clsx";
|
||||
import useArticleTags from "@/hooks/useArticleTags.ts";
|
||||
|
||||
@ -12,8 +12,11 @@ import {IconPin} from "@/components/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
type SearchPanelProps = {
|
||||
rootClassName?: string;
|
||||
onSearch?: (params: ApiArticleSearchParams) => void;
|
||||
defaultParams?: Partial<ApiArticleSearchParams>;
|
||||
hideNewsSource?: boolean;
|
||||
rightRender?: React.ReactNode;
|
||||
}
|
||||
const pagination = {
|
||||
limit: 12, page: 1
|
||||
@ -23,15 +26,22 @@ const DEFAULT_STATE = {
|
||||
tag_level_2_id: -1,
|
||||
subOptions: []
|
||||
}
|
||||
export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) {
|
||||
export default function SearchPanel(
|
||||
{
|
||||
onSearch,
|
||||
defaultParams,
|
||||
hideNewsSource,
|
||||
rightRender,
|
||||
rootClassName
|
||||
}: SearchPanelProps) {
|
||||
const tags = useArticleTags();
|
||||
const {t} = useTranslation()
|
||||
const [params, setParams] = useSetState<ApiArticleSearchParams>({
|
||||
pagination,
|
||||
time_flag:1,
|
||||
time_flag: 1,
|
||||
...(defaultParams || {})
|
||||
});
|
||||
const [prevSearchName, setPrevSearchName] = useState<string>(defaultParams?.title||'')
|
||||
const [prevSearchName, setPrevSearchName] = useState<string>(defaultParams?.title || '')
|
||||
|
||||
const [state, setState] = useSetState<{
|
||||
tag_level_1_id: number;
|
||||
@ -39,11 +49,11 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
subOptions: (string | number)[]
|
||||
}>({
|
||||
...DEFAULT_STATE,
|
||||
...(defaultParams&&defaultParams.tag_level_1_id?{tag_level_1_id:defaultParams.tag_level_1_id}: {}),
|
||||
...(defaultParams&&defaultParams.tag_level_2_id?{tag_level_2_id:defaultParams.tag_level_2_id}: {})
|
||||
...(defaultParams && defaultParams.tag_level_1_id ? {tag_level_1_id: defaultParams.tag_level_1_id} : {}),
|
||||
...(defaultParams && defaultParams.tag_level_2_id ? {tag_level_2_id: defaultParams.tag_level_2_id} : {})
|
||||
})
|
||||
useEffect(()=>{
|
||||
if(!defaultParams){
|
||||
useEffect(() => {
|
||||
if (!defaultParams) {
|
||||
return;
|
||||
}
|
||||
const _state = {
|
||||
@ -51,18 +61,18 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
tag_level_2_id: -1,
|
||||
}
|
||||
|
||||
if(defaultParams.tag_level_1_id){
|
||||
if (defaultParams.tag_level_1_id) {
|
||||
_state.tag_level_1_id = defaultParams.tag_level_1_id
|
||||
if(tags && tags.length > 0){
|
||||
if (tags && tags.length > 0) {
|
||||
const tag = tags.find(s => s.value == defaultParams.tag_level_1_id)
|
||||
setSubOptions(tag?.children || [])
|
||||
}
|
||||
}
|
||||
if(defaultParams.tag_level_2_id){
|
||||
if (defaultParams.tag_level_2_id) {
|
||||
_state.tag_level_2_id = defaultParams.tag_level_2_id
|
||||
}
|
||||
setState(_state)
|
||||
},[tags])
|
||||
}, [tags])
|
||||
const [pinnedTag, setPinnedTag] = useLocalStorageState<number[]>(
|
||||
'user-pinned-tag-list',
|
||||
{
|
||||
@ -117,31 +127,31 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
}
|
||||
return [] as OptionItem[];
|
||||
}, [pinnedTag, tags])
|
||||
const pinnedManagePanel = useRef<HTMLDivElement|null>(null)
|
||||
const pinnedManagePanel = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const togglePinnedManagePanel = useCallback((visible: boolean) => {
|
||||
if(!pinnedManagePanel.current){
|
||||
if (!pinnedManagePanel.current) {
|
||||
return;
|
||||
}
|
||||
const _target = pinnedManagePanel.current!;
|
||||
if(visible){
|
||||
if (visible) {
|
||||
_target.style.height = 'auto'
|
||||
const {height} = _target.getBoundingClientRect()
|
||||
_target.style.height = '38px'
|
||||
requestAnimationFrame(()=>{
|
||||
requestAnimationFrame(() => {
|
||||
_target.style.height = `${height}px`
|
||||
})
|
||||
}else{
|
||||
requestAnimationFrame(()=>{
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
_target.style.height = '0'
|
||||
})
|
||||
}
|
||||
},[pinnedManagePanel])
|
||||
const setTrue = ()=> togglePinnedManagePanel(true)
|
||||
const setFalse = ()=>togglePinnedManagePanel(false)
|
||||
}, [pinnedManagePanel])
|
||||
const setTrue = () => togglePinnedManagePanel(true)
|
||||
const setFalse = () => togglePinnedManagePanel(false)
|
||||
useClickAway(() => setFalse(), pinnedManagePanel)
|
||||
|
||||
return (<div className={`${styles.searchPanel} pt-6 pb-2`}>
|
||||
return (<div className={`${styles.searchPanel} ${rootClassName??'pt-6 pb-2'}`}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="search-form flex items-center gap-4">
|
||||
<Input
|
||||
@ -155,12 +165,13 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
/>
|
||||
<TimeSelect
|
||||
className="w-[140px] ml-1"
|
||||
value={typeof(params.time_flag) != "undefined" ? params.time_flag : 1}
|
||||
value={typeof (params.time_flag) != "undefined" ? params.time_flag : 1}
|
||||
onChange={handleTimeFilter}
|
||||
/>
|
||||
</div>
|
||||
{rightRender && <div className="right-placeholder">{rightRender}</div>}
|
||||
</div>
|
||||
<div className="filter-container mt-5">
|
||||
{!hideNewsSource && <div className="filter-container mt-5">
|
||||
<div className="list-container relative">
|
||||
<div className="justify-between flex items-start border-b pb-2 overflow-hidden">
|
||||
<div className="pinned-tag-list flex flex-wrap flex-1 min-w-0">
|
||||
@ -182,7 +193,7 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
)}
|
||||
</div>
|
||||
<div className="pinned-menu mt-2">
|
||||
<span className={'cursor-pointer block hover:text-blue-500'} onClick={e=>{
|
||||
<span className={'cursor-pointer block hover:text-blue-500'} onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setTrue();
|
||||
@ -206,7 +217,7 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
tags.filter(s => s.value !== 999999).map(it => {
|
||||
const currentPinned = pinnedTag?.includes(Number(it.value));
|
||||
return (<div
|
||||
className={`filter-item border flex items-center px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${currentPinned?'bg-gray-100':''} hover:border-gray-400`}
|
||||
className={`filter-item border flex items-center px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${currentPinned ? 'bg-gray-100' : ''} hover:border-gray-400`}
|
||||
key={it.value}
|
||||
onClick={() => {
|
||||
const value = Number(it.value)
|
||||
@ -236,13 +247,13 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_2_id == it.value ? 'text-black' : ' text-gray-400 hover:text-gray-600'}`}
|
||||
key={it.value}
|
||||
onClick={() => {
|
||||
handleFilter({tag_level_1_id:state.tag_level_1_id,tag_level_2_id: Number(it.value)})
|
||||
handleFilter({tag_level_1_id: state.tag_level_1_id, tag_level_2_id: Number(it.value)})
|
||||
}}>{it.label}</div>)
|
||||
)
|
||||
}
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>}
|
||||
</div>)
|
||||
}
|
@ -44,7 +44,7 @@
|
||||
}
|
||||
}
|
||||
.col{
|
||||
@apply flex items-center justify-center relative pl-6;
|
||||
@apply flex items-center justify-center relative pl-6 text-sm;
|
||||
height: 54px;
|
||||
&:after{
|
||||
@apply absolute;
|
||||
@ -55,6 +55,9 @@
|
||||
left:0;
|
||||
}
|
||||
}
|
||||
.cover{
|
||||
@apply pl-2;
|
||||
}
|
||||
.title{
|
||||
@apply flex-1 pl-0;
|
||||
&:after{
|
||||
@ -82,9 +85,8 @@
|
||||
.header{
|
||||
@apply bg-primary-bg;
|
||||
.col{
|
||||
@apply text-sm;
|
||||
@apply text-base;
|
||||
height: 42px;
|
||||
|
||||
}
|
||||
.operations{
|
||||
}
|
||||
@ -97,3 +99,33 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.orderDataList{
|
||||
|
||||
:global {
|
||||
.title{
|
||||
text-align: center;
|
||||
}
|
||||
.id{
|
||||
@apply pl-0;
|
||||
width: 140px;
|
||||
line-height: 1.2em;
|
||||
&:after{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.cover{
|
||||
width: 140px;
|
||||
//img{
|
||||
// max-width: 100px;
|
||||
// max-height: 56px;
|
||||
//}
|
||||
}
|
||||
.title {
|
||||
@apply flex-1 pl-4;
|
||||
min-width: 100px;
|
||||
&:after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import {Checkbox, Popconfirm, Space} from "antd";
|
||||
import {Checkbox, Space} from "antd";
|
||||
|
||||
import React, {useRef, useState} from "react";
|
||||
import {useRequest} from "ahooks";
|
||||
@ -10,12 +10,13 @@ import ButtonPush2Video, {ProcessResult} from "@/pages/news/components/button-pu
|
||||
|
||||
import styles from './components/style.module.scss'
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import {IconDelete, IconEdit, IconWarningCircle} from "@/components/icons";
|
||||
import {IconDelete, IconEdit} from "@/components/icons";
|
||||
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";
|
||||
import {DeleteItemPopoverConfirm} from "@/components/message/confirm.tsx";
|
||||
|
||||
const FilterCache: Partial<ApiArticleSearchParams> = {
|
||||
tags: [],
|
||||
@ -39,6 +40,8 @@ export default function NewEdit() {
|
||||
onSuccess: (data) => {
|
||||
FilterCache.title = params.title;
|
||||
FilterCache.tags = params.tags;
|
||||
setSelectedRowKeys(()=>([]))
|
||||
setState({checkAll: false})
|
||||
setData(prev => {
|
||||
// 判断页码是否是第1页
|
||||
if (data.pagination.page == 1) return data;
|
||||
@ -99,10 +102,12 @@ export default function NewEdit() {
|
||||
<span className={'inline-block cursor-pointer mr-2'} onClick={() => {
|
||||
handleCheckAll(!state.checkAll)
|
||||
}}>{t('select.select_all')}</span>
|
||||
<Checkbox checked={state.checkAll && (!data?.list || selectedRowKeys.length == data?.list?.length)}
|
||||
<Checkbox
|
||||
checked={state.checkAll && (!data?.list || selectedRowKeys.length == data?.list?.length)}
|
||||
onChange={e => {
|
||||
handleCheckAll(e.target.checked)
|
||||
}}/>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.newListTable}>
|
||||
@ -119,15 +124,15 @@ export default function NewEdit() {
|
||||
...prev,
|
||||
pagination: {page, limit: 10}
|
||||
}))
|
||||
}} onScroll={(top) => setState({showToTop: top > 30})} loading={loading}
|
||||
}} onScroll={(top) => setState(s=>({...s,showToTop: top > 30}))} loading={loading}
|
||||
pagination={data?.pagination}>
|
||||
<div className="body">
|
||||
{data?.list?.map((item, i) => {
|
||||
const checked = selectedRowKeys.includes(item.id)
|
||||
return <div key={i} className={clsx("row flex", {checked})}>
|
||||
return <div key={item.id} className={clsx("row flex", {checked})}>
|
||||
<div className="col title cursor-pointer" onClick={() => setEditId(item.id)}>
|
||||
<div className="flex-1">
|
||||
<div className="text-base">{item.title}</div>
|
||||
<div className="text-base line-clamp-1">{item.title}</div>
|
||||
<div
|
||||
className="summary text-xs text-gray-400 line-clamp-1">{item.summary}</div>
|
||||
</div>
|
||||
@ -147,19 +152,9 @@ export default function NewEdit() {
|
||||
</div>
|
||||
<div className="col operations">
|
||||
<span className="icon-btn" onClick={()=>setEditId(item.id)}><IconEdit/></span>
|
||||
<Popconfirm
|
||||
rootClassName={'popconfirm-main'}
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={t('news.delete_confirm')}
|
||||
description={<span dangerouslySetInnerHTML={{__html:t('news.delete_description')}}></span>}
|
||||
onConfirm={() => {
|
||||
handleDelete(item.id)
|
||||
}}
|
||||
>
|
||||
<DeleteItemPopoverConfirm onConfirm={() => {handleDelete(item.id)}}>
|
||||
<span className="icon-btn"><IconDelete/></span>
|
||||
</Popconfirm>
|
||||
</DeleteItemPopoverConfirm>
|
||||
<Checkbox checked={checked}
|
||||
onChange={e => handleItemChecked(e.target.checked, item)}/>
|
||||
</div>
|
||||
@ -179,6 +174,7 @@ export default function NewEdit() {
|
||||
<ArticleEditModal
|
||||
type="news"
|
||||
id={editId}
|
||||
onRefresh={refresh}
|
||||
onClose={(saved) => {
|
||||
setEditId(-1)
|
||||
if (saved) refresh()
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, {useMemo, useRef, useState} from "react";
|
||||
import {Checkbox, Divider, Empty, Modal, Space} from "antd";
|
||||
import {useRequest} from "ahooks";
|
||||
import { useRequest, useSetState } from 'ahooks';
|
||||
import {CloseOutlined} from "@ant-design/icons"
|
||||
import {clsx} from "clsx";
|
||||
|
||||
@ -30,7 +30,7 @@ export default function NewsIndex() {
|
||||
|
||||
const [activeNews, setActiveNews] = useState<NewsInfo>()
|
||||
|
||||
const [state, setState] = useState<{
|
||||
const [state, setState] = useSetState<{
|
||||
checkAll?: boolean;
|
||||
showToTop?: boolean;
|
||||
}>({})
|
||||
@ -43,10 +43,10 @@ export default function NewsIndex() {
|
||||
FilterCache.tag_level_2_id = params.tag_level_2_id;
|
||||
FilterCache.title = params.title;
|
||||
FilterCache.time_flag = params.time_flag;
|
||||
console.log('success',FilterCache)
|
||||
if (params.pagination.page === 1) {
|
||||
setCheckedId([])
|
||||
setData(_data)
|
||||
setState({checkAll: checkedId && _data.list && checkedId.length === _data.list.length})
|
||||
setState({checkAll: false,showToTop: false})
|
||||
} else {
|
||||
setData({
|
||||
pagination: _data.pagination,
|
||||
@ -89,7 +89,11 @@ export default function NewsIndex() {
|
||||
}
|
||||
}
|
||||
return (<div className={'container pb-5'}>
|
||||
<SearchPanel defaultParams={params} onSearch={setParams}/>
|
||||
<SearchPanel defaultParams={params} onSearch={(params)=>{
|
||||
// 滚动到顶部
|
||||
scrollerRef.current?.scrollToPosition(0)
|
||||
setParams(params)
|
||||
}}/>
|
||||
{activeNews && <Modal
|
||||
rootClassName={'news-detail-modal'}
|
||||
closeIcon={null} open={true} width={1000}
|
||||
|
104
src/pages/order/index.tsx
Normal file
104
src/pages/order/index.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Empty, Pagination} from "antd";
|
||||
import {useRequest} from "ahooks";
|
||||
|
||||
import SearchPanel from "@/pages/news/components/search-panel.tsx";
|
||||
import styles from "@/pages/news/components/style.module.scss";
|
||||
import {formatDurationToTime, formatTime} from "@/util/strings.ts";
|
||||
import {getList} from "@/service/api/order.ts";
|
||||
|
||||
import ImageErr from "@/assets/images/error/ic_broken_image.png"
|
||||
|
||||
function OrderIndex() {
|
||||
const {t} = useTranslation()
|
||||
const [params, setParams] = useState<ApiArticleSearchParams>({
|
||||
pagination: {page: 1, limit: 10},
|
||||
time_flag: 0,
|
||||
})
|
||||
const {data} = useRequest(() => getList(params), {
|
||||
refreshDeps: [params],
|
||||
})
|
||||
|
||||
return <div className="pb-5 page-order-index">
|
||||
<div className=" mb-5" style={{backgroundColor:'#dae8fc'}}>
|
||||
<div className="container" style={{padding:0}}>
|
||||
<SearchPanel
|
||||
rootClassName="py-6"
|
||||
hideNewsSource={true}
|
||||
defaultParams={params}
|
||||
onSearch={setParams}
|
||||
rightRender={<div>{t('order.left_time')}: <span
|
||||
className={`${!data?.remaining_duration || Number(data?.remaining_duration) < 3600 ? 'text-red-600' : ''}`}>{formatDurationToTime(data?.remaining_duration)}</span>
|
||||
</div>}
|
||||
/>
|
||||
</div></div>
|
||||
<div className="mt-5 container" style={{padding:"20px 0"}}>
|
||||
<div className={`${styles.newListTable} ${styles.orderDataList} `}>
|
||||
<div className="header row flex">
|
||||
<div className="col id w-[160px]">{t('order.list.id')}</div>
|
||||
<div className="col cover">{t('order.list.cover')}</div>
|
||||
<div className="col title w-min-60px">{t('order.list.title')}</div>
|
||||
<div className="col w-[180px]">{t('order.list.order_time')}</div>
|
||||
<div className="col w-[120px]">{t('order.list.consume_time')}</div>
|
||||
<div className="col w-[180px]">{t('order.list.operator')}</div>
|
||||
</div>
|
||||
<div>
|
||||
{data?.list.length === 0 && <div style={{marginTop: 50}}>
|
||||
<Empty/>
|
||||
</div>}
|
||||
{data?.list.map((item, i) => {
|
||||
return <div key={i} className="row flex">
|
||||
<div className="col id w-[160px] text-center">
|
||||
<div className="flex-1">
|
||||
<div className="break-all">{item.order_id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col cover">
|
||||
<div
|
||||
className="w-[100px] h-[56px] flex items-center rounded overflow-hidden border border-gray-50"
|
||||
>
|
||||
<img
|
||||
src={item.img_url || ImageErr}
|
||||
className="w-[100px] object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col title order-title flex-1 w-min-60px">
|
||||
<div className="line-clamp-2">{item.title}</div>
|
||||
</div>
|
||||
<div className="col w-[180px]">{formatTime(item.order_time, 'YYYY-MM-DD HH:mm')}
|
||||
</div>
|
||||
<div className="col w-[120px]">{formatDurationToTime(item.consumption_duration)}
|
||||
</div>
|
||||
<div className="col w-[180px]">{item.operator}</div>
|
||||
</div>
|
||||
})}
|
||||
|
||||
<div className="footer flex justify-end mt-10">
|
||||
<Pagination
|
||||
onChange={(page, limit) => {
|
||||
setParams({
|
||||
...params,
|
||||
pagination: {page, limit}
|
||||
})
|
||||
}}
|
||||
total={data?.pagination.total || 0}
|
||||
showTotal={(total) => <div>{t('page.total_item', {total})}</div>}
|
||||
showSizeChanger={{
|
||||
options: [
|
||||
{value: 10, label: t('page.size_10')},
|
||||
{value: 20, label: t('page.size_20')},
|
||||
{value: 30, label: t('page.size_30')}
|
||||
]
|
||||
}}
|
||||
showQuickJumper={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default OrderIndex
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
@ -4,6 +4,9 @@
|
||||
:global {
|
||||
.video-bottom {
|
||||
}
|
||||
.video-time-info{
|
||||
min-width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import styles from './style.module.scss'
|
||||
type VideoItemProps = {
|
||||
videoInfo: VideoInfo;
|
||||
onLive?: boolean;
|
||||
onClick?: (autoPlay:boolean) => void;
|
||||
onClick?: (autoPlay: boolean) => void;
|
||||
onRemove?: () => void;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
checked?: boolean;
|
||||
@ -25,14 +25,14 @@ export default function VideoItem(props: VideoItemProps) {
|
||||
<div className="cover">
|
||||
<img className={'w-full cursor-pointer object-cover'} src={props.videoInfo.cover}/>
|
||||
<div className={'absolute inset-x-0 top-0 flex items-center justify-center bottom-[36px]'}>
|
||||
<div className={styles.playIcon} onClick={()=>props.onClick?.(true)}><CaretRightOutlined /></div>
|
||||
<div className={styles.playIcon} onClick={() => props.onClick?.(true)}><CaretRightOutlined/></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="video-bottom bg-black/30 backdrop-blur-[2px] text-sm absolute inset-x-0 bottom-0 text-white py-2 px-3 items-center flex justify-between">
|
||||
<div className="title cursor-pointer flex-1 text-nowrap overflow-hidden text-ellipsis min-w-0 mr-4"
|
||||
onClick={()=>props.onClick?.(false)}>{props.videoInfo.title}</div>
|
||||
<div className="video-time-info">{timeFromNow(props.videoInfo.ctime)}</div>
|
||||
onClick={() => props.onClick?.(false)}>{props.videoInfo.title}</div>
|
||||
<div className="video-time-info text-right">{timeFromNow(props.videoInfo.d_time)}</div>
|
||||
</div>
|
||||
<div
|
||||
className={"absolute top-1 left-1 bg-black/50 rounded-3xl text-white px-3 py-0.5"}>{Math.ceil(props.videoInfo.duration / 1000)}s
|
@ -1,25 +1,25 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {Checkbox, Modal, Space} from "antd";
|
||||
import {Checkbox, Empty, Modal, Space} from "antd";
|
||||
import {useRequest, useSetState} from "ahooks";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
import VideoItem from "@/pages/library/components/video-item.tsx";
|
||||
import SearchForm from "@/pages/library/components/search-form.tsx";
|
||||
import VideoDetail from "@/pages/library/components/video-detail.tsx";
|
||||
import {deleteHistories, push2room, search} from "@/service/api/video.ts";
|
||||
import {getList} from "@/service/api/live.ts";
|
||||
import VideoItem from "@/pages/recycle/components/video-item.tsx";
|
||||
import SearchForm from "@/pages/recycle/components/search-form.tsx";
|
||||
import VideoDetail from "@/pages/recycle/components/video-detail.tsx";
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
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";
|
||||
import {showToast} from "@/components/message.ts";
|
||||
import {BizError} from "@/service/types.ts";
|
||||
import {getList as getLiveList} from "@/service/api/live.ts";
|
||||
import {getList, remove, restore} from "@/service/api/recycle.ts";
|
||||
|
||||
const DEFAULT_PAGE_LIMIT = {
|
||||
page: 1,
|
||||
limit: 12
|
||||
}
|
||||
export default function LibraryIndex() {
|
||||
export default function RecycleIndex() {
|
||||
const {t} = useTranslation()
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
@ -31,20 +31,27 @@ export default function LibraryIndex() {
|
||||
checkedAll: false,
|
||||
loading: false,
|
||||
pushedCount: 0,
|
||||
pushedList: [-1],
|
||||
showToTop: false
|
||||
})
|
||||
const [data, setData] = useState<DataList<VideoInfo>>()
|
||||
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
|
||||
|
||||
const {loading} = useRequest(() => search(params), {
|
||||
const {loading} = useRequest(() => getList(params), {
|
||||
refreshDeps: [params],
|
||||
onSuccess: (data) => {
|
||||
setData(prev => {
|
||||
// 判断页码是否是第1页
|
||||
if (data.pagination.page == 1) return data;
|
||||
if (data.pagination.page == 1) {
|
||||
setCheckedIdArray([])
|
||||
return data;
|
||||
}
|
||||
return {
|
||||
list: [...(prev?.list || []), ...(data?.list || [])],
|
||||
pagination: data.pagination
|
||||
pagination: data.pagination || {
|
||||
page: 1,
|
||||
limit: DEFAULT_PAGE_LIMIT.limit
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -74,23 +81,27 @@ export default function LibraryIndex() {
|
||||
autoPlay: boolean
|
||||
}>()
|
||||
const handleAllCheckedChange = (checked: boolean) => {
|
||||
if (!data) return;
|
||||
if (!data || data.pagination.total == 0) return;
|
||||
setCheckedIdArray(checked ? data.list.map(v => v.id) : [])
|
||||
setState({
|
||||
checkedAll: !state.checkedAll
|
||||
})
|
||||
}
|
||||
const loadPushedState = () => {
|
||||
getList().then((ret) => {
|
||||
getLiveList().then((ret) => {
|
||||
if (ret.list) {
|
||||
setState({pushedCount: ret.list.length})
|
||||
setState({pushedCount: ret.list.length, pushedList: ret.list.map(s => s.id)})
|
||||
}
|
||||
})
|
||||
}
|
||||
const refresh = () => {
|
||||
loadPushedState();
|
||||
// loadPushedState();
|
||||
setParams(prev => ({...prev, pagination: {page: 1, limit: DEFAULT_PAGE_LIMIT.limit}, request_time: Date.now()}))
|
||||
}
|
||||
// const pusdedCount = useMemo(() => {
|
||||
// if (state.pushedCount == 0 || !data || !data.list || data.list.length == 0) return 0;
|
||||
// return data.list.filter(s => state.pushedList.includes(s.id)).length
|
||||
// }, [state.pushedList, state.pushedCount, data])
|
||||
|
||||
useEffect(loadPushedState, [])
|
||||
|
||||
@ -99,7 +110,12 @@ export default function LibraryIndex() {
|
||||
{contextHolder}
|
||||
<div className="search-form-container">
|
||||
<SearchForm
|
||||
onSearch={setParams}
|
||||
onSearch={(params) => {
|
||||
setParams({
|
||||
...params,
|
||||
pagination: {...DEFAULT_PAGE_LIMIT}
|
||||
})
|
||||
}}
|
||||
onBtnStartClick={handleLive}
|
||||
loading={loading}
|
||||
/>
|
||||
@ -109,16 +125,18 @@ export default function LibraryIndex() {
|
||||
<div className="pl-[70px]"></div>
|
||||
<div className="flex items-center">
|
||||
<Space className="text-gray-400" size={20}>
|
||||
<span>{t('select.total',{count:data?.list?.length || 0})}</span>
|
||||
<span>{t('history.pushed',{count:state.pushedCount})}</span>
|
||||
<span className={'text-blue-500'}>{t('select.selected_some',{count:checkedIdArray.length})}</span>
|
||||
<span>{t('select.total', {count: data?.list?.length || 0})}</span>
|
||||
{/*<span>{t('history.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-4"
|
||||
onClick={() => handleAllCheckedChange(checkedIdArray.length != data?.list.length)}>
|
||||
<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}
|
||||
<Checkbox
|
||||
disabled={data?.pagination.total == 0 || data?.list?.length == 0}
|
||||
checked={checkedIdArray.length == data?.list?.length}
|
||||
onChange={e => handleAllCheckedChange(e.target.checked)}/>
|
||||
</div>
|
||||
</div>
|
||||
@ -131,6 +149,9 @@ export default function LibraryIndex() {
|
||||
}))
|
||||
}} onScroll={(top) => setState({showToTop: top > 30})}
|
||||
>
|
||||
{data?.pagination.total == 0 && !loading && <div className="mt-20">
|
||||
<Empty/>
|
||||
</div>}
|
||||
<div className={'video-list-container grid gap-4 grid-cols-3 xl:grid-cols-4'}>
|
||||
{data?.list?.map((it, idx) => (
|
||||
<VideoItem
|
||||
@ -163,28 +184,33 @@ export default function LibraryIndex() {
|
||||
icon={<IconDelete className=""/>}
|
||||
title={
|
||||
checkedIdArray.length == 1
|
||||
? t('video.delete_description',{count:checkedIdArray.length})
|
||||
: t('video.delete_description_count',{count:checkedIdArray.length})
|
||||
? t('video.delete_description', {count: checkedIdArray.length})
|
||||
: t('video.delete_description_count', {count: checkedIdArray.length})
|
||||
}
|
||||
emptyMessage={t('video.delete_empty')}
|
||||
confirmMessage={<span dangerouslySetInnerHTML={{
|
||||
__html: checkedIdArray.length == 1
|
||||
? t('video.delete_confirm')
|
||||
: t('video.delete_confirm_count',{count:checkedIdArray.length})
|
||||
? t('video.delete_forever_confirm')
|
||||
: t('video.delete_forever_confirm_count', {count: checkedIdArray.length})
|
||||
}}></span>}
|
||||
onProcess={deleteHistories}
|
||||
>{t('delete_batch')}</ButtonBatch>}
|
||||
onProcess={remove}
|
||||
>{t('recycle.remove_forever')}</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}
|
||||
onProcess={restore}
|
||||
confirmMessage={<span dangerouslySetInnerHTML={{
|
||||
__html: checkedIdArray.length == 1
|
||||
? t('video.restore_confirm')
|
||||
: t('video.restore_confirm_count', {count: checkedIdArray.length})
|
||||
}}></span>}
|
||||
emptyMessage={t('video.push_empty')}
|
||||
onError={e=>{
|
||||
showToast(String((e as BizError).data || e.message),'error')
|
||||
onError={e => {
|
||||
showToast(String((e as BizError).data || e.message), 'error')
|
||||
}}
|
||||
>{t('video.push_to_live')}</ButtonBatch>}
|
||||
>{t('recycle.restore_video')}</ButtonBatch>}
|
||||
</div>
|
||||
</>)
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
import {Button, Modal} from "antd";
|
||||
import {Modal} from "antd";
|
||||
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 {IconArrowRight} from "@/components/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import ModalWarning from "@/components/icons/ModalWarning.tsx";
|
||||
|
||||
|
||||
export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];onSuccess?:()=>void; }) {
|
||||
export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[]; onSuccess?: () => void; }) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const {t} = useTranslation()
|
||||
const handlePush = () => {
|
||||
@ -15,9 +16,9 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on
|
||||
const vids = props.list.filter(v => v.status == VideoStatus.Generated && props.ids.includes(v.id)).map(v => v.id)
|
||||
push2room(vids).then(() => {
|
||||
props.onSuccess?.()
|
||||
if(props.ids.length == vids.length){
|
||||
if (props.ids.length == vids.length) {
|
||||
showToast(t("video.push_success"), 'success')
|
||||
}else{
|
||||
} else {
|
||||
showToast(t("video.push_failed"), 'success')
|
||||
}
|
||||
}).catch(showErrorToast).finally(() => {
|
||||
@ -25,17 +26,18 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on
|
||||
})
|
||||
}
|
||||
const onPushClick = () => {
|
||||
if(loading) return;
|
||||
if (loading) return;
|
||||
if (props.ids.length === 0) {
|
||||
showToast(t("video.push_empty"), 'warning')
|
||||
return
|
||||
}
|
||||
Modal.confirm({
|
||||
wrapClassName:'root-modal-confirm',
|
||||
title: '操作提示',
|
||||
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
|
||||
title: <ModalWarning.Title/>,
|
||||
icon: <ModalWarning.Icon/>,
|
||||
content: t("video.push_confirm"),
|
||||
onOk: handlePush
|
||||
onOk: handlePush,
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
return (
|
||||
@ -47,7 +49,7 @@ export default function ButtonPush2Room(props: { ids: Id[]; list: VideoInfo[];on
|
||||
onClick={onPushClick}
|
||||
>
|
||||
<span className={'text'}>{t("video.push_to_live")}</span>
|
||||
<IconArrowRight />
|
||||
<IconArrowRight/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {Checkbox, Empty, Space} from "antd";
|
||||
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {DndContext} from "@dnd-kit/core";
|
||||
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
|
||||
import {useSetState} from "ahooks";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
||||
@ -10,13 +12,11 @@ import {deleteFromList, getList, modifyOrder, regenerateById, VideoStatus} from
|
||||
import {formatDuration} from "@/util/strings.ts";
|
||||
import ButtonBatch from "@/components/button-batch.tsx";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
import {Player, PlayerInstance} from "@/components/video/player.tsx";
|
||||
import {Mp4Player as Player, PlayerInstance} from "@/components/video/Mp4Player.tsx";
|
||||
import ButtonPush2Room from "@/pages/video/components/button-push2room.tsx";
|
||||
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, useNavigate} from "react-router-dom";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
export default function VideoIndex() {
|
||||
const {t} = useTranslation()
|
||||
@ -29,6 +29,7 @@ export default function VideoIndex() {
|
||||
const [state, setState] = useSetState({
|
||||
checkedAll: false,
|
||||
playingId: -1,
|
||||
videoPlaying: false,
|
||||
showToTop: false,
|
||||
showStatePos: false,
|
||||
playState: {
|
||||
@ -76,7 +77,7 @@ export default function VideoIndex() {
|
||||
|
||||
// 播放视频
|
||||
const playVideo = (video: VideoInfo) => {
|
||||
console.log('play video',video)
|
||||
|
||||
if (state.playingId == video.id) {
|
||||
player.current?.pause();
|
||||
setState({playingId: -1})
|
||||
@ -86,9 +87,9 @@ export default function VideoIndex() {
|
||||
// setState({playingIndex})
|
||||
// player.current?.play('https://staticplus.gachafun.com/ai-collect/composite_video/2024-12-17/1186196465916190720.flv', 30)
|
||||
//
|
||||
if (video.oss_video_url && video.status !== 1) {
|
||||
if (video.oss_video_mp4_url && video.status !== 1) {
|
||||
setState({playingId: video.id})
|
||||
player.current?.play(video.oss_video_url, 0)
|
||||
player.current?.play(video.oss_video_mp4_url, 0)
|
||||
}
|
||||
}
|
||||
// 处理全选
|
||||
@ -170,6 +171,12 @@ export default function VideoIndex() {
|
||||
onChange={(state) => {
|
||||
if (state.end || state.error) setState({playingId: -1})
|
||||
}}
|
||||
onPause={() => {
|
||||
setState({videoPlaying:false})
|
||||
}}
|
||||
onPlay={() => {
|
||||
setState({videoPlaying:true})
|
||||
}}
|
||||
onProgress={(current, duration) => {
|
||||
setState({
|
||||
playState: {
|
||||
@ -185,7 +192,7 @@ export default function VideoIndex() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="video-list-container rounded mt-2 flex flex-col flex-1">
|
||||
<div className="live-control flex justify-between">
|
||||
<div className="live-control flex justify-between h-[30px]">
|
||||
<div className="pl-[70px]"></div>
|
||||
<div className="flex items-center">
|
||||
<Space size={20}>
|
||||
@ -227,14 +234,6 @@ export default function VideoIndex() {
|
||||
handleModifySort(newSorts)
|
||||
return newSorts;
|
||||
});
|
||||
// modal.confirm({
|
||||
// title: '提示',
|
||||
// content: '是否要移动到指定位置',
|
||||
// onOk: handleModifySort,
|
||||
// onCancel: () => {
|
||||
// setVideoData(originArr);
|
||||
// }
|
||||
// })
|
||||
}
|
||||
}}>
|
||||
<SortableContext items={videoData}>
|
||||
@ -246,7 +245,7 @@ export default function VideoIndex() {
|
||||
key={index}
|
||||
type={'create'}
|
||||
active={checkedIdArray.includes(v.id)}
|
||||
playing={state.playingId == v.id}
|
||||
playing={state.playingId == v.id && state.videoPlaying}
|
||||
checked={checkedIdArray.includes(v.id)}
|
||||
className={`list-item-${index} mt-3 mb-2 list-item-state-${v.status} `}
|
||||
onCheckedChange={(checked) => {
|
||||
@ -262,8 +261,9 @@ export default function VideoIndex() {
|
||||
setEditId(v.article_id)
|
||||
}:undefined}
|
||||
onRegenerate={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated?()=>{
|
||||
processGenerateVideo(v)
|
||||
processGenerateVideo(v).catch(console.log)
|
||||
}:undefined}
|
||||
downloadUrl={v.oss_video_mp4_url}
|
||||
hideCheckBox={v.status != VideoStatus.Generating && v.status != VideoStatus.Generated}
|
||||
editable={v.status != VideoStatus.Generating}
|
||||
sortable={v.status == VideoStatus.Generated}
|
||||
@ -278,19 +278,20 @@ export default function VideoIndex() {
|
||||
</div>
|
||||
<div className="page-action">
|
||||
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
|
||||
{checkedIdArray.length > 0 && <ButtonBatch
|
||||
{
|
||||
checkedIdArray.length > 0 && <ButtonBatch
|
||||
onProcess={deleteFromList}
|
||||
selected={checkedIdArray}
|
||||
emptyMessage={t('video.delete_empty')}
|
||||
title={
|
||||
checkedIdArray.length == 1 ? t('video.delete_description',{count:checkedIdArray.length}):
|
||||
t('video.delete_description_count',{count:checkedIdArray.length})
|
||||
checkedIdArray.length == 1 ? t('video.delete_description', {count: checkedIdArray.length}) :
|
||||
t('video.delete_description_count', {count: checkedIdArray.length})
|
||||
}
|
||||
className='bg-gray-300 hover:bg-gray-400 text-white'
|
||||
confirmMessage={<span dangerouslySetInnerHTML={{
|
||||
__html:checkedIdArray.length == 1?
|
||||
t('video.delete_confirm',{count:checkedIdArray.length}):
|
||||
t('video.delete_confirm_count',{count:checkedIdArray.length})
|
||||
__html: checkedIdArray.length == 1 ?
|
||||
t('video.delete_confirm', {count: checkedIdArray.length}) :
|
||||
t('video.delete_confirm_count', {count: checkedIdArray.length})
|
||||
}}></span>}
|
||||
onSuccess={() => {
|
||||
showToast(t('delete_success'), 'success')
|
||||
@ -299,10 +300,16 @@ export default function VideoIndex() {
|
||||
>
|
||||
<span className="text">{t('delete_batch')}</span>
|
||||
<IconDelete/>
|
||||
</ButtonBatch>}
|
||||
</ButtonBatch>
|
||||
}
|
||||
<ButtonPush2Room ids={checkedIdArray} list={videoData} onSuccess={loadList}/>
|
||||
</div>
|
||||
</div>
|
||||
<ArticleEditModal type={'video'} id={editId} onClose={() => setEditId(-1)}/>
|
||||
<ArticleEditModal type={'video'} id={editId} onClose={(saved) =>{
|
||||
setEditId(-1)
|
||||
if(saved) {
|
||||
loadList()
|
||||
}
|
||||
}}/>
|
||||
</div>)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import {createBrowserRouter, RouterProvider,} from "react-router-dom";
|
||||
import {Suspense, useEffect,} from "react";
|
||||
import {ConfigProvider, App} from "antd";
|
||||
import React, {Suspense, useEffect,} from "react";
|
||||
import {ConfigProvider, App, Modal} from "antd";
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
// for date-picker i18n
|
||||
import dayjs from "dayjs";
|
||||
@ -9,9 +9,10 @@ import ErrorBoundary from "./error.tsx";
|
||||
import Loader from "@/components/loader.tsx";
|
||||
import routes from "@/routes/routes.tsx";
|
||||
import {DocumentTitle} from "@/components/document.tsx";
|
||||
import useConfig from "@/hooks/useConfig.ts";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import useGlobalConfig from "@/hooks/useGlobalConfig.ts";
|
||||
import {getRemainingDuration} from "@/service/api/order.ts";
|
||||
import ModalWarning from "@/components/icons/ModalWarning.tsx";
|
||||
|
||||
|
||||
const router = createBrowserRouter([
|
||||
@ -33,15 +34,38 @@ const AppRouter = () => {
|
||||
const {globalConfig} = useGlobalConfig();
|
||||
const {t,i18n} = useTranslation();
|
||||
|
||||
const initRemainingDuration = () => {
|
||||
getRemainingDuration().then(remain => {
|
||||
if(remain <= 0){
|
||||
Modal.warning({
|
||||
wrapClassName:'root-modal-confirm',
|
||||
title: t('confirm.title'),
|
||||
icon: <ModalWarning.Icon/>,
|
||||
content: t("order.remaining_duration_warning"),
|
||||
okText: t('confirm.ok'),
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
console.log('remain', remain)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
if(i18n.language){
|
||||
if(i18n.language == 'multiple'){
|
||||
const lang = localStorage.getItem('ai-human-lang') || (navigator.language.toLocaleLowerCase().indexOf('cn') != -1 ? 'zh-CN' : 'en-US')
|
||||
i18n.changeLanguage(lang).catch(console.log)
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (i18n && i18n.language == 'zh-CN') {
|
||||
dayjs.locale('zh-cn');
|
||||
}else{
|
||||
dayjs.locale('en')
|
||||
}
|
||||
initRemainingDuration()
|
||||
globalConfig.i18n = i18n.language
|
||||
// i18n.changeLanguage(i18n).then(()=>console.log('change lang to ',i18n))
|
||||
}, [i18n])
|
||||
}, [i18n.language])
|
||||
|
||||
return (<ConfigProvider
|
||||
locale={i18n?.language?.toString() == 'zh-CN' ? zhCN : undefined}
|
||||
|
@ -14,7 +14,6 @@ const AuthGuard = ({ children }:BasicComponentProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized && !isLoggedIn && location.pathname !== '/user') {
|
||||
console.log(location)
|
||||
navigate(`/user?from=${location.pathname}`, {
|
||||
state: {
|
||||
from: location.pathname
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {Outlet, useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import {Button, Divider, Dropdown, MenuProps} from "antd";
|
||||
import React, {useEffect} from "react";
|
||||
import {Outlet, useLocation, useNavigate} from "react-router-dom";
|
||||
import {Divider, Dropdown, MenuProps} from "antd";
|
||||
import React, {useEffect, useMemo} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
import AuthGuard from "@/routes/layout/auth-guard.tsx";
|
||||
import {LogoText} from "@/components/icons/logo.tsx";
|
||||
@ -11,9 +12,8 @@ import {DashboardNavigation} from "@/routes/layout/dashboard-navigation.tsx";
|
||||
import useAuth from "@/hooks/useAuth.ts";
|
||||
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";
|
||||
import {IconOrderFill, IconRecycleFill} from "@/components/icons";
|
||||
import LanguageSwitcher from "@/components/icons/language-switcher.tsx";
|
||||
|
||||
|
||||
type LayoutProps = {
|
||||
@ -21,17 +21,24 @@ type LayoutProps = {
|
||||
}
|
||||
|
||||
const NavigationUserContainer = () => {
|
||||
const {t } = useTranslation()
|
||||
const {t} = useTranslation()
|
||||
const {logout, user} = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const handleLogout = ()=>{
|
||||
const handleLogout = () => {
|
||||
logout().then(() => navigate('/user'))
|
||||
}
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: 'profile',
|
||||
label: <div className="nav-item" onClick={() => navigate('/history')}>
|
||||
<IconVideo />
|
||||
key: 'order',
|
||||
label: <div className="nav-item" onClick={() => navigate('/order')}>
|
||||
<IconOrderFill/>
|
||||
<span className={"nav-text"}>{t('order.text')}</span>
|
||||
</div>,
|
||||
},
|
||||
{
|
||||
key: 'recycle',
|
||||
label: <div className="nav-item" onClick={() => navigate('/recycle')}>
|
||||
<IconRecycleFill/>
|
||||
<span className={"nav-text"}>{t('history.text')}</span>
|
||||
</div>,
|
||||
},
|
||||
@ -51,7 +58,7 @@ const NavigationUserContainer = () => {
|
||||
{user ? <Dropdown
|
||||
rootClassName={'z-[999999] userinfo-drop-menu'}
|
||||
menu={{items}} placement="bottomRight"
|
||||
dropdownRender={(menu)=>(
|
||||
dropdownRender={(menu) => (
|
||||
<div>
|
||||
<div className="user-profile flex gap-4">
|
||||
<div className="avatar"><UserAvatar className="user-avatar"/></div>
|
||||
@ -60,11 +67,11 @@ const NavigationUserContainer = () => {
|
||||
<div>ID: {user?.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Divider style={{margin: 0}}/>
|
||||
<div className="menu-list-container">
|
||||
{menu}
|
||||
</div>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Divider style={{margin: 0}}/>
|
||||
<div className="logout">
|
||||
<div onClick={handleLogout}>{t('user.logout')}</div>
|
||||
</div>
|
||||
@ -76,26 +83,29 @@ const NavigationUserContainer = () => {
|
||||
</Dropdown> : <UserButton/>}
|
||||
</div>)
|
||||
}
|
||||
const ExtraNavItems = {
|
||||
'/order':'order.text',
|
||||
'/recycle':'history.text',
|
||||
}
|
||||
export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
|
||||
const {i18n} = useTranslation();
|
||||
const [params] = useSearchParams();
|
||||
const {pathname} = useLocation()
|
||||
const {t,i18n} = useTranslation()
|
||||
const extraNav = useMemo(()=>{
|
||||
if(!pathname || !ExtraNavItems[pathname]) return null
|
||||
return t(ExtraNavItems[pathname])
|
||||
},[pathname,i18n.language])
|
||||
return (<div className={'dashboard-layout min-h-screen'}>
|
||||
<div className="min-h-screen w-full">
|
||||
<div className="app-header">
|
||||
<div className="logo-container">
|
||||
<div className="logo-container flex items-center">
|
||||
<LogoText style={{fontSize: 30}}/>
|
||||
{extraNav && <div className="extra-nav-name ml-2">
|
||||
<span className="nav-item active">{extraNav}</span>
|
||||
</div>}
|
||||
</div>
|
||||
<DashboardNavigation/>
|
||||
<div className="flex items-center">
|
||||
{(params.get('lang') == 'yes' || AppConfig.APP_LANG == 'multiple') && <div>
|
||||
{
|
||||
i18n.language == 'zh-CN'?(
|
||||
<Button className="ml-2" onClick={()=>i18n.changeLanguage('en-US')}>Change To EN</Button>
|
||||
):(
|
||||
<Button className="ml-2" onClick={()=>i18n.changeLanguage('zh-CN')}>显示中文</Button>
|
||||
)
|
||||
}
|
||||
</div>}
|
||||
<LanguageSwitcher />
|
||||
<NavigationUserContainer/>
|
||||
</div>
|
||||
</div>
|
||||
@ -112,12 +122,12 @@ export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
|
||||
const DashboardLayout: React.FC<{ children?: React.ReactNode }> = ({children}) => {
|
||||
const loc = useLocation()
|
||||
const navigate = useNavigate()
|
||||
useEffect(()=>{
|
||||
if(!defaultCache.firstLoadPath && loc.pathname == '/live'){
|
||||
useEffect(() => {
|
||||
if (!defaultCache.firstLoadPath && loc.pathname == '/live') {
|
||||
defaultCache.firstLoadPath = loc.pathname;
|
||||
navigate('/')
|
||||
}
|
||||
},[])
|
||||
}, [])
|
||||
return <AuthGuard>
|
||||
<div className="fixed">{defaultCache.firstLoadPath}</div>
|
||||
<BaseLayout>
|
||||
|
@ -1,16 +1,16 @@
|
||||
import {RouteObject} from "react-router-dom";
|
||||
import ErrorBoundary from "@/routes/error.tsx";
|
||||
|
||||
;
|
||||
import DashboardLayout from "@/routes/layout/dashboard-layout.tsx";
|
||||
import React from "react";
|
||||
|
||||
import ErrorBoundary from "@/routes/error.tsx";
|
||||
import DashboardLayout from "@/routes/layout/dashboard-layout.tsx";
|
||||
|
||||
const UserAuth = React.lazy(() => import("@/pages/user"))
|
||||
const CreateVideoIndex = React.lazy(() => import("@/pages/video"))
|
||||
const LibraryIndex = React.lazy(() => import("@/pages/library"))
|
||||
const RecycleIndex = React.lazy(() => import("../pages/recycle"))
|
||||
const LiveIndex = React.lazy(() => import("@/pages/live"))
|
||||
const NewsIndex = React.lazy(() => import("@/pages/news"))
|
||||
const NewsEdit = React.lazy(() => import("@/pages/news/edit.tsx"))
|
||||
const OrderIndex = React.lazy(() => import("@/pages/order/index.tsx"))
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
|
||||
@ -36,8 +36,12 @@ const routes: RouteObject[] = [
|
||||
element: <CreateVideoIndex/>
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
element: <LibraryIndex/>
|
||||
path: 'recycle',
|
||||
element: <RecycleIndex/>
|
||||
},
|
||||
{
|
||||
path: 'order',
|
||||
element: <OrderIndex/>
|
||||
},
|
||||
{
|
||||
path: 'live',
|
||||
|
@ -20,13 +20,8 @@ export function getById(id: Id) {
|
||||
return post<ArticleDetail>({url: '/article/detail/' + id})
|
||||
}
|
||||
|
||||
export function save(title: string, metahuman_text: string, content_group: BlockContent[][], id?: number) {
|
||||
return post<{ content: string }>(id && id > 0 ? '/article/modify' : '/article/create/new', {
|
||||
title,
|
||||
metahuman_text,
|
||||
content_group,
|
||||
id
|
||||
})
|
||||
export function save(params:ArticleSaveParam) {
|
||||
return post<{ content: string }>(params.id && params.id > 0 ? '/article/modify' : '/article/create/new',params)
|
||||
}
|
||||
|
||||
export function push2video(article_ids: Id[]) {
|
||||
|
@ -15,6 +15,9 @@ export function modifyOrder(ids: Id[]) {
|
||||
export function deleteByIds(ids: Id[]) {
|
||||
return post('/room/remove', {ids})
|
||||
}
|
||||
export function restoreByIds(ids: Id[]) {
|
||||
return post('/room/restore', {ids})
|
||||
}
|
||||
|
||||
export function getLiveUrl() {
|
||||
return get<{flv_url:string}>({
|
||||
|
15
src/service/api/order.ts
Normal file
15
src/service/api/order.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {post} from "@/service/request.ts";
|
||||
|
||||
type OrderInfoData = DataList<OrderInfo> & {
|
||||
remaining_duration: string | number;
|
||||
}
|
||||
|
||||
export function getList(params: OrderSearchParam) {
|
||||
return post<OrderInfoData>('/order/list', params)
|
||||
}
|
||||
|
||||
|
||||
export async function getRemainingDuration() {
|
||||
const result = await getList({pagination: {page: 1, limit: 1}})
|
||||
return Number(result.remaining_duration)
|
||||
}
|
11
src/service/api/recycle.ts
Normal file
11
src/service/api/recycle.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {post} from "@/service/request.ts";
|
||||
|
||||
export function getList(params: NormalSearchParams) {
|
||||
return post<DataList<VideoInfo>>('/recycle/list', params)
|
||||
}
|
||||
export function remove(ids: Id[]) {
|
||||
return post('/recycle/remove', {ids})
|
||||
}
|
||||
export function restore(ids: Id[]) {
|
||||
return post('/recycle/restore', {ids})
|
||||
}
|
@ -17,21 +17,24 @@ export function deleteHistories(ids: Id[]) {
|
||||
* @param content_group
|
||||
* @param article_id
|
||||
*/
|
||||
export function regenerate(title: string, metahuman_text: string, content_group: BlockContent[][], article_id?: Id) {
|
||||
export function regenerate(params:{title: string, metahuman_text: string, content_group: BlockContent[][], id?: Id}) {
|
||||
return post<{ content: string }>({
|
||||
url: '/video/regenerate',
|
||||
data: {
|
||||
title,
|
||||
metahuman_text,
|
||||
content_group,
|
||||
article_id
|
||||
...params,
|
||||
article_id:params.id
|
||||
}
|
||||
})
|
||||
}
|
||||
// 重新生成视频
|
||||
export async function regenerateById(article_id: Id) {
|
||||
const article = await getArticle(article_id);
|
||||
return await regenerate(article.title, article.metahuman_text, article.content_group, article_id)
|
||||
return await regenerate({
|
||||
title:article.title,
|
||||
metahuman_text:article.metahuman_text,
|
||||
content_group:article.content_group,
|
||||
id:article_id
|
||||
})
|
||||
}
|
||||
|
||||
export function getById(id: Id) {
|
||||
@ -39,7 +42,7 @@ export function getById(id: Id) {
|
||||
}
|
||||
|
||||
export function deleteFromList(ids: Id[]) {
|
||||
return post('/video/outside', {ids})
|
||||
return post('/video/remove', {ids})
|
||||
}
|
||||
|
||||
|
||||
|
@ -12,6 +12,7 @@ const Axios = axios.create({
|
||||
headers: {'Content-Type': JSON_FORMAT}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const {globalConfig} = useGlobalConfig();
|
||||
// 请求前拦截
|
||||
Axios.interceptors.request.use(config => {
|
||||
|
29
src/types/api.d.ts
vendored
29
src/types/api.d.ts
vendored
@ -67,6 +67,7 @@ interface BasicArticleInfo {
|
||||
content_word_count?: number;
|
||||
media_id: number;
|
||||
fanwen_column_id: number;
|
||||
hot_news: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,6 +86,11 @@ declare interface ListCrawlerNewsItem extends BasicArticleInfo {
|
||||
// 内部文章关联id
|
||||
internal_article_id: number;
|
||||
}
|
||||
declare interface NormalSearchParams extends ApiRequestPageParams{
|
||||
// 标题
|
||||
title?: string;
|
||||
time_flag?: number;
|
||||
}
|
||||
declare interface VideoSearchParams extends ApiRequestPageParams{
|
||||
// 标题
|
||||
title?: string;
|
||||
@ -96,11 +102,13 @@ declare interface VideoInfo {
|
||||
title: string;
|
||||
cover: string;
|
||||
oss_video_url: string;
|
||||
oss_video_mp4_url?: string;
|
||||
duration: number;
|
||||
article_id: number;
|
||||
status: number;
|
||||
publish_time?: number|string;
|
||||
ctime?: number|string;
|
||||
d_time?: number|string;
|
||||
}
|
||||
declare interface VideoListItem extends VideoInfo {
|
||||
playing?: boolean;
|
||||
@ -113,6 +121,7 @@ declare interface LiveVideoInfo {
|
||||
video_title: string;
|
||||
cover: string;
|
||||
video_duration: number;
|
||||
oss_video_url?: string;
|
||||
video_oss_url: string;
|
||||
status: number;
|
||||
order_no: string;
|
||||
@ -124,3 +133,23 @@ declare interface LiveState{
|
||||
id: number;
|
||||
live_start_time: number;
|
||||
}
|
||||
|
||||
// order
|
||||
declare interface OrderSearchParam extends ApiRequestPageParams{
|
||||
// 标题
|
||||
title?: string;
|
||||
time_flag?: number;
|
||||
}
|
||||
declare interface OrderInfo {
|
||||
order_id: number| string;
|
||||
// 缩略图
|
||||
img_url: string;
|
||||
// 标题
|
||||
title: string;
|
||||
// 下单时间
|
||||
order_time: number | string;
|
||||
// 消费时长
|
||||
consumption_duration: number;
|
||||
// 操作人
|
||||
operator: string;
|
||||
}
|
||||
|
26
src/types/core.d.ts
vendored
26
src/types/core.d.ts
vendored
@ -28,11 +28,35 @@ declare interface ArticleContentGroup {
|
||||
blocks: BlockContent[];
|
||||
}
|
||||
|
||||
interface TemplateOption {
|
||||
background: string;
|
||||
template_id: string;
|
||||
}
|
||||
|
||||
interface ArticleTemplateInfo {
|
||||
select: string;
|
||||
options: TemplateOption[];
|
||||
}
|
||||
|
||||
interface ArticleSaveParam {
|
||||
title: string;
|
||||
metahuman_text: string;
|
||||
video_tag?: string;
|
||||
background?: string;
|
||||
content_group: BlockContent[][];
|
||||
hot_news: string[];
|
||||
id?: number;
|
||||
}
|
||||
|
||||
declare interface ArticleDetail {
|
||||
id: number;
|
||||
title: string;
|
||||
metahuman_text: string;
|
||||
content_group: BlockContent[][]
|
||||
video_tag: string;
|
||||
template_info: ArticleTemplateInfo;
|
||||
hot_news_mode?: string;
|
||||
hot_news: string[]; // 4月 6 日新增
|
||||
content_group: BlockContent[][];
|
||||
}
|
||||
|
||||
declare interface NewsInfo {
|
||||
|
61
src/types/tcplayer.d.ts
vendored
Normal file
61
src/types/tcplayer.d.ts
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
|
||||
type TCPlayerEvents = 'play' // 已经开始播放,调用 play() 方法或者设置了 autoplay 为 true 且生效时触发,这时 paused 属性为 false。
|
||||
| 'playing' // 因缓冲而暂停或停止后恢复播放时触发,paused 属性为 false 。通常用这个事件来标记视频真正播放,play 事件只是开始播放,画面并没有开始渲染。
|
||||
| 'loadstart' // 开始加载数据时触发。
|
||||
| 'durationchange' // 视频的时长数据发生变化时触发。
|
||||
| 'loadedmetadata' // 已加载视频的 metadata。
|
||||
| 'loadeddata' // 当前帧的数据已加载,但没有足够的数据来播放视频的下一帧时,触发该事件。
|
||||
| 'progress' // 在获取到媒体数据时触发。
|
||||
| 'canplay' // 当播放器能够开始播放视频时触发。
|
||||
| 'canplaythrough' // 当播放器预计能够在不停下来进行缓冲的情况下持续播放指定的视频时触发。
|
||||
| 'error' // 视频播放出现错误时触发。
|
||||
| 'pause' // 暂停时触发。
|
||||
| 'blocked' // 自动播放被浏览器阻止时触发。(原 2005 回调事件统一合并到 blocked 事件中)。
|
||||
| 'ratechange' // 播放速率变更时触发。
|
||||
| 'seeked' // 搜寻指定播放位置结束时触发。
|
||||
| 'seeking' // 搜寻指定播放位置开始时触发。
|
||||
| 'timeupdate' // 当前播放位置有变更,可以理解为 currentTime 有变更。
|
||||
| 'volumechange' // 设置音量或者 muted 属性值变更时触发。
|
||||
| 'waiting' // 播放停止,下一帧内容不可用时触发。
|
||||
| 'ended' // 视频播放已结束时触发。此时 currentTime 值等于媒体资源最大值。
|
||||
| 'resolutionswitching' // 清晰度切换进行中。
|
||||
| 'resolutionswitched' // 清晰度切换完毕。
|
||||
| 'fullscreenchange' //全屏状态切换时触发。
|
||||
| 'webrtcevent' // 播放 webrtc 时的事件集合。
|
||||
| 'webrtcstats' // 播放 webrtc 时的统计数据。
|
||||
| 'webrtcfallback' // 播放 webrtc 时触发降级。
|
||||
|
||||
declare type TCPlayerInstance = {
|
||||
//监听事件。
|
||||
on: (event: TCPlayerEvents, callback: () => void) => void;
|
||||
// 取消监听事件。
|
||||
off: (event: TCPlayerEvents, callback: () => void) => void;
|
||||
// 监听事件,事件处理函数最多只执行1次。
|
||||
one: (event: TCPlayerEvents, callback: () => void) => void;
|
||||
// 设置播放地址。
|
||||
src: (url: string) => void;
|
||||
// 设置播放器初始化完成后的回调。
|
||||
ready: (callback: () => void) => void;
|
||||
play: () => void;
|
||||
pause: () => void;
|
||||
unload: () => void;
|
||||
//获取或设置播放器是否静音。
|
||||
muted: (mute: boolean) => boolean | void;
|
||||
//获取或设置播放器音量。
|
||||
volume: (percent: number) => number | void;
|
||||
// 获取或设置播放倍速。
|
||||
playbackRate: (percent: number) => number | void;
|
||||
//获取当前播放时间点,或者设置播放时间点,该时间点不能超过视频时长。
|
||||
currentTime: (seconds?: number) => number;
|
||||
//获取视频时长。
|
||||
duration: () => number;
|
||||
// 销毁播放器。
|
||||
dispose: () => number;
|
||||
};
|
||||
|
||||
declare function TCPlayer(container: HTMLVideoElement | string, options: any): TCPlayerInstance;
|
||||
|
||||
declare module 'tcplayer.js' {
|
||||
export default TCPlayer;
|
||||
}
|
@ -54,6 +54,18 @@ function getDayjs(time:any){
|
||||
}
|
||||
return dayjs(time);
|
||||
}
|
||||
// 将时长(秒)转换成时间
|
||||
export function formatDurationToTime(duration?: number|string) {
|
||||
duration = duration ? Number(duration) : 0;
|
||||
if (!duration || isNaN(duration) || duration < 0) return '00:00';
|
||||
duration = Math.ceil(duration / 1000);
|
||||
// 计算
|
||||
const hour = Math.floor(duration / 3600);
|
||||
const minute = Math.floor((duration - hour * 3600) / 60);
|
||||
const second = duration - hour * 3600 - minute * 60;
|
||||
// 需要补0
|
||||
return padStart(hour.toString(), 2, '0') + ':' + padStart(minute.toString(), 2, '0') + ':' + padStart(second.toString(), 2, '0')
|
||||
}
|
||||
|
||||
export function formatTime(time: any, template: 'min' | 'date' | string = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!time) return '-';
|
||||
|
2
src/vite-env.d.ts
vendored
2
src/vite-env.d.ts
vendored
@ -14,6 +14,8 @@ declare const AppConfig: {
|
||||
API_PREFIX: string;
|
||||
ONLY_LIVE: string;
|
||||
APP_LANG: string;
|
||||
// 腾讯播放器
|
||||
TCPlayerLicense: string;
|
||||
};
|
||||
declare const AppMode: 'test' | 'production' | 'development';
|
||||
|
||||
|
@ -4,6 +4,10 @@ import {resolve} from "path";
|
||||
import AppPackage from './package.json'
|
||||
import dayjs from "dayjs";
|
||||
|
||||
// 播放器 SDK Web 端(TCPlayer)自 5.0.0 版本起需获取 License 授权后方可使用。
|
||||
// <p>https://cloud.tencent.com/document/product/881/77877#.E5.87.86.E5.A4.87.E5.B7.A5.E4.BD.9C</p>
|
||||
const TCPlayerLicense = 'https://license.vod2.myqcloud.com/license/v2/1328581896_1/v_cube.license'
|
||||
|
||||
const DevServerList:{
|
||||
[key:string]:string
|
||||
} = {
|
||||
@ -30,7 +34,8 @@ export default defineConfig(({mode}) => {
|
||||
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',
|
||||
APP_LANG: process.env.APP_LANGUAGE || 'zh-CN'
|
||||
APP_LANG: process.env.APP_LANGUAGE,
|
||||
TCPlayerLicense
|
||||
}),
|
||||
AppMode: JSON.stringify(mode),
|
||||
AppBuildVersion: JSON.stringify(AppPackage.name + '-' + AppPackage.version + '-' + dayjs().format('YYYYMMDDHH_mmss'))
|
||||
|
Loading…
x
Reference in New Issue
Block a user