Add streamer pause/resume feature and UI updates
Introduces a pause/resume button for streamers in the UI and backend, including API endpoint and status handling. Refactors streamer status display, updates related TypeScript interfaces, and improves modal forms. Also updates @douyinfe/semi-ui and related dependencies to v2.86.0, and bumps workspace version to 1.1.10. Minor logging and trait changes in Rust backend for improved clarity and error handling.
This commit is contained in:
1 parent
d4b6a5fb22
commit
92b5a30df5
21 files changed
+270
-148
No files matched your search
@@ -1,21 +1,21 @@
|
||||
'use client'
|
||||
import {
|
||||
Layout,
|
||||
Nav,
|
||||
Button,
|
||||
Tag,
|
||||
Typography,
|
||||
Popconfirm,
|
||||
Notification,
|
||||
Card,
|
||||
Layout,
|
||||
Nav,
|
||||
Button,
|
||||
Tag,
|
||||
Typography,
|
||||
Popconfirm,
|
||||
Notification,
|
||||
Card, Dropdown,
|
||||
} from '@douyinfe/semi-ui'
|
||||
import {
|
||||
IconHelpCircle,
|
||||
IconPlusCircle,
|
||||
IconVideoListStroked,
|
||||
IconEdit2Stroked,
|
||||
IconDeleteStroked,
|
||||
IconWrench,
|
||||
IconHelpCircle,
|
||||
IconPlusCircle,
|
||||
IconVideoListStroked,
|
||||
IconEdit2Stroked,
|
||||
IconDeleteStroked,
|
||||
IconWrench, IconTreeTriangleDown, IconPause, IconPlay,
|
||||
} from '@douyinfe/semi-icons'
|
||||
import { List, ButtonGroup } from '@douyinfe/semi-ui'
|
||||
import React, { useState } from 'react'
|
||||
@@ -24,6 +24,7 @@ import TemplateModal from '../../ui/TemplateModal'
|
||||
import OverrideModal from '../../ui/OverrideModal'
|
||||
import { LiveStreamerEntity, put, requestDelete, sendRequest } from '../../lib/api-streamer'
|
||||
import useSWRMutation from 'swr/mutation'
|
||||
import {PauseButton} from "@/app/ui/StreamerActions/PauseButton";
|
||||
|
||||
export default function Home() {
|
||||
const { Header, Content } = Layout
|
||||
@@ -54,25 +55,28 @@ export default function Home() {
|
||||
return values
|
||||
}
|
||||
const data: LiveStreamerEntity[] | undefined = streamers?.map(live => {
|
||||
let status
|
||||
let statusTag
|
||||
switch (live.status) {
|
||||
case 'Working':
|
||||
status = <Tag color="red">直播中</Tag>
|
||||
statusTag = <Tag color="red">直播中</Tag>
|
||||
break
|
||||
case 'Idle':
|
||||
status = <Tag color="green">空闲</Tag>
|
||||
statusTag = <Tag color="green">空闲</Tag>
|
||||
break
|
||||
case 'Pending':
|
||||
status = <Tag color="grey">未知</Tag>
|
||||
statusTag = <Tag color="grey">等待下载</Tag>
|
||||
break
|
||||
case 'Inspecting':
|
||||
status = <Tag color="indigo">检测/上传中</Tag>
|
||||
statusTag = <Tag color="indigo">检测/上传中</Tag>
|
||||
break
|
||||
case 'OutOfSchedule':
|
||||
status = <Tag color="green">非录播时间</Tag>
|
||||
statusTag = <Tag color="green">非录播时间</Tag>
|
||||
break
|
||||
case 'Pause':
|
||||
statusTag = <Tag color="pink">暂停中</Tag>
|
||||
break
|
||||
}
|
||||
return { ...handleEntityPostprocessor(live), status }
|
||||
return { ...handleEntityPostprocessor(live), statusTag }
|
||||
})
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
@@ -96,6 +100,7 @@ export default function Home() {
|
||||
const handleUpdate = async (values: any) => {
|
||||
console.log(values);
|
||||
delete values.status
|
||||
delete values.statusTag
|
||||
if (values?.postprocessor) {
|
||||
values.postprocessor = values.postprocessor.map(
|
||||
({ cmd, value }: { cmd: string; value: string }) => (cmd === 'rm' ? 'rm' : { [cmd]: value })
|
||||
@@ -112,6 +117,7 @@ export default function Home() {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
@@ -214,7 +220,7 @@ export default function Home() {
|
||||
}
|
||||
}
|
||||
>
|
||||
<div style={{ position: 'absolute', right: 20, top: 9 }}>{item.status}</div>
|
||||
<div style={{ position: 'absolute', right: 20, top: 9 }}>{item.statusTag}</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<h3
|
||||
style={{
|
||||
@@ -248,9 +254,7 @@ export default function Home() {
|
||||
<Button theme="borderless" icon={<IconEdit2Stroked />}></Button>
|
||||
</TemplateModal>
|
||||
<span className="semi-button-group-line semi-button-group-line-borderless semi-button-group-line-primary"></span>
|
||||
<OverrideModal onOk={handleUpdate} entity={item}>
|
||||
<Button theme="borderless" icon={<IconWrench />}></Button>
|
||||
</OverrideModal>
|
||||
<PauseButton streamer={item}/>
|
||||
<span className="semi-button-group-line semi-button-group-line-borderless semi-button-group-line-primary"></span>
|
||||
<Popconfirm
|
||||
title="确定是否要删除?"
|
||||
@@ -260,6 +264,18 @@ export default function Home() {
|
||||
>
|
||||
<Button theme="borderless" icon={<IconDeleteStroked />}></Button>
|
||||
</Popconfirm>
|
||||
<span className="semi-button-group-line semi-button-group-line-borderless semi-button-group-line-primary"></span>
|
||||
<Dropdown render={
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item>
|
||||
<OverrideModal onOk={handleUpdate} entity={item}>
|
||||
<Button theme="borderless" icon={<IconWrench />}></Button>
|
||||
</OverrideModal>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
} trigger="click" position="bottomRight">
|
||||
<Button theme="borderless" type="primary" icon={<IconTreeTriangleDown />}></Button>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -100,7 +100,8 @@ export interface LiveStreamerEntity {
|
||||
split_time?: number;
|
||||
split_size?: number;
|
||||
upload_id?: number;
|
||||
status?: string | React.ReactNode;
|
||||
status?: string;
|
||||
statusTag?: React.ReactNode;
|
||||
format?: string;
|
||||
time_range?: string | Date[];
|
||||
excluded_keywords?: string[];
|
||||
|
||||
@@ -222,12 +222,6 @@ const OverrideModal: React.FC<TemplateModalProps> = ({ children, entity, onOk })
|
||||
stopValidateWithError={true}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="filename_prefix"
|
||||
label="文件名模板(filename_prefix)"
|
||||
placeholder="{streamer}%Y-%m-%dT%H_%M_%S"
|
||||
/>
|
||||
|
||||
<Form.InputNumber
|
||||
field="filtering_threshold"
|
||||
label="碎片过滤(filtering_threshold)"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSWRConfig } from 'swr';
|
||||
import useSWRMutation from 'swr/mutation';
|
||||
import {Button} from "@douyinfe/semi-ui";
|
||||
import {IconPause, IconPlay} from "@douyinfe/semi-icons";
|
||||
import {API_BASE, LiveStreamerEntity} from "@/app/lib/api-streamer";
|
||||
|
||||
interface PauseButtonProps {
|
||||
streamer: LiveStreamerEntity;
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
// 暂停主播
|
||||
export const pauseStreamer = async (url: string, ) => {
|
||||
const response = await fetch(API_BASE + url,
|
||||
{
|
||||
method: 'PUT',
|
||||
// headers: {'Content-Type': 'application/json'},
|
||||
}
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
export const PauseButton: React.FC<PauseButtonProps> = ({
|
||||
streamer,
|
||||
onSuccess,
|
||||
onError
|
||||
}) => {
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const { trigger: pauseTrigger } = useSWRMutation(
|
||||
`/v1/streamers/${streamer.id}/pause`,
|
||||
pauseStreamer
|
||||
);
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
await pauseTrigger();
|
||||
// 重新加载列表数据
|
||||
await mutate('/v1/streamers');
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error('暂停失败:', error);
|
||||
onError?.(error as Error);
|
||||
}
|
||||
};
|
||||
console.log("????????????", streamer.status)
|
||||
return (
|
||||
<Button onClick={handlePause} icon={streamer.status === 'Pause'? <IconPlay />: <IconPause />} theme="borderless" aria-label="暂停" />
|
||||
);
|
||||
};
|
||||
@@ -155,6 +155,12 @@ const TemplateModal: React.FC<TemplateModalProps> = ({ children, entity, onOk })
|
||||
rules={[{ required: true, message }]}
|
||||
/>
|
||||
|
||||
<Form.Input
|
||||
field="filename_prefix"
|
||||
label="文件名模板(可选)"
|
||||
placeholder="{streamer}%Y-%m-%dT%H_%M_%S"
|
||||
/>
|
||||
|
||||
<Form.Select
|
||||
showClear
|
||||
field="upload_streamers_id"
|
||||
|
||||
Reference in new issue
Block a user