feat: ✨️ 新增订单/回收站导航
fix:直播间回退
style: 📚️调整相关UI
This commit is contained in:
parent
42e2d3fcc0
commit
99323df02b
@ -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 {
|
||||
|
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 |
@ -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'
|
||||
}>
|
||||
failed ? (i18n.language == 'zh-CN'?'重新生成':'Regenerate') : i18n.language == 'zh-CN'?'删除':'Delete'
|
||||
} style={{fontSize:20}}>
|
||||
{removeIcon ? removeIcon : (failed ?
|
||||
<IconRollbackCircle/> :
|
||||
<IconDelete/>)}
|
||||
</button>
|
||||
</span>
|
||||
</DeleteItemPopoverConfirm>}
|
||||
{hideCheckBox ? <></> :
|
||||
<Checkbox checked={state.checked} onChange={() => {
|
||||
|
@ -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>)
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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,8 +20,11 @@ function OrderIndex() {
|
||||
refreshDeps: [params],
|
||||
})
|
||||
|
||||
return <div className="container pb-5 page-order-index">
|
||||
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}
|
||||
@ -29,7 +32,8 @@ function OrderIndex() {
|
||||
className={`${!data?.remaining_duration || Number(data?.remaining_duration) < 3600 ? 'text-red-600' : ''}`}>{formatDurationToTime(data?.remaining_duration)}</span>
|
||||
</div>}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
</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>
|
||||
@ -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>
|
||||
<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>
|
||||
})}
|
||||
|
||||
|
@ -32,7 +32,7 @@ export default function VideoItem(props: VideoItemProps) {
|
||||
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>
|
||||
<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
|
||||
|
@ -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">
|
||||
|
@ -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
1
src/types/api.d.ts
vendored
@ -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;
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user