From 7ee5cac05220c943966d3321b76f75cb302aa0ec Mon Sep 17 00:00:00 2001 From: callmeyan Date: Tue, 24 Dec 2024 14:58:22 +0800 Subject: [PATCH] update ui --- .ide/Dockerfile | 19 ++++++ src/assets/core.scss | 53 +++++++++++++--- src/components/icons/index.tsx | 15 +++++ src/components/scoller/infinite-scroller.tsx | 14 +++-- src/components/video/player.tsx | 15 ++++- src/components/video/video-list-item.tsx | 66 ++++++++++---------- src/pages/live/index.tsx | 26 +++++--- src/pages/news/components/search-panel.tsx | 12 ++-- src/pages/news/components/style.module.scss | 3 +- src/pages/news/edit.tsx | 38 ++++++----- src/pages/video/index.tsx | 29 ++++++--- src/types/api.d.ts | 2 + tailwind.config.js | 8 +++ 13 files changed, 212 insertions(+), 88 deletions(-) create mode 100644 .ide/Dockerfile diff --git a/.ide/Dockerfile b/.ide/Dockerfile new file mode 100644 index 0000000..5799cba --- /dev/null +++ b/.ide/Dockerfile @@ -0,0 +1,19 @@ +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 \ No newline at end of file diff --git a/src/assets/core.scss b/src/assets/core.scss index 7897b61..14b57a7 100644 --- a/src/assets/core.scss +++ b/src/assets/core.scss @@ -152,7 +152,7 @@ } .list-row { - @apply flex bg-white mt-2 py-2 px-4 rounded-xl gap-2 border; + @apply flex bg-white mt-2 py-1 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: 80px; + height: 60px; &:after { @apply absolute; @@ -187,7 +187,7 @@ } .number { - width: 50px; + width: 70px; 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-between; - width: 180px; - padding: 0 20px 0 30px; + @apply flex items-center ml-2 gap-4 text-lg text-gray-400 justify-center; + width: 120px; + padding: 0; } } } @@ -266,6 +266,45 @@ } // 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; diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx index 893d6d1..4c80765 100644 --- a/src/components/icons/index.tsx +++ b/src/components/icons/index.tsx @@ -112,6 +112,21 @@ export const IconAddCircle = ({style, className}: IconProps) => ( fill="currentColor"/> ) + +export const IconWarningCircle = ({style, className}: IconProps) => ( + + + + + + + +) export const IconAdd = ({style, className}: IconProps) => ( diff --git a/src/components/scoller/infinite-scroller.tsx b/src/components/scoller/infinite-scroller.tsx index c6ed587..d08ed1c 100644 --- a/src/components/scoller/infinite-scroller.tsx +++ b/src/components/scoller/infinite-scroller.tsx @@ -1,5 +1,7 @@ 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 @@ -27,8 +29,7 @@ const InfiniteScroller = React.forwardRef { if (scrollContainerRef.current) { - console.log('xaf'); - scrollContainerRef.current.scrollTo({ + scrollContainerRef.current!.scrollTo({ top, behavior: 'smooth' }) @@ -59,16 +60,19 @@ const InfiniteScroller = React.forwardRef + }> + {props.loading &&
}
{props.children}
{props?.pagination && props.pagination.total > props.pagination.limit * props.pagination.page && (props.loadingPlaceholder ||
加载中...
)} - {props?.empty && props.pagination?.total == 0 &&
-
- {props.empty} + {props?.empty && !props.loading && props.pagination?.total == 0 &&
+
+ {props.empty}
} +
); }) //(props: InfiniteScrollerProps) =>{} export default InfiniteScroller \ No newline at end of file diff --git a/src/components/video/player.tsx b/src/components/video/player.tsx index b0126bf..8573eb9 100644 --- a/src/components/video/player.tsx +++ b/src/components/video/player.tsx @@ -93,10 +93,19 @@ export const Player = React.forwardRef((props, ref) => { }) setTcPlayer(() => player) return () => { - if (tcPlayer) { - tcPlayer.pause() - tcPlayer.unload() + // 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) } + playerVideo.parentElement.removeChild(playerVideo) } }, []) React.useImperativeHandle(ref, () => { diff --git a/src/components/video/video-list-item.tsx b/src/components/video/video-list-item.tsx index 536ebf4..124da78 100644 --- a/src/components/video/video-list-item.tsx +++ b/src/components/video/video-list-item.tsx @@ -1,12 +1,10 @@ import {useSortable} from "@dnd-kit/sortable"; import {useSetState} from "ahooks"; import React, {useEffect} from "react"; -import {clsx} from "clsx"; -import {App, Checkbox, Popconfirm} from "antd"; -import {CheckCircleFilled, MenuOutlined, MinusCircleFilled, LoadingOutlined} from "@ant-design/icons"; +import {Checkbox, Popconfirm} from "antd"; import ImageCover from '@/assets/images/cover.png' -import {IconDelete, IconEdit, IconPlay, IconPlaying} from "@/components/icons"; +import {IconDelete, IconEdit, IconPlaying, IconWarningCircle} from "@/components/icons"; import {VideoStatus} from "@/service/api/video.ts"; import {formatTime} from "@/util/strings.ts"; @@ -46,16 +44,7 @@ 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
{video.publish_time ? formatTime(video.publish_time) : ''}
+ >{video.ctime ? formatTime(video.ctime,'min') : '-'}
{/*{sortable && !generating && (!active ?*/} {/* : )}*/} +
+ {editable && !generating && <> + {onEdit && + } - {editable && !generating && <> - {onEdit && - } - - {onRemove && } - { - if (onCheckedChange) { - onCheckedChange(!state.checked) - } else { - setState({checked: !state.checked}) - } - }} /> - } + {onRemove && } + title={'你确定要删除吗?'} + description={`删除后需从重新${type == 'news' ? '生成' : '推流'}`} + onConfirm={onRemove} + >} + { + if (onCheckedChange) { + onCheckedChange(!state.checked) + } else { + setState({checked: !state.checked}) + } + }} /> + } +
diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx index c40c09c..6d052bf 100644 --- a/src/pages/live/index.tsx +++ b/src/pages/live/index.tsx @@ -33,7 +33,8 @@ export default function LiveIndex() { showToTop: false, checkedAll: false, originSort: '', - playProgress: 0 + playProgress: 0, + loading:false }) const activeIndex = useRef(state.activeIndex) useEffect(() => { @@ -111,6 +112,7 @@ 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 || [])) @@ -118,6 +120,8 @@ export default function LiveIndex() { originSort: res.list ? res.list.map(s => s.id).join(',') : '' }) setCheckedIdArray([]) + }).catch(showErrorToast).finally(()=>{ + setState({loading: false}) }); } @@ -191,14 +195,13 @@ export default function LiveIndex() { return checkedIdArray.filter(id => currentId.id != id) }, [checkedIdArray, state.activeIndex]) - return (
+ return (
{contextHolder} +
-
-
- 视频时长: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)} -
-
+
+
直播界面
+
+
+ 视频时长: {formatDuration(currentTotalDuration)} / {formatDuration(totalDuration)} +
-
-
+
+
当前{state.activeIndex == -1 ? '暂未播放' : `播放到${state.activeIndex}条`}, 共{videoData.length}条 @@ -250,6 +256,7 @@ export default function LiveIndex() {
setState({showToTop: top > 30})} onCallback={() => { }} @@ -274,6 +281,7 @@ 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) => { diff --git a/src/pages/news/components/search-panel.tsx b/src/pages/news/components/search-panel.tsx index 431e836..cc6e889 100644 --- a/src/pages/news/components/search-panel.tsx +++ b/src/pages/news/components/search-panel.tsx @@ -202,9 +202,10 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps)
{ - tags.filter(s => s.value !== 999999).map(it => ( -
s.value !== 999999).map(it => { + const currentPinned = pinnedTag?.includes(Number(it.value)); + return (
{ const value = Number(it.value) @@ -215,10 +216,11 @@ export default function SearchPanel({onSearch,defaultParams}: SearchPanelProps) } }}> {it.label} - {pinnedTag?.includes(Number(it.value)) && + {currentPinned && }
) - ) + }) + }
diff --git a/src/pages/news/components/style.module.scss b/src/pages/news/components/style.module.scss index 239bbc7..073e227 100644 --- a/src/pages/news/components/style.module.scss +++ b/src/pages/news/components/style.module.scss @@ -78,8 +78,9 @@ .header{ @apply bg-primary-bg; .col{ - + @apply text-sm; height: 42px; + } .operations{ } diff --git a/src/pages/news/edit.tsx b/src/pages/news/edit.tsx index 63b010f..05b7073 100644 --- a/src/pages/news/edit.tsx +++ b/src/pages/news/edit.tsx @@ -10,7 +10,7 @@ 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, IconEdit} from "@/components/icons"; +import {IconDelete, IconWarningCircle} 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"; @@ -58,11 +58,11 @@ export default function NewEdit() { setSelectedRowKeys([]) } } - const scrollerRef = useRef(null) - const handleDelete = (id)=>{ - deleteByIds([id]).then(()=>{ + const scrollerRef = useRef(null) + const handleDelete = (id) => { + deleteByIds([id]).then(() => { refresh() - showToast('删除成功','success') + showToast('删除成功', 'success') }).catch(showErrorToast) } @@ -83,9 +83,10 @@ export default function NewEdit() { { handleCheckAll(!state.checkAll) }}>全选 - { - handleCheckAll(e.target.checked) - }} /> + { + handleCheckAll(e.target.checked) + }}/>
@@ -102,7 +103,8 @@ 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}>
{data?.list?.map((item, i) => { const checked = selectedRowKeys.includes(item.id) @@ -129,9 +131,17 @@ export default function NewEdit() {
{/**/} - { - handleDelete(item.id) - }}> + } + title={'你确定要删除吗?'} + description={'删除后需从新闻素材中重新选择'} + onConfirm={() => { + handleDelete(item.id) + }} + >
- scrollerRef.current?.scrollToPosition(0)} /> - {selectedRowKeys?.length >0 && } + scrollerRef.current?.scrollToPosition(0)}/> + {selectedRowKeys?.length > 0 && }
diff --git a/src/pages/video/index.tsx b/src/pages/video/index.tsx index 914e822..12fd083 100644 --- a/src/pages/video/index.tsx +++ b/src/pages/video/index.tsx @@ -1,4 +1,4 @@ -import {Checkbox, Empty} from "antd"; +import {Checkbox, Empty, Space} from "antd"; import React, {useEffect, useMemo, useRef, useState} from "react"; import {DndContext} from "@dnd-kit/core"; import {arrayMove, SortableContext} from "@dnd-kit/sortable"; @@ -16,7 +16,6 @@ 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) @@ -32,12 +31,14 @@ export default function VideoIndex() { playState: { current: -1, total: -1 - } + }, + loading:false }) const [checkedIdArray, setCheckedIdArray] = useState([]) // 加载列表 const loadList = (needReset = true) => { + setState({loading: true}) getList().then((ret) => { const list = ret.list || [] setVideoData(list) @@ -50,6 +51,9 @@ export default function VideoIndex() { // 每5s重新获取一次最新数据 setTimeout(() => loadList(false), 5000) } + }).catch(showErrorToast) + .finally(()=>{ + setState({loading: false}) }) } @@ -112,12 +116,13 @@ export default function VideoIndex() { }).catch(showErrorToast) } - return (
+ return (
+
预览视频 - 点击视频列表播放
-
+
{ @@ -137,11 +142,15 @@ export default function VideoIndex() {
{formatDuration(state.playState.current)} / {formatDuration(state.playState.total)}
-
+
-
-
-
+
No.
@@ -159,7 +168,7 @@ export default function VideoIndex() {
- setState({showToTop: top > 30})}> + setState({showToTop: top > 30})}> { videoData.length == 0 ?
:
diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 4d0fada..347fb0d 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -98,6 +98,7 @@ declare interface VideoInfo { article_id: number; status: number; publish_time?: number|string; + ctime?: number|string; } // room live declare interface LiveVideoInfo { @@ -111,6 +112,7 @@ declare interface LiveVideoInfo { status: number; order_no: string; publish_time?: number|string; + ctime?: number|string; } declare interface LiveState{ diff --git a/tailwind.config.js b/tailwind.config.js index 9b69019..7c077de 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,6 +11,11 @@ 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', @@ -48,6 +53,9 @@ export default { }, backgroundColor: { ...themeConfig.colors, + }, + textColor: { + ...themeConfig.colors, } }, screens: {