Compare commits
No commits in common. "4935c0674ff7f27a3fc9f24be8b47bf641a3b3a5" and "6603bbf75facfc6946d169de98ce4b20abe0aa20" have entirely different histories.
4935c0674f
...
6603bbf75f
@ -1,19 +0,0 @@
|
||||
FROM node:20
|
||||
|
||||
# 以及按需安装其他软件
|
||||
# RUN apt-get update && apt-get install -y git
|
||||
|
||||
# 安装 code-server 和 vscode 常用插件
|
||||
RUN curl -fsSL https://code-server.dev/install.sh | sh \
|
||||
&& code-server --install-extension redhat.vscode-yaml \
|
||||
&& code-server --install-extension dbaeumer.vscode-eslint \
|
||||
&& code-server --install-extension eamodio.gitlens \
|
||||
&& code-server --install-extension tencent-cloud.coding-copilot \
|
||||
&& echo done
|
||||
|
||||
# 安装 ssh 服务,用于支持 VSCode 客户端通过 Remote-SSH 访问开发环境
|
||||
RUN apt-get update && apt-get install -y wget unzip openssh-server
|
||||
|
||||
# 指定字符集支持命令行输入中文(根据需要选择字符集)
|
||||
ENV LANG C.UTF-8
|
||||
ENV LANGUAGE C.UTF-8
|
@ -152,7 +152,7 @@
|
||||
}
|
||||
|
||||
.list-row {
|
||||
@apply flex bg-white mt-2 py-1 rounded-xl gap-2 border;
|
||||
@apply flex bg-white mt-2 py-2 px-4 rounded-xl gap-2 border;
|
||||
border-width: 2px;
|
||||
&.playing{
|
||||
@apply border-primary-blue bg-[#d9eaff];
|
||||
@ -161,10 +161,10 @@
|
||||
@apply border-primary-blue bg-[#f4f7fc];
|
||||
}
|
||||
&.header-row{
|
||||
@apply text-sm;
|
||||
background: none;
|
||||
.col{
|
||||
height: 42px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,7 +174,7 @@
|
||||
|
||||
.col {
|
||||
@apply flex items-center relative pl-4 text-center justify-center;
|
||||
height: 60px;
|
||||
height: 80px;
|
||||
|
||||
&:after {
|
||||
@apply absolute;
|
||||
@ -187,7 +187,7 @@
|
||||
}
|
||||
|
||||
.number {
|
||||
width: 70px;
|
||||
width: 50px;
|
||||
padding-left: 10px;
|
||||
&:after {
|
||||
display: none;
|
||||
@ -210,9 +210,9 @@
|
||||
}
|
||||
|
||||
.operation {
|
||||
@apply flex items-center ml-2 gap-4 text-lg text-gray-400 justify-center;
|
||||
width: 120px;
|
||||
padding: 0;
|
||||
@apply flex items-center ml-2 gap-4 text-lg text-gray-400 justify-between;
|
||||
width: 180px;
|
||||
padding: 0 20px 0 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -266,45 +266,6 @@
|
||||
}
|
||||
|
||||
// override antd style
|
||||
.data-list-load-spin{
|
||||
.ant-spin-container::after{
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.popconfirm-main{
|
||||
.ant-popover-inner{
|
||||
@apply bg-white px-6 py-6 rounded-xl;
|
||||
min-width: 360px;
|
||||
box-shadow: 0 0 10px rgba(25, 25, 25, 0.1);
|
||||
}
|
||||
.icon-warning{
|
||||
@apply text-red-500;
|
||||
font-size: 20px;
|
||||
transform: translateY(5px);
|
||||
margin-right: 10px;
|
||||
}
|
||||
.ant-popconfirm-message{
|
||||
.ant-popconfirm-title{
|
||||
@apply text-xl font-bold;
|
||||
}
|
||||
.ant-popconfirm-description{
|
||||
@apply mt-4 text-gray-400 text-sm;
|
||||
margin-left: -30px;
|
||||
}
|
||||
}
|
||||
.ant-popconfirm-buttons{
|
||||
@apply mt-8;
|
||||
button{
|
||||
@apply rounded-2xl py-4 px-8;
|
||||
}
|
||||
.ant-btn-default{
|
||||
@apply bg-white shadow-none text-popconfirm-btn-cancel border border-popconfirm-btn-cancel hover:border-popconfirm-btn-cancel hover:text-popconfirm-btn-cancel hover:bg-white hover:bg-popconfirm-btn-cancel/10;
|
||||
}
|
||||
.ant-btn-primary{
|
||||
@apply bg-white shadow-none text-popconfirm-bg border border-popconfirm-bg hover:text-popconfirm-bg hover:bg-white hover:bg-popconfirm-btn-primary-hover/10;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-checkbox {
|
||||
border-radius: 2px;
|
||||
|
||||
|
@ -158,7 +158,7 @@ export default function ArticleEditModal(props: Props) {
|
||||
</div>
|
||||
<div className="modal-control-footer flex justify-end">
|
||||
<div className="text-lg flex gap-10 ">
|
||||
{props.type == 'news' && props.id ? <button className="text-gray-400 hover:text-gray-800" onClick={handlePush2Video}>{state.generating?'推送中...':'生成视频'}</button> : null}
|
||||
{props.type == 'news' && props.id > 0 ? <button className="text-gray-400 hover:text-gray-800" onClick={handlePush2Video}>{state.generating?'推送中...':'生成视频'}</button> : null}
|
||||
<button className="text-gray-400 hover:text-gray-800" onClick={() => props.onClose?.()}>取消</button>
|
||||
<button onClick={handleSave} className="text-gray-800 hover:text-blue-500">{props.type == 'news' ? '确定' : '重新生成'}</button>
|
||||
</div>
|
||||
|
@ -112,21 +112,6 @@ export const IconAddCircle = ({style, className}: IconProps) => (
|
||||
fill="currentColor"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const IconWarningCircle = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-warning`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 22 22" version="1.1">
|
||||
<g>
|
||||
<path
|
||||
d="M9.625 12.375V5.5H12.375V12.375H9.625ZM11 16.5C11.3647 16.5 11.7144 16.3551 11.9723 16.0973C12.2301 15.8394 12.375 15.4897 12.375 15.125C12.375 14.7603 12.2301 14.4106 11.9723 14.1527C11.7144 13.8949 11.3647 13.75 11 13.75C10.6353 13.75 10.2856 13.8949 10.0277 14.1527C9.76987 14.4106 9.625 14.7603 9.625 15.125C9.625 15.4897 9.76987 15.8394 10.0277 16.0973C10.2856 16.3551 10.6353 16.5 11 16.5Z"
|
||||
fill="currentColor"/>
|
||||
<path
|
||||
d="M0 11C0 8.08262 1.15893 5.28473 3.22183 3.22183C5.28473 1.15893 8.08262 0 11 0C13.9174 0 16.7153 1.15893 18.7782 3.22183C20.8411 5.28473 22 8.08262 22 11C22 13.9174 20.8411 16.7153 18.7782 18.7782C16.7153 20.8411 13.9174 22 11 22C8.08262 22 5.28473 20.8411 3.22183 18.7782C1.15893 16.7153 0 13.9174 0 11ZM11 2.75C9.91659 2.75 8.8438 2.96339 7.84286 3.37799C6.84193 3.7926 5.93245 4.40029 5.16637 5.16637C4.40029 5.93245 3.7926 6.84193 3.37799 7.84286C2.96339 8.8438 2.75 9.91659 2.75 11C2.75 12.0834 2.96339 13.1562 3.37799 14.1571C3.7926 15.1581 4.40029 16.0675 5.16637 16.8336C5.93245 17.5997 6.84193 18.2074 7.84286 18.622C8.8438 19.0366 9.91659 19.25 11 19.25C13.188 19.25 15.2865 18.3808 16.8336 16.8336C18.3808 15.2865 19.25 13.188 19.25 11C19.25 8.81196 18.3808 6.71354 16.8336 5.16637C15.2865 3.61919 13.188 2.75 11 2.75Z"
|
||||
fill="currentColor"/>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
)
|
||||
export const IconAdd = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
|
||||
|
@ -1,7 +1,5 @@
|
||||
import React, {CSSProperties, useCallback, useEffect, useImperativeHandle, useRef} from "react";
|
||||
import {useInViewport, useScroll} from "ahooks";
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {Spin} from "antd";
|
||||
|
||||
export type InfiniteScrollerRef = {
|
||||
scrollToPosition: (top: number) => void
|
||||
@ -29,7 +27,8 @@ const InfiniteScroller = React.forwardRef<InfiniteScrollerRef, InfiniteScrollerP
|
||||
const scrollPosition = useScroll(scrollContainerRef);
|
||||
const scrollToPosition = useCallback((top: number) => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current!.scrollTo({
|
||||
console.log('xaf');
|
||||
scrollContainerRef.current.scrollTo({
|
||||
top,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
@ -60,19 +59,16 @@ const InfiniteScroller = React.forwardRef<InfiniteScrollerRef, InfiniteScrollerP
|
||||
}, [inView])
|
||||
|
||||
return (<div ref={scrollContainerRef} className={`data-list-container ${props.rootClassName}`} style={props.style}>
|
||||
<Spin wrapperClassName="data-list-load-spin" spinning={props.loading} indicator={<LoadingOutlined style={{fontSize:30}} spin />}>
|
||||
<div className={`data-list-container-inner ${props.className}`}>{props.children}</div>
|
||||
{props.loading && <div style={{minHeight:'30vh'}}></div>}
|
||||
{props?.pagination && props.pagination.total > props.pagination.limit * props.pagination.page && (props.loadingPlaceholder ||
|
||||
<div className="data-load-control-element py-10 text-center">
|
||||
<div className="loading-text">加载中...</div>
|
||||
</div>)}
|
||||
{props?.empty && !props.loading && props.pagination?.total == 0 && <div className="flex justify-center text-center pt-20">
|
||||
<div className="rounded-lg px-4 py-10">
|
||||
{props?.empty && props.pagination?.total == 0 && <div className="flex justify-center text-center pt-20">
|
||||
<div className=" rounded-lg px-4 py-10">
|
||||
{props.empty}
|
||||
</div>
|
||||
</div>}
|
||||
</Spin>
|
||||
</div>);
|
||||
}) //(props: InfiniteScrollerProps) =>{}
|
||||
export default InfiniteScroller
|
@ -93,19 +93,10 @@ export const Player = React.forwardRef<PlayerInstance, Props>((props, ref) => {
|
||||
})
|
||||
setTcPlayer(() => player)
|
||||
return () => {
|
||||
// if (tcPlayer) {
|
||||
// tcPlayer.pause()
|
||||
// tcPlayer.unload()
|
||||
// }else{
|
||||
// playerVideo.pause()
|
||||
// }
|
||||
console.log('destroy video')
|
||||
try{
|
||||
Array.from(document.querySelectorAll('video')).forEach(v => v.pause())
|
||||
}catch (e){
|
||||
console.log(e)
|
||||
if (tcPlayer) {
|
||||
tcPlayer.pause()
|
||||
tcPlayer.unload()
|
||||
}
|
||||
playerVideo.parentElement?.removeChild(playerVideo)
|
||||
}
|
||||
}, [])
|
||||
React.useImperativeHandle(ref, () => {
|
||||
|
@ -1,10 +1,12 @@
|
||||
import {useSortable} from "@dnd-kit/sortable";
|
||||
import {useSetState} from "ahooks";
|
||||
import React, {useEffect} from "react";
|
||||
import {Checkbox, Popconfirm} from "antd";
|
||||
import {clsx} from "clsx";
|
||||
import {App, Checkbox, Popconfirm} from "antd";
|
||||
import {CheckCircleFilled, MenuOutlined, MinusCircleFilled, LoadingOutlined} from "@ant-design/icons";
|
||||
|
||||
import ImageCover from '@/assets/images/cover.png'
|
||||
import {IconDelete, IconEdit, IconPlaying, IconWarningCircle} from "@/components/icons";
|
||||
import {IconDelete, IconEdit, IconPlay, IconPlaying} from "@/components/icons";
|
||||
import {VideoStatus} from "@/service/api/video.ts";
|
||||
import {formatTime} from "@/util/strings.ts";
|
||||
|
||||
@ -44,7 +46,16 @@ export const VideoListItem = (
|
||||
}, [checked])
|
||||
|
||||
const generating = (type == 'create' && video.status == VideoStatus.Generating )
|
||||
|
||||
const {modal} = App.useApp()
|
||||
const handleDelete = () => {
|
||||
if(!onRemove) return;
|
||||
modal.confirm({
|
||||
title: '提示',
|
||||
centered: true,
|
||||
content: '是否要删除该视频',
|
||||
onOk: onRemove,
|
||||
})
|
||||
}
|
||||
return <div
|
||||
className={`video-item ${className}`}
|
||||
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}
|
||||
@ -85,13 +96,13 @@ export const VideoListItem = (
|
||||
className="col generated-time"
|
||||
{... (sortable && !generating?listeners:{})}
|
||||
{... (sortable && !generating?attributes:{})}
|
||||
>{video.ctime ? formatTime(video.ctime,'min') : '-'}</div>
|
||||
>{video.publish_time ? formatTime(video.publish_time) : ''}</div>
|
||||
<div className="col operation">
|
||||
{/*{sortable && !generating && (!active ?*/}
|
||||
{/* <button className="hover:text-blue-500 cursor-move">*/}
|
||||
{/* <MenuOutlined/>*/}
|
||||
{/* </button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>)}*/}
|
||||
<div className={"flex items-center gap-4"}>
|
||||
|
||||
{editable && !generating && <>
|
||||
{onEdit &&
|
||||
<button className="hover:text-blue-500" onClick={e=>{
|
||||
@ -102,15 +113,7 @@ export const VideoListItem = (
|
||||
<IconEdit/>
|
||||
</button>}
|
||||
|
||||
{onRemove && <Popconfirm
|
||||
rootClassName={'popconfirm-main'}
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={'你确定要删除吗?'}
|
||||
description={`删除后需从重新${type == 'create' ? '生成' : '推流'}`}
|
||||
onConfirm={onRemove}
|
||||
><button className="hover:text-blue-500"><IconDelete/></button></Popconfirm>}
|
||||
{onRemove && <button className="hover:text-blue-500" onClick={handleDelete}><IconDelete/></button>}
|
||||
<Checkbox checked={state.checked} onChange={() => {
|
||||
if (onCheckedChange) {
|
||||
onCheckedChange(!state.checked)
|
||||
@ -122,5 +125,4 @@ export const VideoListItem = (
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -8,6 +8,42 @@ type StoreInstance<T> = {
|
||||
add: (value: T) => void;
|
||||
remove: (value: T) => void
|
||||
}
|
||||
// function createStore<T>(set: Get<StoreApi<StoreInstance<Id>>, "setState", never>){
|
||||
// return {
|
||||
// data: [],
|
||||
// set: (values: T[]) => {
|
||||
// set(s=>{
|
||||
//
|
||||
// })
|
||||
// // set()
|
||||
// set({data: values})
|
||||
// },
|
||||
// clear: () => {
|
||||
// set({data: []})
|
||||
// },
|
||||
// add: (id: Id) => {
|
||||
// set((state) => ({
|
||||
// data: [...state.data, id]
|
||||
// }))
|
||||
// },
|
||||
// remove: (id: Id) => {
|
||||
// set((state) => ({
|
||||
// data: state.data.filter((item) => item != id)
|
||||
// }))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// export const useIndexIdCache = create<StoreInstance<Id>>((set) => {
|
||||
// //createStore<Id>(set)
|
||||
// return {
|
||||
// data: [],
|
||||
// xxx: () => {
|
||||
// set(s => {
|
||||
// s.data = [...s.data, 1]
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
export const useIndexArrayCache = create<StoreInstance<Id>>((set) => ({
|
||||
cache: [],
|
||||
set: (values: Id[]) => {
|
||||
@ -27,4 +63,74 @@ export const useIndexArrayCache = create<StoreInstance<Id>>((set) => ({
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
//
|
||||
// export const useCacheStore = create<{
|
||||
// [dataKey: string]: Id[];
|
||||
// clear: (key: string) => void;
|
||||
// set: (key: string, value: Id[]) => void;
|
||||
// add: (key: string, id: Id) => void;
|
||||
// remove: (key: string, id: Id) => void
|
||||
// }>((set) => ({
|
||||
// data: {},
|
||||
// clear: (key: string) => {
|
||||
// set(s => {
|
||||
// s[key] = []
|
||||
// return s;
|
||||
// })
|
||||
// },
|
||||
// set: (key: string, value: Id[]) => {
|
||||
// set((s) => {
|
||||
// s[key] = value
|
||||
// return s;
|
||||
// })
|
||||
// },
|
||||
// add: (key: string, id: Id) => {
|
||||
// console.log(id, 'add cache', key)
|
||||
// set((s) => {
|
||||
// if (!s[key]) {
|
||||
// s[key] = [];
|
||||
// }
|
||||
// s[key].push(id)
|
||||
// return s;
|
||||
// })
|
||||
// },
|
||||
// remove: (key: string, id: Id) => {
|
||||
// set((s) => {
|
||||
// if (!s[key]) {
|
||||
// return s;
|
||||
// }
|
||||
// s[key] = s[key].filter((item) => item != id)
|
||||
// return s;
|
||||
// })
|
||||
// }
|
||||
// }))
|
||||
//
|
||||
// function useCache(key: 'index' | 'edit') {
|
||||
// //{data,set,add,remove,clear}
|
||||
// const store = useCacheStore()
|
||||
//
|
||||
// return {
|
||||
// store,
|
||||
// data: store[key],
|
||||
// getCache: () => {
|
||||
// // return cache.data[key] || []
|
||||
// return store[key] || [];
|
||||
// },
|
||||
// setCache: (value: Id[]) => {
|
||||
// store.set(key, value)
|
||||
// },
|
||||
// clearCache: () => {
|
||||
// store.clear(key)
|
||||
// },
|
||||
// addCache: (id: Id, addSame = false) => {
|
||||
// if (!addSame && store[key]?.includes(id)) return;
|
||||
// console.log(id, 'add cache')
|
||||
// store.add(key, id)
|
||||
// },
|
||||
// removeCache: (id: Id) => {
|
||||
// store.remove(key, id)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// export default useCache
|
@ -33,8 +33,7 @@ export default function LiveIndex() {
|
||||
showToTop: false,
|
||||
checkedAll: false,
|
||||
originSort: '',
|
||||
playProgress: 0,
|
||||
loading:false
|
||||
playProgress: 0
|
||||
})
|
||||
const activeIndex = useRef(state.activeIndex)
|
||||
useEffect(() => {
|
||||
@ -112,7 +111,6 @@ export default function LiveIndex() {
|
||||
|
||||
const loadList = () => {
|
||||
clearAllTimer();
|
||||
setState({loading: true})
|
||||
getList().then(res => {
|
||||
// console.log('origin list', res.list.map(s => s.id))
|
||||
setVideoData(() => (res.list || []))
|
||||
@ -120,22 +118,13 @@ export default function LiveIndex() {
|
||||
originSort: res.list ? res.list.map(s => s.id).join(',') : ''
|
||||
})
|
||||
setCheckedIdArray([])
|
||||
}).catch(showErrorToast).finally(()=>{
|
||||
setState({loading: false})
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(initPlayingState, [videoData])
|
||||
useEffect(() => {
|
||||
loadList()
|
||||
return ()=>{
|
||||
clearAllTimer();
|
||||
try{
|
||||
Array.from(document.querySelectorAll('video')).forEach(v => v.pause())
|
||||
}catch (e){
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
return clearAllTimer;
|
||||
}, [])
|
||||
|
||||
const processDeleteVideo = async (ids: Id[]) => {
|
||||
@ -202,14 +191,14 @@ export default function LiveIndex() {
|
||||
return checkedIdArray.filter(id => currentId.id != id)
|
||||
}, [checkedIdArray, state.activeIndex])
|
||||
|
||||
return (<div className="container py-5 page-live">
|
||||
return (<div className="container py-10 page-live">
|
||||
{contextHolder}
|
||||
<div className="h-[36px]"></div>
|
||||
<div className="flex">
|
||||
<div className="video-player-container mr-16 flex items-center">
|
||||
<div>
|
||||
<div className="text-center text-base text-gray-400">直播界面</div>
|
||||
<div className="video-player flex justify-center flex-1 mt-1">
|
||||
<div className="video-player-container mr-8 flex flex-col">
|
||||
<div className="text-center text-base">
|
||||
<span>视频时长: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)}</span>
|
||||
</div>
|
||||
<div className="video-player flex justify-center flex-1 mt-5">
|
||||
<div className="live-player relative rounded overflow-hidden w-[360px] h-[636px]"
|
||||
style={{backgroundColor: 'hsl(210, 100%, 48%)'}}>
|
||||
<Player
|
||||
@ -221,14 +210,10 @@ export default function LiveIndex() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center text-sm mt-4 text-gray-400">
|
||||
<span>视频时长: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="video-list-container video-list-sort-container flex-1 mt-1">
|
||||
<div className="live-control flex justify-between mb-1">
|
||||
<div className="video-list-container video-list-sort-container flex-1">
|
||||
<div className="live-control flex justify-between mb-4">
|
||||
<div className="text-sm">
|
||||
<span>当前{state.activeIndex == -1 ? '暂未播放' : `播放到${state.activeIndex}条`},</span>
|
||||
<span>共{videoData.length}条</span>
|
||||
@ -265,7 +250,6 @@ export default function LiveIndex() {
|
||||
<div className="live-video-list-sort-container ">
|
||||
<InfiniteScroller
|
||||
ref={scrollerRef}
|
||||
loading={state.loading}
|
||||
onScroll={top => setState({showToTop: top > 30})}
|
||||
onCallback={() => {
|
||||
}}
|
||||
@ -290,7 +274,6 @@ export default function LiveIndex() {
|
||||
id={v.id}
|
||||
key={index}
|
||||
active={state.activeIndex == index}
|
||||
playing={state.activeIndex == index}
|
||||
className={`list-item-${index} mt-3 mb-2`}
|
||||
checked={checkedIdArray.includes(v.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
|
@ -1,20 +1,18 @@
|
||||
import {Input} from "antd";
|
||||
import {SearchOutlined} from "@ant-design/icons";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import React, {useState} from "react";
|
||||
import {useSetState} from "ahooks";
|
||||
import useArticleTags from "@/hooks/useArticleTags.ts";
|
||||
import TagSelect from "@/components/form/tag-select.tsx";
|
||||
|
||||
export default function EditSearchForm(props: {
|
||||
onSubmit: (values: ApiArticleSearchParams) => void;
|
||||
defaultParams?: Partial<ApiArticleSearchParams>;
|
||||
}) {
|
||||
const articleTags = useArticleTags()
|
||||
const [tags, _setTags] = useState<Id[][]>([]);
|
||||
const [prevSearchName, setPrevSearchName] = useState<string>(props.defaultParams?.title||'')
|
||||
const [prevSearchName, setPrevSearchName] = useState<string>()
|
||||
const [params, setParams] = useSetState<ApiArticleSearchParams>({
|
||||
pagination: {limit: 10, page: 1},
|
||||
title:props.defaultParams?.title||''
|
||||
});
|
||||
|
||||
const handleSubmit = (_tags?:Id[][],from?:'input') => {
|
||||
@ -42,20 +40,6 @@ export default function EditSearchForm(props: {
|
||||
}
|
||||
})
|
||||
}
|
||||
useEffect(()=>{
|
||||
const {defaultParams} = props;
|
||||
if(!defaultParams){
|
||||
return;
|
||||
}
|
||||
const tags = []
|
||||
|
||||
if(defaultParams.tags){
|
||||
defaultParams.tags.forEach(it=>{
|
||||
tags.push([it.level1, it.level2])
|
||||
})
|
||||
_setTags(tags)
|
||||
}
|
||||
},[articleTags])
|
||||
const setTags = (_tags: Id[][])=>{
|
||||
console.log(_tags)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Input} from "antd";
|
||||
import {useBoolean, useLocalStorageState, useSetState,useClickAway} from "ahooks";
|
||||
import {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {useCallback, useMemo, useRef, useState} from "react";
|
||||
import {clsx} from "clsx";
|
||||
import useArticleTags from "@/hooks/useArticleTags.ts";
|
||||
|
||||
@ -12,7 +12,6 @@ import {IconPin} from "@/components/icons";
|
||||
|
||||
type SearchPanelProps = {
|
||||
onSearch?: (params: ApiArticleSearchParams) => void;
|
||||
defaultParams?: Partial<ApiArticleSearchParams>;
|
||||
}
|
||||
const pagination = {
|
||||
limit: 12, page: 1
|
||||
@ -22,45 +21,19 @@ const DEFAULT_STATE = {
|
||||
tag_level_2_id: -1,
|
||||
subOptions: []
|
||||
}
|
||||
export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) {
|
||||
export default function SearchPanel({onSearch}: SearchPanelProps) {
|
||||
const tags = useArticleTags();
|
||||
const [params, setParams] = useSetState<ApiArticleSearchParams>({
|
||||
pagination,
|
||||
time_flag:1,
|
||||
...(defaultParams || {})
|
||||
time_flag:1
|
||||
});
|
||||
const [prevSearchName, setPrevSearchName] = useState<string>(defaultParams?.title||'')
|
||||
const [prevSearchName, setPrevSearchName] = useState<string>()
|
||||
|
||||
const [state, setState] = useSetState<{
|
||||
tag_level_1_id: number;
|
||||
tag_level_2_id: number;
|
||||
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}: {})
|
||||
})
|
||||
useEffect(()=>{
|
||||
if(!defaultParams){
|
||||
return;
|
||||
}
|
||||
const _state = {
|
||||
tag_level_1_id: -1,
|
||||
tag_level_2_id: -1,
|
||||
}
|
||||
|
||||
if(defaultParams.tag_level_1_id){
|
||||
_state.tag_level_1_id = defaultParams.tag_level_1_id
|
||||
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){
|
||||
_state.tag_level_2_id = defaultParams.tag_level_2_id
|
||||
}
|
||||
setState(_state)
|
||||
},[tags])
|
||||
}>({...DEFAULT_STATE})
|
||||
const [pinnedTag, setPinnedTag] = useLocalStorageState<number[]>(
|
||||
'user-pinned-tag-list',
|
||||
{
|
||||
@ -201,10 +174,9 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
</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`}
|
||||
tags.filter(s => s.value !== 999999).map(it => (
|
||||
<div
|
||||
className={`filter-item border flex items-center px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded`}
|
||||
key={it.value}
|
||||
onClick={() => {
|
||||
const value = Number(it.value)
|
||||
@ -215,11 +187,10 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
}
|
||||
}}>
|
||||
<span>{it.label}</span>
|
||||
{currentPinned &&
|
||||
{pinnedTag?.includes(Number(it.value)) &&
|
||||
<span className={'ml-2'}><IconPin/></span>}
|
||||
</div>)
|
||||
})
|
||||
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -231,7 +202,7 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
|
||||
{
|
||||
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'}`}
|
||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded text-gray-400 ${state.tag_level_2_id == it.value ? 'text-black' : '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)})
|
||||
|
@ -78,9 +78,8 @@
|
||||
.header{
|
||||
@apply bg-primary-bg;
|
||||
.col{
|
||||
@apply text-sm;
|
||||
height: 42px;
|
||||
|
||||
height: 42px;
|
||||
}
|
||||
.operations{
|
||||
}
|
||||
|
@ -10,15 +10,13 @@ import ButtonPush2Video from "@/pages/news/components/button-push2video.tsx";
|
||||
|
||||
import styles from './components/style.module.scss'
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import {IconDelete, IconWarningCircle} from "@/components/icons";
|
||||
import {IconDelete, IconEdit} from "@/components/icons";
|
||||
import {clsx} from "clsx";
|
||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import ButtonDeleteBatch from "@/pages/news/components/button-delete-batch.tsx";
|
||||
import {showErrorToast, showToast} from "@/components/message.ts";
|
||||
|
||||
const FilterCache: Partial<ApiArticleSearchParams> = {
|
||||
tags: [],
|
||||
}
|
||||
|
||||
export default function NewEdit() {
|
||||
const [state, setState] = useState<{
|
||||
checkAll?: boolean;
|
||||
@ -28,15 +26,12 @@ export default function NewEdit() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<Id[]>([])
|
||||
|
||||
const [params, setParams] = useState<ApiArticleSearchParams>({
|
||||
pagination: {page: 1, limit: 10},
|
||||
...FilterCache
|
||||
pagination: {page: 1, limit: 10}
|
||||
})
|
||||
const [data, setData] = useState<DataList<ListArticleItem>>()
|
||||
const {refresh, loading} = useRequest(() => getList(params), {
|
||||
refreshDeps: [params],
|
||||
onSuccess: (data) => {
|
||||
FilterCache.title = params.title;
|
||||
FilterCache.tags = params.tags;
|
||||
setData(prev => {
|
||||
// 判断页码是否是第1页
|
||||
if (data.pagination.page == 1) return data;
|
||||
@ -63,18 +58,18 @@ export default function NewEdit() {
|
||||
setSelectedRowKeys([])
|
||||
}
|
||||
}
|
||||
const scrollerRef = useRef<InfiniteScrollerRef | null>(null)
|
||||
const handleDelete = (id) => {
|
||||
deleteByIds([id]).then(() => {
|
||||
const scrollerRef = useRef<InfiniteScrollerRef|null>(null)
|
||||
const handleDelete = (id)=>{
|
||||
deleteByIds([id]).then(()=>{
|
||||
refresh()
|
||||
showToast('删除成功', 'success')
|
||||
showToast('删除成功','success')
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
|
||||
return (<div className="container pb-5 news-edit">
|
||||
<div className="search-panel-container my-5">
|
||||
<div className="search-form flex pt-1 gap-5 justify-between">
|
||||
<EditSearchForm defaultParams={params} onSubmit={setParams}/>
|
||||
<EditSearchForm onSubmit={setParams}/>
|
||||
{/*<Button type="primary" onClick={() => setEditId(0)}>手动新增</Button>*/}
|
||||
</div>
|
||||
|
||||
@ -88,10 +83,9 @@ export default function NewEdit() {
|
||||
<span className={'inline-block cursor-pointer mr-2'} onClick={() => {
|
||||
handleCheckAll(!state.checkAll)
|
||||
}}>全选</span>
|
||||
<Checkbox checked={state.checkAll && selectedRowKeys.length == data?.list.length}
|
||||
onChange={e => {
|
||||
<Checkbox checked={state.checkAll && selectedRowKeys.length == data?.list.length} onChange={e => {
|
||||
handleCheckAll(e.target.checked)
|
||||
}}/>
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.newListTable}>
|
||||
@ -108,8 +102,7 @@ export default function NewEdit() {
|
||||
...prev,
|
||||
pagination: {page, limit: 10}
|
||||
}))
|
||||
}} onScroll={(top) => setState({showToTop: top > 30})} loading={loading}
|
||||
pagination={data?.pagination}>
|
||||
}} onScroll={(top)=> setState({showToTop: top > 30})} loading={loading} pagination={data?.pagination}>
|
||||
<div className="body">
|
||||
{data?.list?.map((item, i) => {
|
||||
const checked = selectedRowKeys.includes(item.id)
|
||||
@ -136,17 +129,9 @@ export default function NewEdit() {
|
||||
</div>
|
||||
<div className="col operations">
|
||||
{/*<span className="icon-btn"><IconEdit/></span>*/}
|
||||
<Popconfirm
|
||||
rootClassName={'popconfirm-main'}
|
||||
placement={'left'}
|
||||
arrow={false}
|
||||
icon={<IconWarningCircle/>}
|
||||
title={'你确定要删除吗?'}
|
||||
description={'删除后需从新闻素材中重新选择'}
|
||||
onConfirm={() => {
|
||||
<Popconfirm title={'确认删除此新闻吗?'} description={'删除后需从新闻素材中重新选择'} onConfirm={()=>{
|
||||
handleDelete(item.id)
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<span className="icon-btn"><IconDelete/></span>
|
||||
</Popconfirm>
|
||||
<Checkbox checked={checked}
|
||||
@ -159,8 +144,8 @@ export default function NewEdit() {
|
||||
</div>
|
||||
|
||||
<div className="page-action">
|
||||
<ButtonToTop visible={state.showToTop} onClick={() => scrollerRef.current?.scrollToPosition(0)}/>
|
||||
{selectedRowKeys?.length > 0 && <ButtonDeleteBatch ids={selectedRowKeys} onSuccess={refresh}/>}
|
||||
<ButtonToTop visible={state.showToTop} onClick={()=>scrollerRef.current?.scrollToPosition(0)} />
|
||||
{selectedRowKeys?.length >0 && <ButtonDeleteBatch ids={selectedRowKeys} onSuccess={refresh}/>}
|
||||
<ButtonPush2Video ids={selectedRowKeys} onSuccess={refresh}/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,19 +12,15 @@ import ButtonNewsDownload from "@/pages/news/components/button-news-download.tsx
|
||||
import {clsx} from "clsx";
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import {useIndexArrayCache} from "@/hooks/useCache.ts";
|
||||
import { useIndexArrayCache} from "@/hooks/useCache.ts";
|
||||
|
||||
const FilterCache: Partial<ApiArticleSearchParams> = {
|
||||
time_flag: 1,
|
||||
}
|
||||
export default function NewsIndex() {
|
||||
const [params, setParams] = useState<ApiArticleSearchParams>({
|
||||
pagination: {page: 1, limit: 12},
|
||||
...FilterCache
|
||||
})
|
||||
|
||||
const [params, setParams] = useState<ApiArticleSearchParams>({
|
||||
pagination: {page: 1, limit: 12},time_flag:1
|
||||
})
|
||||
// const [checkedId, setCheckedId] = useState<Id[]>([])
|
||||
const {cache: checkedId, set: setCheckedId} = useIndexArrayCache()
|
||||
const {cache:checkedId,set:setCheckedId} = useIndexArrayCache()
|
||||
|
||||
const [activeNews, setActiveNews] = useState<NewsInfo>()
|
||||
|
||||
@ -37,11 +33,6 @@ export default function NewsIndex() {
|
||||
const {loading} = useRequest(() => getList(params), {
|
||||
refreshDeps: [params],
|
||||
onSuccess: (_data) => {
|
||||
FilterCache.tag_level_1_id = params.tag_level_1_id;
|
||||
FilterCache.tag_level_2_id = params.tag_level_2_id;
|
||||
FilterCache.title = params.title;
|
||||
FilterCache.time_flag = params.time_flag;
|
||||
console.log('success',FilterCache)
|
||||
if (params.pagination.page === 1) {
|
||||
setData(_data)
|
||||
setState({checkAll: checkedId && _data.list && checkedId.length === _data.list.length})
|
||||
@ -64,12 +55,12 @@ export default function NewsIndex() {
|
||||
})
|
||||
}
|
||||
|
||||
const currentEnabledList = useMemo(() => {
|
||||
if (data?.list && data?.list?.length > 0) {
|
||||
return data.list.filter(s => s.internal_article_id == 0)
|
||||
const currentEnabledList = useMemo(()=>{
|
||||
if(data?.list && data?.list?.length > 0){
|
||||
return data.list.filter(s=>s.internal_article_id == 0)
|
||||
}
|
||||
return [];
|
||||
}, [data?.list])
|
||||
},[data?.list])
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
setState({checkAll: checked})
|
||||
if (checked) {
|
||||
@ -87,7 +78,7 @@ export default function NewsIndex() {
|
||||
}
|
||||
}
|
||||
return (<div className={'container pb-5'}>
|
||||
<SearchPanel defaultParams={params} onSearch={setParams}/>
|
||||
<SearchPanel onSearch={setParams}/>
|
||||
{activeNews && <Modal
|
||||
rootClassName={'news-detail-modal'}
|
||||
closeIcon={null} open={true} width={1000}
|
||||
@ -171,7 +162,7 @@ export default function NewsIndex() {
|
||||
<div className="info text-gray-400 mt-4 text-sm">
|
||||
<div className="line-clamp-1">来源: <span>{item.data_source_name}</span></div>
|
||||
<div className="extras flex items-center justify-between gap-3">
|
||||
<div><span>{formatTime(item.publish_time, 'min')}</span></div>
|
||||
<div><span>{formatTime(item.publish_time,'min')}</span></div>
|
||||
<div><span>图片数: {item.img_num}</span></div>
|
||||
<div><span>字数: {item.content_word_count}</span></div>
|
||||
<div
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Checkbox, Empty, Space} from "antd";
|
||||
import {Checkbox, Empty} from "antd";
|
||||
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {DndContext} from "@dnd-kit/core";
|
||||
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
|
||||
@ -16,6 +16,7 @@ import ButtonToTop from "@/components/scoller/button-to-top.tsx";
|
||||
import InfiniteScroller, {InfiniteScrollerRef} from "@/components/scoller/infinite-scroller.tsx";
|
||||
import {IconDelete} from "@/components/icons";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import {playState} from "@/service/api/live.ts";
|
||||
|
||||
export default function VideoIndex() {
|
||||
const [editId, setEditId] = useState(-1)
|
||||
@ -31,14 +32,12 @@ export default function VideoIndex() {
|
||||
playState: {
|
||||
current: -1,
|
||||
total: -1
|
||||
},
|
||||
loading:false
|
||||
}
|
||||
})
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<Id[]>([])
|
||||
|
||||
// 加载列表
|
||||
const loadList = (needReset = true) => {
|
||||
setState({loading: true})
|
||||
getList().then((ret) => {
|
||||
const list = ret.list || []
|
||||
setVideoData(list)
|
||||
@ -51,9 +50,6 @@ export default function VideoIndex() {
|
||||
// 每5s重新获取一次最新数据
|
||||
setTimeout(() => loadList(false), 5000)
|
||||
}
|
||||
}).catch(showErrorToast)
|
||||
.finally(()=>{
|
||||
setState({loading: false})
|
||||
})
|
||||
}
|
||||
|
||||
@ -88,14 +84,6 @@ export default function VideoIndex() {
|
||||
loadList();
|
||||
showToast('调整视频顺序失败,请重试!', 'warning')
|
||||
})
|
||||
|
||||
return ()=>{
|
||||
try{
|
||||
Array.from(document.querySelectorAll('video')).forEach(v => v.pause())
|
||||
}catch (e){
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
useEffect(loadList, [])
|
||||
@ -124,14 +112,12 @@ export default function VideoIndex() {
|
||||
}).catch(showErrorToast)
|
||||
}
|
||||
|
||||
return (<div className="container py-5 page-live">
|
||||
<div className="h-[36px]"></div>
|
||||
return (<div className="container py-10 page-live">
|
||||
<div className="flex">
|
||||
<div className="video-player-container mr-16 w-[360px] flex items-center">
|
||||
<div>
|
||||
<div className="video-player-container mr-16 w-[360px] flex flex-col">
|
||||
<div className="text-center text-base text-gray-400">预览视频 - 点击视频列表播放</div>
|
||||
<div className="video-player flex items-center mt-2">
|
||||
<div className=" w-[360px] h-[636px] rounded overflow-hidden">
|
||||
<div className=" w-[360px] h-[630px] rounded overflow-hidden">
|
||||
<Player
|
||||
ref={player} url={videoData[state.playingIndex]?.oss_video_url}
|
||||
onChange={(state) => {
|
||||
@ -151,16 +137,11 @@ export default function VideoIndex() {
|
||||
</div>
|
||||
<div className="text-center text-sm mt-4 text-gray-400">{formatDuration(state.playState.current)} / {formatDuration(state.playState.total)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="video-list-container rounded mt-2 flex flex-col flex-1">
|
||||
<div className="video-list-container rounded flex flex-col flex-1">
|
||||
<div className="live-control flex justify-between">
|
||||
<div className="pl-[70px]"></div>
|
||||
<div className="flex items-center">
|
||||
<Space>
|
||||
<span>总共 {videoData.length || 0} 条</span>
|
||||
<span className={'text-blue-500'}>已选 {checkedIdArray.length} 条</span>
|
||||
</Space>
|
||||
<button className="hover:text-blue-300 text-gray-400 ml-2"
|
||||
<div className="flex items-center pr-[10px]">
|
||||
<button className="hover:text-blue-300 text-gray-400 text-lg"
|
||||
onClick={handleAllCheckedChange}>
|
||||
<span className="text-sm mr-2">全选</span>
|
||||
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
|
||||
@ -168,7 +149,7 @@ export default function VideoIndex() {
|
||||
<Checkbox checked={state.checkedAll} onChange={() => handleAllCheckedChange()}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'video-list-sort-container flex-1 mt-1'}>
|
||||
<div className={'video-list-sort-container flex-1'}>
|
||||
<div className="list-header">
|
||||
<div className="list-row header-row">
|
||||
<div className="col number">No.</div>
|
||||
@ -178,7 +159,7 @@ export default function VideoIndex() {
|
||||
<div className="col operation"></div>
|
||||
</div>
|
||||
</div>
|
||||
<InfiniteScroller loading={state.loading} ref={scrollerRef} onScroll={top => setState({showToTop: top > 30})}>
|
||||
<InfiniteScroller ref={scrollerRef} onScroll={top => setState({showToTop: top > 30})}>
|
||||
{
|
||||
videoData.length == 0 ? <div className="m-auto"><Empty/></div> :
|
||||
<div className="sort-list-container flex-1">
|
||||
|
2
src/types/api.d.ts
vendored
2
src/types/api.d.ts
vendored
@ -98,7 +98,6 @@ declare interface VideoInfo {
|
||||
article_id: number;
|
||||
status: number;
|
||||
publish_time?: number|string;
|
||||
ctime?: number|string;
|
||||
}
|
||||
// room live
|
||||
declare interface LiveVideoInfo {
|
||||
@ -112,7 +111,6 @@ declare interface LiveVideoInfo {
|
||||
status: number;
|
||||
order_no: string;
|
||||
publish_time?: number|string;
|
||||
ctime?: number|string;
|
||||
}
|
||||
|
||||
declare interface LiveState{
|
||||
|
@ -11,11 +11,6 @@ const themeConfig = {
|
||||
'active': '#FFE0E0',
|
||||
'primary-red':'#F5222D',
|
||||
'primary-red-70':'rgba(245,34,45,0.7)',
|
||||
|
||||
'popconfirm-bg':'#ff5C5C',
|
||||
'popconfirm-btn-primary-hover':'#f15656',
|
||||
'popconfirm-btn-cancel':'#818181',
|
||||
'popconfirm-btn-cancel-hover':'rgba(71, 71, 71, 1)',
|
||||
},
|
||||
widths:{
|
||||
'chat-avatar-size': '32px',
|
||||
@ -53,9 +48,6 @@ export default {
|
||||
},
|
||||
backgroundColor: {
|
||||
...themeConfig.colors,
|
||||
},
|
||||
textColor: {
|
||||
...themeConfig.colors,
|
||||
}
|
||||
},
|
||||
screens: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user