update
This commit is contained in:
parent
25de13eb85
commit
17903f5486
@ -13,6 +13,7 @@
|
|||||||
--app-header-header: 90px;
|
--app-header-header: 90px;
|
||||||
--container-width: 1440px;
|
--container-width: 1440px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@ -31,66 +32,81 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.card{
|
|
||||||
|
.card {
|
||||||
@apply bg-white rounded-lg p-5 my-10;
|
@apply bg-white rounded-lg p-5 my-10;
|
||||||
}
|
}
|
||||||
.radio-icon,.checkbox-icon{
|
|
||||||
|
.radio-icon, .checkbox-icon {
|
||||||
@apply w-4 h-4 mr-2 border border-gray-400 rounded-2xl inline-block flex items-center justify-center relative;
|
@apply w-4 h-4 mr-2 border border-gray-400 rounded-2xl inline-block flex items-center justify-center relative;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
&:after{
|
|
||||||
|
&:after {
|
||||||
@apply inline-block bg-blue-500 w-full h-full;
|
@apply inline-block bg-blue-500 w-full h-full;
|
||||||
content: ' ';
|
content: ' ';
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.checkbox-icon{
|
|
||||||
|
.checkbox-icon {
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
&:before{
|
|
||||||
|
&:before {
|
||||||
content: ' ';
|
content: ' ';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-left: solid 2px #fff;
|
border-left: solid 2px #fff;
|
||||||
border-bottom: solid 2px #fff;
|
border-bottom: solid 2px #fff;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: rotate(-45deg) translateY(-1px) translateX(1px);
|
transform: rotate(-45deg) translateY(-1px) translateX(1px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
&:after{
|
|
||||||
|
&:after {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-select-item-option {
|
.ant-select-item-option {
|
||||||
&.ant-select-item-option-selected{
|
&.ant-select-item-option-selected {
|
||||||
.radio-icon,.checkbox-icon{
|
.radio-icon, .checkbox-icon {
|
||||||
@apply border-blue-500;
|
@apply border-blue-500;
|
||||||
&:after,&:before {
|
&:after, &:before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.select-hide-checked{
|
|
||||||
.ant-select-item-option-selected{
|
.select-hide-checked {
|
||||||
.ant-select-item-option-state{
|
.ant-select-item-option-selected {
|
||||||
opacity: 0 !important;
|
.ant-select-item-option-state {
|
||||||
|
opacity: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.select-no-wrap{
|
|
||||||
.ant-select-selector{
|
.select-no-wrap {
|
||||||
.ant-select-selection-overflow{
|
.ant-select-selector {
|
||||||
|
.ant-select-selection-overflow {
|
||||||
@apply flex flex-nowrap;
|
@apply flex flex-nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.simple-pagination{
|
|
||||||
.ant-pagination-simple-pager{
|
.simple-pagination {
|
||||||
input[type=text]{
|
.ant-pagination-simple-pager {
|
||||||
|
input[type=text] {
|
||||||
width: auto;
|
width: auto;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item-shadow {
|
||||||
|
box-shadow: 0 0 6px 0 var(--tw-shadow-color);
|
||||||
|
//filter: drop-shadow(0 0 6px var(--tw-shadow-color));
|
||||||
}
|
}
|
@ -37,12 +37,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.uploadImage {
|
.uploadImage {
|
||||||
@apply flex justify-center items-center cursor-pointer;
|
@apply flex justify-center items-center relative;
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
}
|
}
|
||||||
|
.uploadTips{
|
||||||
|
@apply absolute inset-0 cursor-pointer opacity-0 rounded flex items-center justify-center bg-black/50 text-white;
|
||||||
|
}
|
||||||
|
.imagePlaceholder{
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
&:hover{
|
||||||
|
.uploadTips{
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
|
63
src/components/article/edit-modal.tsx
Normal file
63
src/components/article/edit-modal.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import {Input, Modal} from "antd";
|
||||||
|
import ArticleGroup from "@/components/article/group.tsx";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {useSetState} from "ahooks";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string;
|
||||||
|
groups?: ArticleContentGroup[];
|
||||||
|
onSave?: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArticleEditModal(props: Props) {
|
||||||
|
|
||||||
|
const [groups, setGroups] = useState<ArticleContentGroup[]>([]);
|
||||||
|
const [title, setTitle] = useState(props.title)
|
||||||
|
const [state, setState] = useSetState({
|
||||||
|
loading: false,
|
||||||
|
open: false
|
||||||
|
})
|
||||||
|
const handleSave = () => {
|
||||||
|
setState({loading: true})
|
||||||
|
props.onSave?.().finally(() => {
|
||||||
|
setState({loading: false,open: false})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
setState({open: typeof(props.title) != "undefined"})
|
||||||
|
setGroups(props.groups || [])
|
||||||
|
setTitle(props.title||'')
|
||||||
|
}, [props.title,props.groups])
|
||||||
|
|
||||||
|
return (<Modal
|
||||||
|
title={'编辑文章'}
|
||||||
|
open={state.open}
|
||||||
|
maskClosable={false}
|
||||||
|
keyboard={false}
|
||||||
|
width={800}
|
||||||
|
onCancel={()=>setState({open: false})}
|
||||||
|
okButtonProps={{loading: state.loading}}
|
||||||
|
coOk={handleSave}
|
||||||
|
>
|
||||||
|
<div className="article-title mt-5">
|
||||||
|
<div className="title">
|
||||||
|
<span className="text text-base">标题</span>
|
||||||
|
<span className="require ml-1 font-bold text-red-500">*</span>
|
||||||
|
</div>
|
||||||
|
<div className="box mt-1">
|
||||||
|
<Input value={title} onChange={e => {
|
||||||
|
setTitle(e.target.value)
|
||||||
|
}} placeholder={'请输入文章标题'}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="aricle-body mt-2">
|
||||||
|
<div className="title">
|
||||||
|
<span className="text text-base">正文</span>
|
||||||
|
<span className="require ml-1 font-bold text-red-500">*</span>
|
||||||
|
</div>
|
||||||
|
<div className="box mt-1">
|
||||||
|
<ArticleGroup editable groups={groups} onChange={list => setGroups(() => list)}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from './article.module.scss'
|
import styles from './article.module.scss'
|
||||||
import {Input, Upload} from "antd";
|
import {Button, Input, Upload} from "antd";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@ -15,7 +15,14 @@ export function BlockImage({data,editable}: Props) {
|
|||||||
{editable ? <div>
|
{editable ? <div>
|
||||||
<Upload accept="image/*">
|
<Upload accept="image/*">
|
||||||
<div className={styles.uploadImage} >
|
<div className={styles.uploadImage} >
|
||||||
<img src={data.content}/>
|
{ data.content ? <>
|
||||||
|
<img src={data.content}/>
|
||||||
|
<div className={styles.uploadTips}>
|
||||||
|
<span>编辑</span>
|
||||||
|
</div>
|
||||||
|
</> : <div className={styles.imagePlaceholder}>
|
||||||
|
<Button>选择图片</Button>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</Upload>
|
</Upload>
|
||||||
</div> : <div className={styles.uploadImage}><img src={data.content}/></div>}
|
</div> : <div className={styles.uploadImage}><img src={data.content}/></div>}
|
||||||
|
@ -491,4 +491,13 @@ export const IconLive = ({style,className}: { style?: React.CSSProperties;classN
|
|||||||
d="M772.437333 97.52381l51.712 51.712-126.342095 126.342095H828.952381a73.142857 73.142857 0 0 1 73.142857 73.142857v487.619048a73.142857 73.142857 0 0 1-73.142857 73.142857H195.047619a73.142857 73.142857 0 0 1-73.142857-73.142857v-487.619048a73.142857 73.142857 0 0 1 73.142857-73.142857h131.120762L199.850667 149.23581 251.562667 97.52381l178.054095 178.054095h164.742095L772.437333 97.52381zM828.952381 348.720762H195.047619v487.619048h633.904762v-487.619048z m-280.380952 73.142857v341.333333h-73.142858v-341.333333h73.142858z m-134.095239 73.142857v195.047619h-73.142857v-195.047619h73.142857z m268.190477 24.380953v146.285714h-73.142857v-146.285714h73.142857z"
|
d="M772.437333 97.52381l51.712 51.712-126.342095 126.342095H828.952381a73.142857 73.142857 0 0 1 73.142857 73.142857v487.619048a73.142857 73.142857 0 0 1-73.142857 73.142857H195.047619a73.142857 73.142857 0 0 1-73.142857-73.142857v-487.619048a73.142857 73.142857 0 0 1 73.142857-73.142857h131.120762L199.850667 149.23581 251.562667 97.52381l178.054095 178.054095h164.742095L772.437333 97.52381zM828.952381 348.720762H195.047619v487.619048h633.904762v-487.619048z m-280.380952 73.142857v341.333333h-73.142858v-341.333333h73.142858z m-134.095239 73.142857v195.047619h-73.142857v-195.047619h73.142857z m268.190477 24.380953v146.285714h-73.142857v-146.285714h73.142857z"
|
||||||
fill="currentColor"/>
|
fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const IconEdit = ({style,className}: { style?: React.CSSProperties;className?:string; }) => (
|
||||||
|
<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">
|
||||||
|
<path
|
||||||
|
d="M804.6 689.8l64-64c10-10 27.4-3 27.4 11.4V928c0 53-43 96-96 96H96c-53 0-96-43-96-96V224c0-53 43-96 96-96h547c14.2 0 21.4 17.2 11.4 27.4l-64 64c-3 3-7 4.6-11.4 4.6H96v704h704V701c0-4.2 1.6-8.2 4.6-11.2z m313.2-403.6L592.6 811.4l-180.8 20c-52.4 5.8-97-38.4-91.2-91.2l20-180.8L865.8 34.2c45.8-45.8 119.8-45.8 165.4 0l86.4 86.4c45.8 45.8 45.8 120 0.2 165.6zM920.2 348L804 231.8 432.4 603.6l-14.6 130.6 130.6-14.6L920.2 348z m129.6-159.4l-86.4-86.4c-8.2-8.2-21.6-8.2-29.6 0L872 164l116.2 116.2 61.8-61.8c8-8.4 8-21.6-0.2-29.8z"
|
||||||
|
fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
)
|
)
|
@ -1,10 +1,19 @@
|
|||||||
import ReactPlayer from 'react-player'
|
import ReactPlayer from 'react-player'
|
||||||
import { useSetState } from "ahooks";
|
|
||||||
import { PauseOutlined, PlayCircleOutlined, FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons"
|
import { PauseOutlined, PlayCircleOutlined, FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons"
|
||||||
import { Progress } from "antd";
|
import { Progress } from "antd";
|
||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
playing: boolean
|
||||||
|
muted: boolean
|
||||||
|
fullscreen: boolean
|
||||||
|
progress: number
|
||||||
|
playedSeconds: number
|
||||||
|
duration: number
|
||||||
|
}
|
||||||
|
type StateUpdate = Partial<State> | ((prev: State) => Partial<State>)
|
||||||
export function Player({ url, cover, simple, showControls }: { url: string; cover?: string; simple?: boolean; showControls?: boolean }) {
|
export function Player({ url, cover, simple, showControls }: { url: string; cover?: string; simple?: boolean; showControls?: boolean }) {
|
||||||
const [state, setState] = useSetState({
|
const [state, _setState] = useState<State>({
|
||||||
playing: false,
|
playing: false,
|
||||||
muted: false,
|
muted: false,
|
||||||
// 是否全屏
|
// 是否全屏
|
||||||
@ -13,9 +22,15 @@ export function Player({ url, cover, simple, showControls }: { url: string; cove
|
|||||||
playedSeconds: 0,
|
playedSeconds: 0,
|
||||||
duration: 0
|
duration: 0
|
||||||
})
|
})
|
||||||
|
const setState = (data: StateUpdate) => {
|
||||||
|
_setState(prev => {
|
||||||
|
if (typeof(data) === 'function') return { ...prev, ...data(prev) }
|
||||||
|
return { ...prev, ...data }
|
||||||
|
})
|
||||||
|
}
|
||||||
return <div className="video-player">
|
return <div className="video-player">
|
||||||
{simple ? <div>
|
{simple ? <div>
|
||||||
<video src={url} poster={cover} controls={showControls}></video>
|
<video style={{width:400,height:400}} preload={'metadata'} src={url} poster={cover} controls={showControls}></video>
|
||||||
</div> : <>
|
</div> : <>
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
url={url}
|
url={url}
|
||||||
@ -27,16 +42,13 @@ export function Player({ url, cover, simple, showControls }: { url: string; cove
|
|||||||
onEnded={() => setState({ playing: false })}
|
onEnded={() => setState({ playing: false })}
|
||||||
onPause={() => setState({ playing: false })}
|
onPause={() => setState({ playing: false })}
|
||||||
onReady={(_player) => {
|
onReady={(_player) => {
|
||||||
setState({ duration: _player.getDuration() })
|
setState({duration: _player.getDuration() })
|
||||||
}}
|
}}
|
||||||
onProgress={(_) => {
|
onProgress={(_) => {
|
||||||
setState((_prev) => {
|
setState(_prev=>({
|
||||||
return {
|
playedSeconds: _.playedSeconds,
|
||||||
..._prev,
|
progress: Math.floor(_.playedSeconds / _prev.duration * 100)
|
||||||
playedSeconds: _.playedSeconds,
|
}))
|
||||||
progress: Math.floor(_.playedSeconds / _prev.duration * 100)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="video-control p-2 flex items-center gap-2">
|
<div className="video-control p-2 flex items-center gap-2">
|
||||||
|
71
src/components/video/video-list-item.tsx
Normal file
71
src/components/video/video-list-item.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import {useSortable} from "@dnd-kit/sortable";
|
||||||
|
import {useSetState} from "ahooks";
|
||||||
|
import React, {useEffect} from "react";
|
||||||
|
import {clsx} from "clsx";
|
||||||
|
import {CheckCircleFilled, MenuOutlined, MinusCircleFilled} from "@ant-design/icons";
|
||||||
|
|
||||||
|
import {IconEdit, IconPlay} from "@/components/icons";
|
||||||
|
import {Popconfirm} from "antd";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
video: VideoInfo,
|
||||||
|
index?: number;
|
||||||
|
checked?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
onCheckedChange?: (checked: boolean) => void;
|
||||||
|
onPlay?: () => void;
|
||||||
|
onEdit?: () => void;
|
||||||
|
onRemove?: () => void;
|
||||||
|
id:number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoListItem = ({index,id, video, onPlay, onRemove, checked, onCheckedChange,onEdit,active}: Props) => {
|
||||||
|
const {
|
||||||
|
attributes, listeners,
|
||||||
|
setNodeRef, transform
|
||||||
|
} = useSortable({resizeObserverConfig: {}, id})
|
||||||
|
|
||||||
|
|
||||||
|
const [state, setState] = useSetState<{checked?:boolean}>({})
|
||||||
|
useEffect(() => {
|
||||||
|
setState({checked})
|
||||||
|
}, [checked])
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={'video-item flex items-center gap-3 mb-5'}
|
||||||
|
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}>
|
||||||
|
{index && index > 0 && <div className="flex items-center px-2">
|
||||||
|
<div
|
||||||
|
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{id}</div>
|
||||||
|
</div>}
|
||||||
|
<div className={`video-item-info flex gap-2 flex-1 bg-gray-100 min-h-[40px] rounded-lg p-3 shadow-blue-500 ${active?'video-item-shadow':''}`}>
|
||||||
|
<div className={'video-title leading-7 flex-1'}>{video.id} - {video.title}</div>
|
||||||
|
<div className={'video-item-cover'}>
|
||||||
|
<img className="w-[100px] rounded-md" src={video.cover} alt={video.title}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="operation flex items-center ml-2 gap-3 text-lg text-gray-400">
|
||||||
|
<button className="hover:text-blue-500" {...attributes} {...listeners}>
|
||||||
|
<MenuOutlined/>
|
||||||
|
</button>
|
||||||
|
{onPlay && <button className="hover:text-blue-500" onClick={onPlay} style={{fontSize: '1.3em'}}><IconPlay/>
|
||||||
|
</button>}
|
||||||
|
{onEdit && <button className="hover:text-blue-500" onClick={onEdit} style={{fontSize: '1.1em'}}><IconEdit/>
|
||||||
|
</button>}
|
||||||
|
<button className="hover:text-blue-300" onClick={() => {
|
||||||
|
if (onCheckedChange) {
|
||||||
|
onCheckedChange(!state.checked)
|
||||||
|
} else {
|
||||||
|
setState({checked: !state.checked})
|
||||||
|
}
|
||||||
|
}}><CheckCircleFilled className={clsx({'text-blue-500': state.checked})}/></button>
|
||||||
|
{onRemove && <Popconfirm
|
||||||
|
title="提示"
|
||||||
|
description={<div style={{minWidth: 150}}><span>请确认删除此视频?</span></div>}
|
||||||
|
onConfirm={onRemove}
|
||||||
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
><button className="hover:text-blue-500"><MinusCircleFilled/></button></Popconfirm>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,41 +1,126 @@
|
|||||||
import ArticleGroup from "@/components/article/group.tsx";
|
import {Button, message, Modal} from "antd";
|
||||||
import {Input, Modal} from "antd";
|
import React, {useRef, useState} from "react";
|
||||||
import {useState} from "react";
|
|
||||||
|
|
||||||
import { ArticleGroupList } from "@/_local/mock-data";
|
import {ArticleGroupList, MockVideoDataList} from "@/_local/mock-data";
|
||||||
|
import {DndContext} from "@dnd-kit/core";
|
||||||
|
import {arrayMove, SortableContext} from "@dnd-kit/sortable";
|
||||||
|
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||||
|
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
||||||
|
import {useSetState} from "ahooks";
|
||||||
|
import {CheckCircleFilled} from "@ant-design/icons";
|
||||||
|
import {clsx} from "clsx";
|
||||||
|
|
||||||
|
|
||||||
export default function CreateIndex() {
|
export default function CreateIndex() {
|
||||||
const [visible, setVisible] = useState(true)
|
const [editNews, setEditNews] = useSetState<{
|
||||||
const [groups, setGroups] = useState<ArticleContentGroup[]>(ArticleGroupList);
|
title?: string;
|
||||||
return (<div>
|
groups?: ArticleContentGroup[];
|
||||||
<h1>create index</h1>
|
}>({})
|
||||||
<Modal
|
|
||||||
title={'编辑文章'}
|
const [videoData, setVideoData] = useState<VideoInfo[]>(MockVideoDataList)
|
||||||
open={visible}
|
const [modal, contextHolder] = Modal.useModal()
|
||||||
maskClosable={false}
|
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||||
keyboard={false}
|
const [state,setState] = useSetState({
|
||||||
width={800}
|
checkedAll: false
|
||||||
onCancel={() => setVisible(false)}
|
})
|
||||||
>
|
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||||
<div className="article-title mt-5">
|
const processDeleteVideo = async (_idArray: number[]) => {
|
||||||
<div className="title">
|
message.info('删除成功!!!' + _idArray.join(''));
|
||||||
<span className="text text-base">标题</span>
|
}
|
||||||
<span className="require ml-1 font-bold text-red-500">*</span>
|
|
||||||
|
const handleDeleteBatch = () => {
|
||||||
|
modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '是否要删除选择的视频?',
|
||||||
|
onOk: () => processDeleteVideo(checkedIdArray)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const playVideo = (video: VideoInfo) => {
|
||||||
|
console.log('play', video)
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current!.src = video.play_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleAllCheckedChange = ()=>{
|
||||||
|
// setVideoData(list=>{
|
||||||
|
// list.map(s=>{
|
||||||
|
// s.checked = !state.checkedAll
|
||||||
|
// })
|
||||||
|
// return list
|
||||||
|
// })
|
||||||
|
setCheckedIdArray(state.checkedAll?[]:videoData.map(v=>v.id))
|
||||||
|
setState({
|
||||||
|
checkedAll: !state.checkedAll
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<div className="container py-10 page-live">
|
||||||
|
{contextHolder}
|
||||||
|
<div className="flex">
|
||||||
|
<div className="video-list-container bg-white p-10 rounded flex-1">
|
||||||
|
<div className="live-control flex justify-between mb-8">
|
||||||
|
<div className="pl-[70px]">
|
||||||
|
<span>视频时长: 00:00:29</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
||||||
|
<button className="ml-5 hover:text-blue-300 text-gray-400 text-lg" onClick={handleAllCheckedChange}>
|
||||||
|
<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="box mt-1">
|
<DndContext onDragEnd={(e) => {
|
||||||
<Input placeholder={'请输入文章标题'}/>
|
const {active, over} = e;
|
||||||
|
if (over && active.id !== over.id) {
|
||||||
|
let oldIndex = -1, newIndex = -1;
|
||||||
|
const originArr = [...videoData]
|
||||||
|
setVideoData((items) => {
|
||||||
|
oldIndex = items.findIndex(s => s.id == active.id);
|
||||||
|
newIndex = items.findIndex(s => s.id == over.id);
|
||||||
|
return arrayMove(items, oldIndex, newIndex);
|
||||||
|
});
|
||||||
|
modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '是否要移动到指定位置',
|
||||||
|
onCancel: () => {
|
||||||
|
setVideoData(originArr);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<SortableContext items={videoData}>
|
||||||
|
{videoData.map((v, index) => (
|
||||||
|
<VideoListItem
|
||||||
|
video={v}
|
||||||
|
index={index + 1}
|
||||||
|
id={v.id}
|
||||||
|
key={index}
|
||||||
|
checked={checkedIdArray.includes(v.id)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setCheckedIdArray(idArray => {
|
||||||
|
const newArr = checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
|
||||||
|
setState({checkedAll: newArr.length == videoData.length})
|
||||||
|
return newArr;
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onPlay={() => playVideo(v)}
|
||||||
|
onEdit={() => {
|
||||||
|
setEditNews({title:v.title, groups: [...ArticleGroupList]})
|
||||||
|
}}
|
||||||
|
/>))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
<div className="video-player-container ml-8 w-[400px] flex flex-col">
|
||||||
|
<div className="text-center text-base mt-10">预览视频</div>
|
||||||
|
<div className="video-player flex items-center justify-center flex-1">
|
||||||
|
<div className=" rounded overflow-hidden">
|
||||||
|
<video ref={videoRef} controls autoPlay className="w-full bg-white min-w-[360px]"></video>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="aricle-body mt-2">
|
</div>
|
||||||
<div className="title">
|
<ArticleEditModal title={editNews.title} groups={editNews.groups}/>
|
||||||
<span className="text text-base">正文</span>
|
|
||||||
<span className="require ml-1 font-bold text-red-500">*</span>
|
|
||||||
</div>
|
|
||||||
<div className="box mt-1">
|
|
||||||
<ArticleGroup groups={groups} onChange={list => setGroups(() => list)}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
@ -19,8 +19,8 @@ export default function VideoDetail({video, onClose}: Props) {
|
|||||||
<Modal open={!!video} title="新闻视频详情" width={1000} footer={null} onCancel={onClose}>
|
<Modal open={!!video} title="新闻视频详情" width={1000} footer={null} onCancel={onClose}>
|
||||||
<div className="flex gap-2 my-5">
|
<div className="flex gap-2 my-5">
|
||||||
<div className="news-video w-[350px]">
|
<div className="news-video w-[350px]">
|
||||||
<div className="video-container bg-gray-100 rounded overflow-hidden">
|
<div className="video-container bg-gray-100 rounded overflow-hidden h-[400px]">
|
||||||
<Player url={'http://localhost:10020/ymca.mp4'} simple showControls />
|
<Player url={'https://file.wx.wm-app.xyz/os/media/ymca.mp4'} simple showControls />
|
||||||
</div>
|
</div>
|
||||||
<div className="video-info text-right text-sm text-gray-600 mt-3">
|
<div className="video-info text-right text-sm text-gray-600 mt-3">
|
||||||
<span>创建时间: 5小时前</span>
|
<span>创建时间: 5小时前</span>
|
||||||
|
@ -1,182 +1,90 @@
|
|||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, {useRef, useState} from "react";
|
||||||
import {Modal} from "antd";
|
import {Button, message, Modal} from "antd";
|
||||||
import {MenuOutlined, MinusCircleFilled, CheckCircleFilled} from "@ant-design/icons";
|
import {SortableContext, arrayMove} from '@dnd-kit/sortable';
|
||||||
import {useSetState} from "ahooks";
|
|
||||||
import {clsx} from "clsx";
|
|
||||||
import {SortableContext, useSortable,arrayMove} from '@dnd-kit/sortable';
|
|
||||||
import {DndContext} from "@dnd-kit/core";
|
import {DndContext} from "@dnd-kit/core";
|
||||||
|
|
||||||
import {IconPlay} from "@/components/icons";
|
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||||
|
import {MockVideoDataList} from "@/_local/mock-data.ts";
|
||||||
|
|
||||||
const VideoInfoItem = ({index,id, video, onPlay, onRemove, checked, onCheckedChange}: {
|
|
||||||
video: VideoInfo,
|
|
||||||
index?: number;
|
|
||||||
checked?: boolean;
|
|
||||||
onCheckedChange?: (checked: boolean) => void;
|
|
||||||
onPlay?: () => void;
|
|
||||||
onRemove?: () => void;
|
|
||||||
id:number;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
attributes, listeners,
|
|
||||||
setNodeRef, transform
|
|
||||||
} = useSortable({resizeObserverConfig: {}, id})
|
|
||||||
|
|
||||||
|
|
||||||
const [state, setState] = useSetState({
|
|
||||||
checked: false
|
|
||||||
})
|
|
||||||
useEffect(() => {
|
|
||||||
setState({checked})
|
|
||||||
}, [checked])
|
|
||||||
|
|
||||||
return <div
|
|
||||||
className={'video-item flex items-center gap-3 mb-5'}
|
|
||||||
ref={setNodeRef} style={{transform: `translateY(${transform?.y || 0}px)`,}}>
|
|
||||||
{index && index > 0 && <div className="flex items-center px-2">
|
|
||||||
<div
|
|
||||||
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{id}</div>
|
|
||||||
</div>}
|
|
||||||
<div className={'video-item-info flex gap-2 flex-1 bg-gray-100 min-h-[40px] rounded-lg p-3'}>
|
|
||||||
<div className={'video-title leading-7 flex-1'}>{video.id} - {video.title}</div>
|
|
||||||
<div className={'video-item-cover'}>
|
|
||||||
<img className="w-[100px] rounded-md" src={video.cover} alt={video.title}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="operation flex items-center gap-3 text-lg text-gray-400">
|
|
||||||
<button className="hover:text-blue-500" {...attributes} {...listeners}>
|
|
||||||
<MenuOutlined/>
|
|
||||||
</button>
|
|
||||||
{onPlay && <button className="hover:text-blue-500" onClick={onPlay} style={{fontSize: '1.3em'}}><IconPlay/>
|
|
||||||
</button>}
|
|
||||||
<button className="hover:text-blue-300" onClick={() => {
|
|
||||||
if (onCheckedChange) {
|
|
||||||
onCheckedChange(!state.checked)
|
|
||||||
} else {
|
|
||||||
setState({checked: !state.checked})
|
|
||||||
}
|
|
||||||
}}><CheckCircleFilled className={clsx({'text-blue-500': state.checked})}/></button>
|
|
||||||
{onRemove && <button className="hover:text-blue-500" onClick={onRemove}><MinusCircleFilled/></button>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
export default function LiveIndex() {
|
export default function LiveIndex() {
|
||||||
const [videoData, setVideoData] = useState<VideoInfo[]>([
|
const [videoData, setVideoData] = useState<VideoInfo[]>(MockVideoDataList)
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: '习近平出席巴西总统卢拉举行的欢迎宴会',
|
|
||||||
cover: '//www.81.cn/syjdt/_attachment/2024/11/21/16353279_e74ba3ed3897343b03ecee69dd0e2c19.jpg',
|
|
||||||
duration: 100,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
play_url: 'https://reflect.app/home/build/q-c3d7becf.webm',
|
|
||||||
description: '1',
|
|
||||||
tags: ['1'],
|
|
||||||
create_time: 1732187665,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: '习近平向2024年世界互联网大会乌镇峰会开幕视频致贺 指明方向凝聚共识',
|
|
||||||
cover: '//www.81.cn/syjdt/_attachment/2024/11/21/16353279_e74ba3ed3897343b03ecee69dd0e2c19.jpg',
|
|
||||||
duration: 100,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
play_url: 'https://file.wx.wm-app.xyz/os/media/ymca.mp4',
|
|
||||||
description: '1',
|
|
||||||
tags: ['1'],
|
|
||||||
create_time: 1732187665,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: '中华人民共和国和巴西联邦共和国关于携手构建更公正世界和更可持续星球的中巴命运共同体的联合声明',
|
|
||||||
cover: '//www.81.cn/syjdt/_attachment/2024/11/21/16353279_e74ba3ed3897343b03ecee69dd0e2c19.jpg',
|
|
||||||
duration: 100,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
play_url: 'https://reflect.app/home/build/q-c3d7becf.webm',
|
|
||||||
description: '1',
|
|
||||||
tags: ['1'],
|
|
||||||
create_time: 1732187665,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: '中华人民共和国和巴西联邦共和国关于携手构建更公正世界和更可持续星球的中巴命运共同体的联合声明',
|
|
||||||
cover: '//www.81.cn/syjdt/_attachment/2024/11/21/16353279_e74ba3ed3897343b03ecee69dd0e2c19.jpg',
|
|
||||||
duration: 100,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
play_url: 'https://file.wx.wm-app.xyz/os/media/ymca.mp4',
|
|
||||||
description: '1',
|
|
||||||
tags: ['1'],
|
|
||||||
create_time: 1732187665,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: '中华人民共和国和巴西联邦共和国关于携手构建更公正世界和更可持续星球的中巴命运共同体的联合声明',
|
|
||||||
cover: '//www.81.cn/syjdt/_attachment/2024/11/21/16353279_e74ba3ed3897343b03ecee69dd0e2c19.jpg',
|
|
||||||
duration: 100,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
play_url: 'https://reflect.app/home/build/q-c3d7becf.webm',
|
|
||||||
description: '1',
|
|
||||||
tags: ['1'],
|
|
||||||
create_time: 1732187665,
|
|
||||||
}
|
|
||||||
])
|
|
||||||
const [modal, contextHolder] = Modal.useModal()
|
const [modal, contextHolder] = Modal.useModal()
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||||
const playVideo = (video: VideoInfo) => {
|
const processDeleteVideo = async (_idArray: number[]) => {
|
||||||
console.log('play',video)
|
message.info('删除成功!!!' + _idArray.join(''));
|
||||||
if (videoRef.current) {
|
|
||||||
videoRef.current!.src = video.play_url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const handleDeleteBatch = () => {
|
||||||
|
modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '是否要删除选择的视频?',
|
||||||
|
onOk: () => processDeleteVideo(checkedIdArray)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (<div className="container py-10 page-live">
|
return (<div className="container py-10 page-live">
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="video-list-container bg-white p-10 rounded flex-1">
|
<div className="video-player-container mr-8 flex flex-col">
|
||||||
<DndContext onDragEnd={(e) => {
|
<div className="text-center text-base">数字人直播间</div>
|
||||||
const {active, over} = e;
|
<div className="video-player flex justify-center flex-1 mt-5">
|
||||||
if (over && active.id !== over.id) {
|
<div className=" rounded overflow-hidden w-[360px] h-[700px]">
|
||||||
let oldIndex = -1, newIndex = -1;
|
<iframe src="https://fm.gachafun.com/" className="border-0 w-full h-full max-h-full"></iframe>
|
||||||
const originArr = [...videoData]
|
|
||||||
setVideoData((items) => {
|
|
||||||
oldIndex = items.findIndex(s => s.id == active.id);
|
|
||||||
newIndex = items.findIndex(s => s.id == over.id);
|
|
||||||
return arrayMove(items, oldIndex, newIndex);
|
|
||||||
});
|
|
||||||
modal.confirm({
|
|
||||||
title: '提示',
|
|
||||||
content: '是否要移动到指定位置',
|
|
||||||
onCancel: () => {
|
|
||||||
setVideoData(originArr);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<SortableContext items={videoData}>
|
|
||||||
{videoData.map((v, index) => (
|
|
||||||
<VideoInfoItem
|
|
||||||
video={v}
|
|
||||||
index={index + 1}
|
|
||||||
id={v.id}
|
|
||||||
key={index}
|
|
||||||
onPlay={() => playVideo(v)}
|
|
||||||
onRemove={() => {
|
|
||||||
}}
|
|
||||||
/>))}
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
</div>
|
|
||||||
<div className="video-player-container ml-5 w-[300px] flex flex-col">
|
|
||||||
<div className="text-center text-base">预览视频</div>
|
|
||||||
<div className="video-player flex items-center justify-center flex-1">
|
|
||||||
<div className="bg-white rounded overflow-hidden">
|
|
||||||
<video ref={videoRef} autoPlay className="w-full"></video>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="video-list-container flex-1">
|
||||||
|
<div className=" bg-white py-8 px-6 rounded py-1">
|
||||||
|
<div className="live-control flex justify-between mb-8">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button type="primary">开始直播</Button>
|
||||||
|
<Button>暂停直播</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DndContext onDragEnd={(e) => {
|
||||||
|
const {active, over} = e;
|
||||||
|
if (over && active.id !== over.id) {
|
||||||
|
let oldIndex = -1, newIndex = -1;
|
||||||
|
const originArr = [...videoData]
|
||||||
|
setVideoData((items) => {
|
||||||
|
oldIndex = items.findIndex(s => s.id == active.id);
|
||||||
|
newIndex = items.findIndex(s => s.id == over.id);
|
||||||
|
return arrayMove(items, oldIndex, newIndex);
|
||||||
|
});
|
||||||
|
modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '是否要移动到指定位置',
|
||||||
|
onCancel: () => {
|
||||||
|
setVideoData(originArr);
|
||||||
|
},
|
||||||
|
onOk: () => {
|
||||||
|
setVideoData([...videoData])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<SortableContext items={videoData}>
|
||||||
|
{videoData.map((v, index) => (
|
||||||
|
<VideoListItem
|
||||||
|
video={v}
|
||||||
|
index={index + 1}
|
||||||
|
id={v.id}
|
||||||
|
active={index == 0}
|
||||||
|
key={index}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setCheckedIdArray(idArray => {
|
||||||
|
return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id);
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onRemove={() => processDeleteVideo([v.id])}
|
||||||
|
/>))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
@ -5,36 +5,10 @@ import React from "react";
|
|||||||
import {NewsSources} from "@/pages/news/components/news-source.ts";
|
import {NewsSources} from "@/pages/news/components/news-source.ts";
|
||||||
import {useRequest, useSetState} from "ahooks";
|
import {useRequest, useSetState} from "ahooks";
|
||||||
import {formatTime} from "@/util/strings.ts";
|
import {formatTime} from "@/util/strings.ts";
|
||||||
|
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
||||||
|
import {ArticleGroupList} from "@/_local/mock-data.ts";
|
||||||
|
|
||||||
|
|
||||||
const columns: TableColumnsType<NewsInfo> = [
|
|
||||||
{
|
|
||||||
title: '标题',
|
|
||||||
dataIndex: 'title',
|
|
||||||
// render: (text: string) => <a>{text}</a>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容',
|
|
||||||
dataIndex: 'content',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '来源',
|
|
||||||
dataIndex: 'source',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '时间',
|
|
||||||
dataIndex: 'time',
|
|
||||||
render: (_, record) => {
|
|
||||||
return formatTime(record.time, 'YYYY-MM-DD HH:mm')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
align: 'center',
|
|
||||||
render: () => (<Button type="link">编辑</Button>),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const dataList: NewsInfo[] = [
|
const dataList: NewsInfo[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -62,7 +36,11 @@ const rowSelection: TableProps<NewsInfo>['rowSelection'] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function NewEdit() {
|
export default function NewEdit() {
|
||||||
const [state, setState] = useSetState({
|
const [editNews, setEditNews] = useSetState<{
|
||||||
|
title?: string;
|
||||||
|
groups?: ArticleContentGroup[];
|
||||||
|
}>({})
|
||||||
|
const [params, setParams] = useSetState({
|
||||||
source: NewsSources.map(s => s.value),
|
source: NewsSources.map(s => s.value),
|
||||||
search: '',
|
search: '',
|
||||||
page: 1
|
page: 1
|
||||||
@ -70,38 +48,72 @@ export default function NewEdit() {
|
|||||||
const {data} = useRequest(async () => {
|
const {data} = useRequest(async () => {
|
||||||
return [...dataList]
|
return [...dataList]
|
||||||
}, {
|
}, {
|
||||||
refreshDeps: [state]
|
refreshDeps: [params]
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSelectChange = (values: string[]) => {
|
const handleSelectChange = (values: string[]) => {
|
||||||
if (values.length == 0) {
|
if (values.length == 0) {
|
||||||
setState({source: []})
|
setParams({source: []})
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lastValue = values[values.length - 1];
|
const lastValue = values[values.length - 1];
|
||||||
const source = NewsSources.map(s => s.value) || [];
|
const source = NewsSources.map(s => s.value) || [];
|
||||||
const isChecked = values.length > state.source.length; // 是选中还是取消选中
|
const isChecked = values.length > params.source.length; // 是选中还是取消选中
|
||||||
if (lastValue == 'all') {
|
if (lastValue == 'all') {
|
||||||
setState({source})
|
setParams({source})
|
||||||
} else if (isChecked && values.length == source.length - 1 && !values.includes('all')) { // 除全部之外已经都选了 则直接勾选所有
|
} else if (isChecked && values.length == source.length - 1 && !values.includes('all')) { // 除全部之外已经都选了 则直接勾选所有
|
||||||
setState({source})
|
setParams({source})
|
||||||
} else {
|
} else {
|
||||||
const diffValues = state.source.filter(s => !values.includes(s));
|
const diffValues = params.source.filter(s => !values.includes(s));
|
||||||
// 取消的是全部 则取消所有勾选
|
// 取消的是全部 则取消所有勾选
|
||||||
if (state.source.length > 0 && state.source.length > values.length && diffValues.includes('all')) {
|
if (params.source.length > 0 && params.source.length > values.length && diffValues.includes('all')) {
|
||||||
setState({source: []})
|
setParams({source: []})
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState({source: values.filter(s => s != 'all')})
|
setParams({source: values.filter(s => s != 'all')})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const columns: TableColumnsType<NewsInfo> = [
|
||||||
|
{
|
||||||
|
title: '标题',
|
||||||
|
dataIndex: 'title',
|
||||||
|
// render: (text: string) => <a>{text}</a>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '内容',
|
||||||
|
dataIndex: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '来源',
|
||||||
|
dataIndex: 'source',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'time',
|
||||||
|
render: (_, record) => {
|
||||||
|
return formatTime(record.time, 'YYYY-MM-DD HH:mm')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
align: 'center',
|
||||||
|
render: (_, record) => (<Button type="link" onClick={() => {
|
||||||
|
setEditNews({
|
||||||
|
title: record.title, groups: [...ArticleGroupList]
|
||||||
|
})
|
||||||
|
}}>编辑</Button>),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (<div className="container pb-5 news-edit">
|
return (<div className="container pb-5 news-edit">
|
||||||
<Card className="search-panel-container my-5">
|
<Card className="search-panel-container my-5">
|
||||||
<div className="search-form flex gap-5 justify-between">
|
<div className="search-form flex gap-5 justify-between">
|
||||||
<div className="search-form-input flex gap-2 items-center">
|
<div className="search-form-input flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
onPressEnter={(e) => {
|
onPressEnter={(e) => {
|
||||||
setState({search: e.target.value})
|
setParams({search: e.target.value})
|
||||||
}}
|
}}
|
||||||
type="text" className="rounded px-3 w-[220px]"
|
type="text" className="rounded px-3 w-[220px]"
|
||||||
suffix={<SearchOutlined/>}
|
suffix={<SearchOutlined/>}
|
||||||
@ -109,7 +121,8 @@ export default function NewEdit() {
|
|||||||
/>
|
/>
|
||||||
<span className="ml-5 text-sm">来源</span>
|
<span className="ml-5 text-sm">来源</span>
|
||||||
<Select
|
<Select
|
||||||
value={state.source} className="min-w-[300px] select-no-wrap select-hide-checked max-w-[300px] "
|
value={params.source}
|
||||||
|
className="min-w-[300px] select-no-wrap select-hide-checked max-w-[300px] "
|
||||||
options={NewsSources} popupClassName="select-hide-checked"
|
options={NewsSources} popupClassName="select-hide-checked"
|
||||||
mode="multiple" showSearch={false}
|
mode="multiple" showSearch={false}
|
||||||
onChange={handleSelectChange}
|
onChange={handleSelectChange}
|
||||||
@ -127,7 +140,9 @@ export default function NewEdit() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary">手动新增</Button>
|
<Button type="primary" onClick={() => {
|
||||||
|
setEditNews({title: '', groups: []})
|
||||||
|
}}>手动新增</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="news-list-container mt-5">
|
<div className="news-list-container mt-5">
|
||||||
<Table<NewsInfo>
|
<Table<NewsInfo>
|
||||||
@ -138,15 +153,16 @@ export default function NewEdit() {
|
|||||||
pagination={{
|
pagination={{
|
||||||
position: ['bottomLeft'],
|
position: ['bottomLeft'],
|
||||||
simple: true,
|
simple: true,
|
||||||
defaultCurrent:1,
|
defaultCurrent: 1,
|
||||||
total:5000004,
|
total: 5000004,
|
||||||
pageSize:20,
|
pageSize: 20,
|
||||||
showSizeChanger:false,
|
showSizeChanger: false,
|
||||||
rootClassName:'simple-pagination',
|
rootClassName: 'simple-pagination',
|
||||||
onChange: (page)=>setState({page})
|
onChange: (page) => setParams({page})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ArticleEditModal title={editNews.title} groups={editNews.groups}/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
@ -8,6 +8,7 @@ import styles from './style.module.scss'
|
|||||||
|
|
||||||
export default function NewsIndex() {
|
export default function NewsIndex() {
|
||||||
const [list,] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
const [list,] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
||||||
|
const [checkedId, setCheckedId] = useState<number[]>([])
|
||||||
const [activeNews, setActiveNews] = useState<NewsInfo>()
|
const [activeNews, setActiveNews] = useState<NewsInfo>()
|
||||||
return (<div className={'container pb-5'}>
|
return (<div className={'container pb-5'}>
|
||||||
<Card className="search-panel-container my-5">
|
<Card className="search-panel-container my-5">
|
||||||
@ -38,17 +39,27 @@ export default function NewsIndex() {
|
|||||||
<div className={styles.newsList}>
|
<div className={styles.newsList}>
|
||||||
{list.map(id => (
|
{list.map(id => (
|
||||||
<div key={id} className={`py-3 flex items-start border-b border-gray-100 group`}>
|
<div key={id} className={`py-3 flex items-start border-b border-gray-100 group`}>
|
||||||
<div className="checkbox mr-2 opacity-0 group-hover:opacity-100">
|
<div
|
||||||
<Checkbox/>
|
className={`checkbox mr-2 ${checkedId.includes(id) ? '' : 'opacity-0'} group-hover:opacity-100`}>
|
||||||
|
<Checkbox checked={checkedId.includes(id)} onChange={() => {
|
||||||
|
if (checkedId.includes(id)) {
|
||||||
|
setCheckedId(checkedId.filter(item => item != id))
|
||||||
|
} else {
|
||||||
|
setCheckedId([...checkedId, id])
|
||||||
|
}
|
||||||
|
}}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="news-content">
|
<div className="news-content">
|
||||||
<div className="title text-lg cursor-pointer" onClick={() => {
|
<div className="flex items-center justify-between">
|
||||||
setActiveNews({
|
<div className="title text-lg cursor-pointer" onClick={() => {
|
||||||
id: 1,
|
setActiveNews({
|
||||||
title: '习近平抵达巴西利亚开始对巴西进行国事访问',
|
id: 1,
|
||||||
content: '', cover: "", source: "", time: ""
|
title: '习近平抵达巴西利亚开始对巴西进行国事访问',
|
||||||
})
|
content: '', cover: "", source: "", time: ""
|
||||||
}}>习近平抵达巴西利亚开始对巴西进行国事访问
|
})
|
||||||
|
}}>习近平抵达巴西利亚开始对巴西进行国事访问
|
||||||
|
</div>
|
||||||
|
{id == 1 && <div className="text-sm text-blue-500">已加入编辑界面</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="content flex gap-3 mt-2 mb-3">
|
<div className="content flex gap-3 mt-2 mb-3">
|
||||||
<div className="cover border border-gray-100 flex items-center rounded overflow-hidden"
|
<div className="cover border border-gray-100 flex items-center rounded overflow-hidden"
|
||||||
@ -61,7 +72,7 @@ export default function NewsIndex() {
|
|||||||
专机抵达巴西利亚空军基地时,巴西总统府首席部长科斯塔、巴西利亚空军基地司令米格尔、司法部长莱万多夫斯基、总统府机构关系部长帕迪利亚等高级官员在机场热情迎接,代表卢拉总统和巴西政府热烈欢迎习近平主席到访。
|
专机抵达巴西利亚空军基地时,巴西总统府首席部长科斯塔、巴西利亚空军基地司令米格尔、司法部长莱万多夫斯基、总统府机构关系部长帕迪利亚等高级官员在机场热情迎接,代表卢拉总统和巴西政府热烈欢迎习近平主席到访。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="info text-gray-300 flex items-center gap-3 text-sm">
|
<div className="info text-gray-300 flex items-center justify-between gap-3 text-sm">
|
||||||
<div>来源: <span>新华社</span></div>
|
<div>来源: <span>新华社</span></div>
|
||||||
{/*<Divider type="vertical" />*/}
|
{/*<Divider type="vertical" />*/}
|
||||||
<div>发布时间: <span>2024-11-18 10:10:12</span></div>
|
<div>发布时间: <span>2024-11-18 10:10:12</span></div>
|
||||||
|
@ -16,6 +16,7 @@ type FieldType = {
|
|||||||
|
|
||||||
export default function FormLogin() {
|
export default function FormLogin() {
|
||||||
const [disabled, setDisabled] = useState(true)
|
const [disabled, setDisabled] = useState(true)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
const {login} = useAuth();
|
const {login} = useAuth();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -23,15 +24,16 @@ export default function FormLogin() {
|
|||||||
const [phone, setPhone] = useState<string>()
|
const [phone, setPhone] = useState<string>()
|
||||||
const {sending, countdown, sendCode} = useSmsCode()
|
const {sending, countdown, sendCode} = useSmsCode()
|
||||||
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
|
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
|
||||||
if (values.username != 'admin') {
|
if (!values.username || !/^1\d{10}$/.test(values.username)) {
|
||||||
setError('账号或密码错误')
|
setError('账号或密码错误')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setLoading(true)
|
||||||
login(values.username, values.password!).then(() => {
|
login(values.username, values.password!).then(() => {
|
||||||
navigate(params.get('from') || '/')
|
navigate(params.get('from') || '/')
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
setError(e.message)
|
setError(e.message)
|
||||||
});
|
}).finally(()=>setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (<div className="form">
|
return (<div className="form">
|
||||||
@ -81,8 +83,8 @@ export default function FormLogin() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button disabled={disabled} type="primary" size={'large'} htmlType="submit" block shape={'round'}>
|
<Button disabled={disabled || loading} loading={loading} type="primary" size={'large'} htmlType="submit" block shape={'round'}>
|
||||||
立即登录
|
{login?'登录中':'立即登录'}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -21,6 +21,7 @@ export async function getUserInfo() {
|
|||||||
* @param state
|
* @param state
|
||||||
*/
|
*/
|
||||||
export async function auth(_username: string, _password: string) {
|
export async function auth(_username: string, _password: string) {
|
||||||
|
await sleep(1500);
|
||||||
return mockUser;
|
return mockUser;
|
||||||
//return post<UserProfile>('/auth', {code, state})
|
//return post<UserProfile>('/auth', {code, state})
|
||||||
}
|
}
|
1
src/types/api.d.ts
vendored
1
src/types/api.d.ts
vendored
@ -26,4 +26,5 @@ declare interface VideoInfo {
|
|||||||
description: string;
|
description: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
create_time: number;
|
create_time: number;
|
||||||
|
checked?:boolean
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user