This commit is contained in:
LittleBoy 2025-04-11 19:20:06 +08:00
parent be34a8bc9b
commit cea77ea231
12 changed files with 236 additions and 92 deletions

View File

@ -26,7 +26,7 @@
"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": {
@ -134,6 +134,18 @@
"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"
},
"text": "Orders"
},
"select": {
"pushed": "Pushed: {{count}}",
"select_all": "Select all",

View File

@ -26,7 +26,7 @@
"delete_confirm": "是否要删除该视频",
"push_success": "一键推流成功,已推流至数字人直播间,请查看!",
"search_key": "请输入视频标题关键字进行信息",
"text": "历史视频"
"text": "回收站"
},
"history.pushed": "已推送 {{count}} 条",
"live": {
@ -134,6 +134,18 @@
"title_word_count": "字数",
"word_count": "字数"
},
"order": {
"left_time": "当前剩余时长",
"list": {
"consume_time": "消费时长",
"cover": "缩略图",
"id": "订单编号",
"operator": "操作人",
"order_time": "下单时间",
"title": "标题"
},
"text": "订单记录"
},
"select": {
"pushed": "已推送: {{count}} 条",
"select_all": "全选",

View File

@ -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";
@ -14,6 +14,8 @@ import {useTranslation} from "react-i18next";
type SearchPanelProps = {
onSearch?: (params: ApiArticleSearchParams) => void;
defaultParams?: Partial<ApiArticleSearchParams>;
hideNewsSource?: boolean;
rightRender?: React.ReactNode;
}
const pagination = {
limit: 12, page: 1
@ -23,15 +25,15 @@ const DEFAULT_STATE = {
tag_level_2_id: -1,
subOptions: []
}
export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) {
export default function SearchPanel({onSearch, defaultParams, hideNewsSource,rightRender}: 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 +41,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 +53,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,28 +119,28 @@ 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){
return;
}
const _target = pinnedManagePanel.current!;
if(visible){
_target.style.height = 'auto'
const {height} = _target.getBoundingClientRect()
_target.style.height = '38px'
requestAnimationFrame(()=>{
_target.style.height = `${height}px`
})
}else{
requestAnimationFrame(()=>{
_target.style.height = '0'
})
}
},[pinnedManagePanel])
const setTrue = ()=> togglePinnedManagePanel(true)
const setFalse = ()=>togglePinnedManagePanel(false)
if (!pinnedManagePanel.current) {
return;
}
const _target = pinnedManagePanel.current!;
if (visible) {
_target.style.height = 'auto'
const {height} = _target.getBoundingClientRect()
_target.style.height = '38px'
requestAnimationFrame(() => {
_target.style.height = `${height}px`
})
} else {
requestAnimationFrame(() => {
_target.style.height = '0'
})
}
}, [pinnedManagePanel])
const setTrue = () => togglePinnedManagePanel(true)
const setFalse = () => togglePinnedManagePanel(false)
useClickAway(() => setFalse(), pinnedManagePanel)
return (<div className={`${styles.searchPanel} pt-6 pb-2`}>
@ -155,12 +157,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 +185,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();
@ -193,56 +196,56 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
</div>
<div ref={pinnedManagePanel} className={clsx(styles.pinnedManagePanelContainer)}>
{/* 固定新闻来源 */}
<div className={clsx(styles.pinnedManagePanel)}>
<div className="header flex justify-between">
<div className="title font-bold">{t('news.filter_source')}</div>
<div className={'cursor-pointer block hover:text-blue-500'} onClick={setFalse}>
<UpOutlined style={{fontSize: 20}}/>
{/* 固定新闻来源 */}
<div className={clsx(styles.pinnedManagePanel)}>
<div className="header flex justify-between">
<div className="title font-bold">{t('news.filter_source')}</div>
<div className={'cursor-pointer block hover:text-blue-500'} onClick={setFalse}>
<UpOutlined style={{fontSize: 20}}/>
</div>
</div>
<div className="tags-list-container">
{
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`}
key={it.value}
onClick={() => {
const value = Number(it.value)
if (pinnedTag && pinnedTag.includes(value)) {
setPinnedTag(pinnedTag.filter(s => s != value))
} else {
setPinnedTag([...(pinnedTag || []), value])
}
}}>
<span>{it.label}</span>
{currentPinned &&
<span className={'ml-2'}><IconPin/></span>}
</div>)
})
}
</div>
</div>
<div className="tags-list-container">
{
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`}
key={it.value}
onClick={() => {
const value = Number(it.value)
if (pinnedTag && pinnedTag.includes(value)) {
setPinnedTag(pinnedTag.filter(s => s != value))
} else {
setPinnedTag([...(pinnedTag || []), value])
}
}}>
<span>{it.label}</span>
{currentPinned &&
<span className={'ml-2'}><IconPin/></span>}
</div>)
})
}
</div>
</div>
</div>
{/* 二级目录 */}
{state.tag_level_1_id != -1 && subOptions.length > 0 &&
<div
className="absolute news-source-lv-2 flex items-center absolute left-0 right-0">
{
subOptions.map(it => (
<div
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)})
}}>{it.label}</div>)
)
}
</div>}
<div
className="absolute news-source-lv-2 flex items-center absolute left-0 right-0">
{
subOptions.map(it => (
<div
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)})
}}>{it.label}</div>)
)
}
</div>}
</div>
</div>
</div>}
</div>)
}

