diff --git a/src/assets/core.scss b/src/assets/core.scss index 420be76..d19f631 100644 --- a/src/assets/core.scss +++ b/src/assets/core.scss @@ -13,6 +13,7 @@ --app-header-header: 90px; --container-width: 1440px; } + @tailwind base; @tailwind components; @tailwind utilities; @@ -31,66 +32,81 @@ } } } -.card{ + +.card { @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; padding: 3px; - &:after{ + + &:after { @apply inline-block bg-blue-500 w-full h-full; content: ' '; border-radius: 50%; opacity: 0; } } -.checkbox-icon{ + +.checkbox-icon { @apply rounded; padding: 0; - &:before{ + + &:before { content: ' '; display: inline-block; border-left: solid 2px #fff; - border-bottom: solid 2px #fff; + border-bottom: solid 2px #fff; height: 6px; width: 10px; position: absolute; transform: rotate(-45deg) translateY(-1px) translateX(1px); opacity: 0; } - &:after{ + + &:after { border-radius: 2px; } } + .ant-select-item-option { - &.ant-select-item-option-selected{ - .radio-icon,.checkbox-icon{ + &.ant-select-item-option-selected { + .radio-icon, .checkbox-icon { @apply border-blue-500; - &:after,&:before { + &:after, &:before { opacity: 1; } } } } -.select-hide-checked{ - .ant-select-item-option-selected{ - .ant-select-item-option-state{ - opacity: 0 !important; + +.select-hide-checked { + .ant-select-item-option-selected { + .ant-select-item-option-state { + opacity: 0 !important; } } } -.select-no-wrap{ - .ant-select-selector{ - .ant-select-selection-overflow{ + +.select-no-wrap { + .ant-select-selector { + .ant-select-selection-overflow { @apply flex flex-nowrap; } } } -.simple-pagination{ - .ant-pagination-simple-pager{ - input[type=text]{ + +.simple-pagination { + .ant-pagination-simple-pager { + input[type=text] { width: auto; 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)); } \ No newline at end of file diff --git a/src/components/article/article.module.scss b/src/components/article/article.module.scss index 7363bc4..161d5ce 100644 --- a/src/components/article/article.module.scss +++ b/src/components/article/article.module.scss @@ -37,12 +37,24 @@ } .uploadImage { - @apply flex justify-center items-center cursor-pointer; + @apply flex justify-center items-center relative; img { display: block; max-width: 100%; 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 { diff --git a/src/components/article/edit-modal.tsx b/src/components/article/edit-modal.tsx new file mode 100644 index 0000000..cf0decb --- /dev/null +++ b/src/components/article/edit-modal.tsx @@ -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; +} + +export default function ArticleEditModal(props: Props) { + + const [groups, setGroups] = useState([]); + 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 (setState({open: false})} + okButtonProps={{loading: state.loading}} + coOk={handleSave} + > +
+
+ 标题 + * +
+
+ { + setTitle(e.target.value) + }} placeholder={'请输入文章标题'}/> +
+
+
+
+ 正文 + * +
+
+ setGroups(() => list)}/> +
+
+
); +} \ No newline at end of file diff --git a/src/components/article/item.tsx b/src/components/article/item.tsx index 02e315d..c28f701 100644 --- a/src/components/article/item.tsx +++ b/src/components/article/item.tsx @@ -1,6 +1,6 @@ import React from "react"; import styles from './article.module.scss' -import {Input, Upload} from "antd"; +import {Button, Input, Upload} from "antd"; type Props = { children?: React.ReactNode; @@ -15,7 +15,14 @@ export function BlockImage({data,editable}: Props) { {editable ?
- + { data.content ? <> + +
+ 编辑 +
+ :
+ +
}
:
} diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx index 370b259..d917c2f 100644 --- a/src/components/icons/index.tsx +++ b/src/components/icons/index.tsx @@ -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" fill="currentColor"/> +) + +export const IconEdit = ({style,className}: { style?: React.CSSProperties;className?:string; }) => ( + + + ) \ No newline at end of file diff --git a/src/components/video/player.tsx b/src/components/video/player.tsx index 26fcce9..fccc6d0 100644 --- a/src/components/video/player.tsx +++ b/src/components/video/player.tsx @@ -1,10 +1,19 @@ import ReactPlayer from 'react-player' -import { useSetState } from "ahooks"; import { PauseOutlined, PlayCircleOutlined, FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons" 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 | ((prev: State) => Partial) export function Player({ url, cover, simple, showControls }: { url: string; cover?: string; simple?: boolean; showControls?: boolean }) { - const [state, setState] = useSetState({ + const [state, _setState] = useState({ playing: false, muted: false, // 是否全屏 @@ -13,9 +22,15 @@ export function Player({ url, cover, simple, showControls }: { url: string; cove playedSeconds: 0, duration: 0 }) + const setState = (data: StateUpdate) => { + _setState(prev => { + if (typeof(data) === 'function') return { ...prev, ...data(prev) } + return { ...prev, ...data } + }) + } return
{simple ?
- +
: <> setState({ playing: false })} onPause={() => setState({ playing: false })} onReady={(_player) => { - setState({ duration: _player.getDuration() }) + setState({duration: _player.getDuration() }) }} onProgress={(_) => { - setState((_prev) => { - return { - ..._prev, - playedSeconds: _.playedSeconds, - progress: Math.floor(_.playedSeconds / _prev.duration * 100) - } - }) + setState(_prev=>({ + playedSeconds: _.playedSeconds, + progress: Math.floor(_.playedSeconds / _prev.duration * 100) + })) }} />
diff --git a/src/components/video/video-list-item.tsx b/src/components/video/video-list-item.tsx new file mode 100644 index 0000000..0cc1dc4 --- /dev/null +++ b/src/components/video/video-list-item.tsx @@ -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
+ {index && index > 0 &&
+
{id}
+
} +
+
{video.id} - {video.title}
+
+ {video.title}/ +
+
+
+ + {onPlay && } + {onEdit && } + + {onRemove && 请确认删除此视频?
} + onConfirm={onRemove} + okText="删除" + cancelText="取消" + >} +
+
+} \ No newline at end of file diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 1361e31..45145c6 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -1,41 +1,126 @@ -import ArticleGroup from "@/components/article/group.tsx"; -import {Input, Modal} from "antd"; -import {useState} from "react"; +import {Button, message, Modal} from "antd"; +import React, {useRef, 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() { - const [visible, setVisible] = useState(true) - const [groups, setGroups] = useState(ArticleGroupList); - return (
-

create index

- setVisible(false)} - > -
-
- 标题 - * + const [editNews, setEditNews] = useSetState<{ + title?: string; + groups?: ArticleContentGroup[]; + }>({}) + + const [videoData, setVideoData] = useState(MockVideoDataList) + const [modal, contextHolder] = Modal.useModal() + const videoRef = useRef(null) + const [state,setState] = useSetState({ + checkedAll: false + }) + const [checkedIdArray, setCheckedIdArray] = useState([]) + const processDeleteVideo = async (_idArray: number[]) => { + message.info('删除成功!!!' + _idArray.join('')); + } + + 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 (
+ {contextHolder} +
+
+
+
+ 视频时长: 00:00:29 +
+
+ 批量删除 + +
-
- + { + 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); + } + }) + } + }}> + + {videoData.map((v, index) => ( + { + 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]}) + }} + />))} + + +
+
+
预览视频
+
+
+ +
-
-
- 正文 - * -
-
- setGroups(() => list)}/> -
-
- +
+
) } \ No newline at end of file diff --git a/src/pages/library/components/video-detail.tsx b/src/pages/library/components/video-detail.tsx index 2603e6f..9619b1d 100644 --- a/src/pages/library/components/video-detail.tsx +++ b/src/pages/library/components/video-detail.tsx @@ -19,8 +19,8 @@ export default function VideoDetail({video, onClose}: Props) {
-
- +
+
创建时间: 5小时前 diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index 3a30674..9df3eaf 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -1,182 +1,90 @@ -import React, {useEffect, useRef, useState} from "react"; -import {Modal} from "antd"; -import {MenuOutlined, MinusCircleFilled, CheckCircleFilled} from "@ant-design/icons"; -import {useSetState} from "ahooks"; -import {clsx} from "clsx"; -import {SortableContext, useSortable,arrayMove} from '@dnd-kit/sortable'; +import React, {useRef, useState} from "react"; +import {Button, message, Modal} from "antd"; +import {SortableContext, arrayMove} from '@dnd-kit/sortable'; 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
- {index && index > 0 &&
-
{id}
-
} -
-
{video.id} - {video.title}
-
- {video.title}/ -
-
-
- - {onPlay && } - - {onRemove && } -
-
-} export default function LiveIndex() { - const [videoData, setVideoData] = useState([ - { - 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 [videoData, setVideoData] = useState(MockVideoDataList) const [modal, contextHolder] = Modal.useModal() - const videoRef = useRef(null) - const playVideo = (video: VideoInfo) => { - console.log('play',video) - if (videoRef.current) { - videoRef.current!.src = video.play_url - } + const [checkedIdArray, setCheckedIdArray] = useState([]) + const processDeleteVideo = async (_idArray: number[]) => { + message.info('删除成功!!!' + _idArray.join('')); } + const handleDeleteBatch = () => { + modal.confirm({ + title: '提示', + content: '是否要删除选择的视频?', + onOk: () => processDeleteVideo(checkedIdArray) + }) + } + return (
{contextHolder}
-
- { - 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); - } - }) - } - }}> - - {videoData.map((v, index) => ( - playVideo(v)} - onRemove={() => { - }} - />))} - - -
-
-
预览视频
-
-
- +
+
数字人直播间
+
+
+
+
+
+
+
+ + +
+
+ 批量删除 +
+
+ { + 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]) + } + }) + } + }}> + + {videoData.map((v, index) => ( + { + setCheckedIdArray(idArray => { + return checked ? idArray.concat(v.id) : idArray.filter(id => id != v.id); + }) + }} + onRemove={() => processDeleteVideo([v.id])} + />))} + + +
+
) } \ No newline at end of file diff --git a/src/pages/news/edit.tsx b/src/pages/news/edit.tsx index 6a13fd3..47064f8 100644 --- a/src/pages/news/edit.tsx +++ b/src/pages/news/edit.tsx @@ -5,36 +5,10 @@ import React from "react"; import {NewsSources} from "@/pages/news/components/news-source.ts"; import {useRequest, useSetState} from "ahooks"; import {formatTime} from "@/util/strings.ts"; +import ArticleEditModal from "@/components/article/edit-modal.tsx"; +import {ArticleGroupList} from "@/_local/mock-data.ts"; -const columns: TableColumnsType = [ - { - title: '标题', - dataIndex: 'title', - // render: (text: string) => {text}, - }, - { - title: '内容', - dataIndex: 'content', - }, - { - title: '来源', - dataIndex: 'source', - }, - { - title: '时间', - dataIndex: 'time', - render: (_, record) => { - return formatTime(record.time, 'YYYY-MM-DD HH:mm') - } - }, - { - title: '操作', - align: 'center', - render: () => (), - }, -]; - const dataList: NewsInfo[] = [ { id: 1, @@ -62,7 +36,11 @@ const rowSelection: TableProps['rowSelection'] = { }; 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), search: '', page: 1 @@ -70,38 +48,72 @@ export default function NewEdit() { const {data} = useRequest(async () => { return [...dataList] }, { - refreshDeps: [state] + refreshDeps: [params] }) const handleSelectChange = (values: string[]) => { if (values.length == 0) { - setState({source: []}) + setParams({source: []}) return; } const lastValue = values[values.length - 1]; const source = NewsSources.map(s => s.value) || []; - const isChecked = values.length > state.source.length; // 是选中还是取消选中 + const isChecked = values.length > params.source.length; // 是选中还是取消选中 if (lastValue == 'all') { - setState({source}) + setParams({source}) } else if (isChecked && values.length == source.length - 1 && !values.includes('all')) { // 除全部之外已经都选了 则直接勾选所有 - setState({source}) + setParams({source}) } 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')) { - setState({source: []}) + if (params.source.length > 0 && params.source.length > values.length && diffValues.includes('all')) { + setParams({source: []}) return; } - setState({source: values.filter(s => s != 'all')}) + setParams({source: values.filter(s => s != 'all')}) } } + + + const columns: TableColumnsType = [ + { + title: '标题', + dataIndex: 'title', + // render: (text: string) => {text}, + }, + { + 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) => (), + }, + ]; + return (
{ - setState({search: e.target.value}) + setParams({search: e.target.value}) }} type="text" className="rounded px-3 w-[220px]" suffix={} @@ -109,7 +121,8 @@ export default function NewEdit() { /> 来源