feat: ️ 新增订单/回收站导航

fix:直播间回退
style: 📚️调整相关UI
This commit is contained in:
LittleBoy 2025-04-16 11:29:54 +08:00
parent 42e2d3fcc0
commit 99323df02b
12 changed files with 93 additions and 69 deletions

View File

@ -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 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -137,7 +137,7 @@ export const VideoListItem = (
{/* <button className="hover:text-blue-500 cursor-move">*/}
{/* <MenuOutlined/>*/}
{/* </button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>)}*/}
<div className={"flex items-center justify-center 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()
@ -168,15 +168,13 @@ export const VideoListItem = (
{onRemove && !failed && <DeleteItemPopoverConfirm
description={failed ? t('video.rollback_confirm_title') : undefined}
onConfirm={() => onRemove(failed ? 'rollback' : 'delete')}>
<span className="icon-btn">
<button className="hover:text-blue-500" title={
i18n.language == 'zh-CN'?'重新生成':'Regenerate'
}>
{removeIcon ? removeIcon : (failed ?
<IconRollbackCircle/> :
<IconDelete/>)}
</button>
</span>
<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={() => {

View File

@ -1,5 +1,5 @@
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import {Button, Checkbox, Empty, Modal, Popconfirm, 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";
@ -7,16 +7,17 @@ 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 {formatDuration} from "@/util/strings.ts";
import {Player, PlayerInstance} from "@/components/video/player.tsx";
import {IconDelete, IconLocked, IconRollbackCircle, IconWarningCircle} from "@/components/icons";
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 styles from "./style.module.scss"
import {ModalWarningIcon, ModalWarningTitle} from "@/components/icons/ModalWarning.tsx";
const cache: { flvPlayer?: FlvJs.Player, timerPlayNext?: any, timerLoadState?: any, prevUrl?: string } = {}
@ -25,7 +26,6 @@ export default function LiveIndex() {
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)
@ -195,7 +195,7 @@ export default function LiveIndex() {
await deleteByIds(delIds)
}
if(rollbackIds.length > 0) {
showToast('回退暂未实现')
await restoreByIds(rollbackIds)
}
// 调整排序
await modifyOrder(ids);
@ -356,8 +356,9 @@ export default function LiveIndex() {
rootClassName={'popconfirm-main'}
placement={'left'}
arrow={false}
icon={<IconWarningCircle/>}
title={t('video.live_rollback_confirm_title')}
icon={<ModalWarningIcon/>}
title={<ModalWarningTitle />}
description={t('video.live_rollback_confirm_title')}
onConfirm={() => handleRollback(v)}
><button className="hover:text-blue-500"><IconRollbackCircle /></button></Popconfirm>}
</>}
@ -388,6 +389,5 @@ export default function LiveIndex() {
<IconDelete/>
</ButtonBatch>}
</div>
{contextHolder}
</div>)
}

View File

@ -12,6 +12,7 @@ import {IconPin} from "@/components/icons";
import {useTranslation} from "react-i18next";
type SearchPanelProps = {
rootClassName?: string;
onSearch?: (params: ApiArticleSearchParams) => void;
defaultParams?: Partial<ApiArticleSearchParams>;
hideNewsSource?: boolean;
@ -25,7 +26,14 @@ const DEFAULT_STATE = {
tag_level_2_id: -1,
subOptions: []
}
export default function SearchPanel({onSearch, defaultParams, hideNewsSource,rightRender}: SearchPanelProps) {
export default function SearchPanel(
{
onSearch,
defaultParams,
hideNewsSource,
rightRender,
rootClassName
}: SearchPanelProps) {
const tags = useArticleTags();
const {t} = useTranslation()
const [params, setParams] = useSetState<ApiArticleSearchParams>({
@ -143,7 +151,7 @@ export default function SearchPanel({onSearch, defaultParams, hideNewsSource,rig
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

View File

@ -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;
@ -85,9 +85,8 @@
.header{
@apply bg-primary-bg;
.col{
@apply text-sm;
@apply text-base;
height: 42px;
}
.operations{
}
@ -104,21 +103,22 @@
:global {
.title{
justify-content: left;
text-align: center;
}
.id{
@apply pl-0;
width: 130px;
width: 140px;
line-height: 1.2em;
&:after{
display: none;
}
}
.cover{
img{
width: 120px;
max-height: 50px;
}
width: 140px;
//img{
// max-width: 100px;
// max-height: 56px;
//}
}
.title {
@apply flex-1 pl-4;

View File

@ -1,6 +1,6 @@
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {Empty, Image, Pagination} from "antd";
import {Empty, Pagination} from "antd";
import {useRequest} from "ahooks";
import SearchPanel from "@/pages/news/components/search-panel.tsx";
@ -8,7 +8,7 @@ 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/error_img.svg"
import ImageErr from "@/assets/images/error/ic_broken_image.png"
function OrderIndex() {
const {t} = useTranslation()
@ -20,16 +20,20 @@ function OrderIndex() {
refreshDeps: [params],
})
return <div className="container pb-5 page-order-index">
<SearchPanel
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 className="mt-2">
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>
@ -39,7 +43,7 @@ function OrderIndex() {
<div className="col w-[120px]">{t('order.list.consume_time')}</div>
<div className="col w-[180px]">{t('order.list.operator')}</div>
</div>
<div >
<div>
{data?.list.length === 0 && <div style={{marginTop: 50}}>
<Empty/>
</div>}
@ -51,24 +55,23 @@ function OrderIndex() {
</div>
</div>
<div className="col cover">
<Image
src={item.img_url} preview={false} className="rounded object-cover"
fallback={ImageErr}
/>
<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 className="line-clamp-2">{item.title}</div>
</div>
<div className="col w-[180px]">
<div className="text-sm">{formatTime(item.order_time, 'YYYY-MM-DD HH:mm')}</div>
<div className="col w-[180px]">{formatTime(item.order_time, 'YYYY-MM-DD HH:mm')}
</div>
<div className="col w-[120px]">
<div
className="text-sm">{formatDurationToTime(item.consumption_duration)}</div>
</div>
<div className="col w-[180px]">
<div className="text-sm">{item.operator}</div>
<div className="col w-[120px]">{formatDurationToTime(item.consumption_duration)}
</div>
<div className="col w-[180px]">{item.operator}</div>
</div>
})}

View File

@ -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 w-[60px] 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

View File

@ -1,6 +1,6 @@
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";
@ -13,7 +13,6 @@ import useAuth from "@/hooks/useAuth.ts";
import {hidePhone} from "@/util/strings.ts";
import {defaultCache} from "@/hooks/useCache.ts";
import {IconOrderFill, IconRecycleFill} from "@/components/icons";
import useGlobalConfig from "@/hooks/useGlobalConfig.ts";
import LanguageSwitcher from "@/components/icons/language-switcher.tsx";
@ -84,13 +83,25 @@ const NavigationUserContainer = () => {
</Dropdown> : <UserButton/>}
</div>)
}
const ExtraNavItems = {
'/order':'order.text',
'/recycle':'history.text',
}
export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
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">

View File

@ -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}>({

1
src/types/api.d.ts vendored
View File

@ -108,6 +108,7 @@ declare interface VideoInfo {
status: number;
publish_time?: number|string;
ctime?: number|string;
d_time?: number|string;
}
declare interface VideoListItem extends VideoInfo {
playing?: boolean;

View File

@ -58,7 +58,7 @@ function getDayjs(time:any){
export function formatDurationToTime(duration?: number|string) {
duration = duration ? Number(duration) : 0;
if (!duration || isNaN(duration) || duration < 0) return '00:00';
duration = Math.ceil(duration);
duration = Math.ceil(duration / 1000);
// 计算
const hour = Math.floor(duration / 3600);
const minute = Math.floor((duration - hour * 3600) / 60);