merge
This commit is contained in:
parent
be34a8bc9b
commit
cea77ea231
@ -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",
|
||||
|
@ -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": "全选",
|
||||
|
@ -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){
|
||||
if (!pinnedManagePanel.current) {
|
||||
return;
|
||||
}
|
||||
const _target = pinnedManagePanel.current!;
|
||||
if(visible){
|
||||
if (visible) {
|
||||
_target.style.height = 'auto'
|
||||
const {height} = _target.getBoundingClientRect()
|
||||
_target.style.height = '38px'
|
||||
requestAnimationFrame(()=>{
|
||||
requestAnimationFrame(() => {
|
||||
_target.style.height = `${height}px`
|
||||
})
|
||||
}else{
|
||||
requestAnimationFrame(()=>{
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
_target.style.height = '0'
|
||||
})
|
||||
}
|
||||
},[pinnedManagePanel])
|
||||
const setTrue = ()=> togglePinnedManagePanel(true)
|
||||
const setFalse = ()=>togglePinnedManagePanel(false)
|
||||
}, [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();
|
||||
@ -206,7 +209,7 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
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`}
|
||||
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)
|
||||
@ -236,13 +239,13 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
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)})
|
||||
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>)
|
||||
}
|
@ -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
81
src/pages/order/index.tsx
Normal 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
|
@ -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);
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
|
@ -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
13
src/types/api.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
@ -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 '-';
|
||||
|
Loading…
x
Reference in New Issue
Block a user