diff --git a/src/i18n/translations/en-US.json b/src/i18n/translations/en-US.json index 6d6900b..fe065ac 100644 --- a/src/i18n/translations/en-US.json +++ b/src/i18n/translations/en-US.json @@ -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", diff --git a/src/i18n/translations/zh-CN.json b/src/i18n/translations/zh-CN.json index a04c39f..f3bea1f 100644 --- a/src/i18n/translations/zh-CN.json +++ b/src/i18n/translations/zh-CN.json @@ -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": "全选", diff --git a/src/pages/news/components/search-panel.tsx b/src/pages/news/components/search-panel.tsx index f6438ac..920ea90 100644 --- a/src/pages/news/components/search-panel.tsx +++ b/src/pages/news/components/search-panel.tsx @@ -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; + 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({ pagination, - time_flag:1, + time_flag: 1, ...(defaultParams || {}) }); - const [prevSearchName, setPrevSearchName] = useState(defaultParams?.title||'') + const [prevSearchName, setPrevSearchName] = useState(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( 'user-pinned-tag-list', { @@ -117,28 +119,28 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) } return [] as OptionItem[]; }, [pinnedTag, tags]) - const pinnedManagePanel = useRef(null) + const pinnedManagePanel = useRef(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 (
@@ -155,12 +157,13 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) />
+ {rightRender &&
{rightRender}
} -
+ {!hideNewsSource &&
@@ -182,7 +185,7 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) )}
- { + { e.stopPropagation(); e.preventDefault(); setTrue(); @@ -193,56 +196,56 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
- {/* 固定新闻来源 */} -
-
-
{t('news.filter_source')}
-
- + {/* 固定新闻来源 */} +
+
+
{t('news.filter_source')}
+
+ +
+
+
+ { + tags.filter(s => s.value !== 999999).map(it => { + const currentPinned = pinnedTag?.includes(Number(it.value)); + return (
{ + const value = Number(it.value) + if (pinnedTag && pinnedTag.includes(value)) { + setPinnedTag(pinnedTag.filter(s => s != value)) + } else { + setPinnedTag([...(pinnedTag || []), value]) + } + }}> + {it.label} + {currentPinned && + } +
) + }) + + }
-
- { - tags.filter(s => s.value !== 999999).map(it => { - const currentPinned = pinnedTag?.includes(Number(it.value)); - return (
{ - const value = Number(it.value) - if (pinnedTag && pinnedTag.includes(value)) { - setPinnedTag(pinnedTag.filter(s => s != value)) - } else { - setPinnedTag([...(pinnedTag || []), value]) - } - }}> - {it.label} - {currentPinned && - } -
) - }) - - } -
-
{/* 二级目录 */} {state.tag_level_1_id != -1 && subOptions.length > 0 && -
- { - subOptions.map(it => ( -
{ - handleFilter({tag_level_1_id:state.tag_level_1_id,tag_level_2_id: Number(it.value)}) - }}>{it.label}
) - ) - } -
} +
+ { + subOptions.map(it => ( +
{ + handleFilter({tag_level_1_id: state.tag_level_1_id, tag_level_2_id: Number(it.value)}) + }}>{it.label}
) + ) + } +
}
-
+
}
) } \ No newline at end of file diff --git a/src/pages/news/index.tsx b/src/pages/news/index.tsx index fe5bb59..0f38651 100644 --- a/src/pages/news/index.tsx +++ b/src/pages/news/index.tsx @@ -89,7 +89,11 @@ export default function NewsIndex() { } } return (
- + { + // 滚动到顶部 + scrollerRef.current?.scrollToPosition(0) + setParams(params) + }}/> {activeNews && ( + { + 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({ + pagination: {page: 1, limit: 12}, + time_flag: 1, + }) + const [dataList, setDataList] = useState([...mockList]) + const [state, setState] = useSetState({ + loading: false, + leftTime: 2000, + }) + + return
+ {t('order.left_time')}: {formatDurationToTime(state.leftTime)}
} + /> +
+
+
+
{t('order.list.id')}
+
{t('order.list.cover')}
+
{t('order.list.title')}
+
{t('order.list.order_time')}
+
{t('order.list.consume_time')}
+
{t('order.list.operator')}
+
+
+ {dataList?.map((item, i) => { + return
+
+
+
{item.id}
+
+
+
+ +
+
+
{item.title}
+
+
+
{formatTime(item.order_time, 'YYYY-MM-DD HH:mm')}
+
+
+
{formatTime(item.consume_time, 'YYYY-MM-DD HH:mm')}
+
+
+
{item.operator}
+
+
+ })} +
+
+
+
+} + +export default OrderIndex \ No newline at end of file diff --git a/src/pages/video/index.tsx b/src/pages/video/index.tsx index 7cf0c28..b147166 100644 --- a/src/pages/video/index.tsx +++ b/src/pages/video/index.tsx @@ -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); diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 95e1fbb..bd63e34 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -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"; diff --git a/src/routes/layout/auth-guard.tsx b/src/routes/layout/auth-guard.tsx index 7e53893..b352f9e 100644 --- a/src/routes/layout/auth-guard.tsx +++ b/src/routes/layout/auth-guard.tsx @@ -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 diff --git a/src/routes/layout/dashboard-layout.tsx b/src/routes/layout/dashboard-layout.tsx index adbc5df..ab77c15 100644 --- a/src/routes/layout/dashboard-layout.tsx +++ b/src/routes/layout/dashboard-layout.tsx @@ -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:
navigate('/history')}> {t('history.text')}
, }, + { + key: 'order', + label:
navigate('/order')}> + + {t('order.text')} +
, + }, // { // key: 'logout', // label:
退出
, diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx index f22c697..7c21562 100644 --- a/src/routes/routes.tsx +++ b/src/routes/routes.tsx @@ -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: }, + { + path: 'order', + element: + }, { path: 'live', element: diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 62ed11f..f41556d 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -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; +} diff --git a/src/util/strings.ts b/src/util/strings.ts index 7954ab1..07282e3 100644 --- a/src/util/strings.ts +++ b/src/util/strings.ts @@ -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 '-';