View File

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

81
src/pages/order/index.tsx Normal file
View File

@ -0,0 +1,81 @@
import SearchPanel from "@/pages/news/components/search-panel.tsx";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import styles from "@/pages/news/components/style.module.scss";
import {formatDurationToTime, formatTime} from "@/util/strings.ts";
import {IconDelete, IconEdit, IconWarningCircle} from "@/components/icons";
import {Popconfirm} from "antd";
import {useSetState} from "ahooks";
const mockList: OrderInfo[] = Array(10).fill(0).map((_, id) => (
{
id: id + 1,
cover: "https://staticplus.gachafun.com/fengmang/imgs/20241216/3fa3da5027cce22acb03283e8d688749.jpg",
title: `我国成功发射卫星互联网低轨卫星 ${id}`,
order_time: "2025-03-25 11:11:11",
consume_time: 60,
operator: "张三"
}
))
function OrderIndex() {
const {t} = useTranslation()
const [params, setParams] = useState<ApiArticleSearchParams>({
pagination: {page: 1, limit: 12},
time_flag: 1,
})
const [dataList, setDataList] = useState<OrderInfo[]>([...mockList])
const [state, setState] = useSetState({
loading: false,
leftTime: 2000,
})
return <div className="container pb-5 page-order-index">
<SearchPanel
hideNewsSource={true} defaultParams={params} onSearch={setParams}
rightRender={<div>{t('order.left_time')}: <span className={`${state.leftTime < 3600 ? 'text-red-600':''}`}>{formatDurationToTime(state.leftTime)}</span> </div>}
/>
<div className=" mt-2">
<div className={styles.newListTable}>
<div className="header row flex">
<div className="col w-[160px]">{t('order.list.id')}</div>
<div className="col w-[180px]">{t('order.list.cover')}</div>
<div className="col title">{t('order.list.title')}</div>
<div className="col w-[180px]">{t('order.list.order_time')}</div>
<div className="col w-[180px]">{t('order.list.consume_time')}</div>
<div className="col w-[180px]">{t('order.list.operator')}</div>
</div>
<div className="data-list-container">
{dataList?.map((item, i) => {
return <div key={i} className="row flex">
<div className="col w-[160px] text-center">
<div className="flex-1">
<div className="text-base">{item.id}</div>
</div>
</div>
<div className="col w-[180px]">
<img src={item.cover} className="rounded w-[140px] h-[60px]" alt=""/>
</div>
<div className="col flex-1">
<div className="text-sm 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>
<div className="col w-[180px]">
<div
className="text-sm">{formatTime(item.consume_time, 'YYYY-MM-DD HH:mm')}</div>
</div>
<div className="col w-[180px]">
<div className="text-sm">{item.operator}</div>
</div>
</div>
})}
</div>
</div>
</div>
</div>
}
export default OrderIndex

View File

@ -249,6 +249,7 @@ export default function VideoIndex() {
playing={state.playingId == v.id}
checked={checkedIdArray.includes(v.id)}
className={`list-item-${index} mt-3 mb-2 list-item-state-${v.status} `}
downloadVisible={true}
onCheckedChange={(checked) => {
setCheckedIdArray(idArray => {
const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);

View File

@ -9,7 +9,6 @@ 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";

View File

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

View File

@ -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 {useTranslation} from "react-i18next";
import AuthGuard from "@/routes/layout/auth-guard.tsx";
import {LogoText} from "@/components/icons/logo.tsx";
@ -12,8 +13,6 @@ 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";
type LayoutProps = {
@ -29,12 +28,19 @@ const NavigationUserContainer = () => {
}
const items: MenuProps['items'] = [
{
key: 'profile',
key: 'history',
label: <div className="nav-item" onClick={() => navigate('/history')}>
<IconVideo />
<span className={"nav-text"}>{t('history.text')}</span>
</div>,
},
{
key: 'order',
label: <div className="nav-item" onClick={() => navigate('/order')}>
<IconVideo />
<span className={"nav-text"}>{t('order.text')}</span>
</div>,
},
// {
// key: 'logout',
// label: <div onClick={handleLogout}>退出</div>,

View File

@ -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 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[] = [
@ -39,6 +39,10 @@ const routes: RouteObject[] = [
path: 'history',
element: <LibraryIndex/>
},
{
path: 'order',
element: <OrderIndex/>
},
{
path: 'live',
element: <LiveIndex/>

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

@ -127,3 +127,16 @@ declare interface LiveState{
id: number;
live_start_time: number;
}
declare interface OrderInfo {
id: number| string;
// 缩略图
cover: string;
// 标题
title: string;
// 下单时间
order_time: number | string;
// 消费时长
consume_time: number;
// 操作人
operator: string;
}

View File

@ -54,6 +54,16 @@ function getDayjs(time:any){
}
return dayjs(time);
}
// 将时长(秒)转换成时间
export function formatDurationToTime(duration: number) {
if (duration < 0 || isNaN(duration)) return '00:00';
duration = Math.ceil(duration);
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 '-';