diff --git a/src/assets/core.scss b/src/assets/core.scss index ceec141..e5cc379 100644 --- a/src/assets/core.scss +++ b/src/assets/core.scss @@ -8,12 +8,14 @@ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - --main-bg-color: #f6f6f6; + --main-bg-color: #f4f7fc; --brand-color: #43ABFF; --navigation-width: 100vw; --navigation-active-color: #ffe0e0; --app-header-header: 70px; - --container-width: 1440px; + --container-width: 1800px; + --header-z-index: 99999; + --message-z-index: 100001; } @tailwind base; @@ -158,12 +160,13 @@ max-height: calc(100vh - var(--app-header-header) - 200px); overflow: auto; } -.video-player{ - .video-js{ + +.video-player { + .video-js { @apply w-full h-full; max-width: 100%; max-height: 100%; - background:#fff; // hsl(210, 100%, 48%) + background: #fff; // hsl(210, 100%, 48%) } } @@ -171,4 +174,84 @@ @include media-breakpoint-down(md) { display: none; } +} + +.data-list-container { + height: calc(100vh - var(--app-header-header) - 300px); + overflow: auto; + + .data-list-container-inner { + + } +} + +// override antd style +.ant-message { + z-index: var(--message-z-index); +} + +.ant-modal-root { + .ant-modal-mask { + @apply bg-black/20; + backdrop-filter: blur(7px); + } + + .ant-modal { + .ant-modal-content { + background: #f2f2f2; + } + + .ant-modal-body { + padding: 20px; + } + } +} + +// 全局按钮 +.page-action { + @apply fixed right-10 bottom-10 flex flex-col gap-4; + button { + @apply border-0 min-w-[120px] h-[40px] rounded-3xl text-white bg-blue-500 pl-4; + .text { + flex: 1; + } + + &:hover { + @apply bg-blue-600; + } + + &:active { + @apply bg-blue-700; + } + + &:disabled { + @apply bg-gray-400; + } + + &.btn-info { + @apply bg-info text-gray-800; + .svg-icon { + @apply text-gray-800; + } + } + } +} + +.timer-select-container { + .timer-select-value { + @apply text-blue-500 px-4 cursor-pointer h-[31px]; + } + + .timer-select-options { + @apply rounded-xl py-1 overflow-hidden drop-shadow absolute inset-x-0 top-[30px]; + background: linear-gradient(180deg,rgb(244, 247, 252) 0%, rgb(217, 232, 255) 100%); + } + + .timer-select-option-item { + @apply py-1.5 px-4 cursor-pointer text-gray-800 hover:text-blue-500; + &.selected{ + @apply text-blue-500; + } + } + } \ No newline at end of file diff --git a/src/assets/index.scss b/src/assets/index.scss index 9d7b6c9..b302741 100644 --- a/src/assets/index.scss +++ b/src/assets/index.scss @@ -40,7 +40,8 @@ body { } .app-header { - @apply w-full navigation-container flex justify-between items-center p-basic fixed top-0 inset-x-0 z-10; + @apply w-full navigation-container flex justify-between items-center p-basic fixed top-0 inset-x-0; + z-index: var(--header-z-index); height: var(--app-header-header); } @@ -57,8 +58,8 @@ body { } .container { - max-width: 90%; - width: var(--container-width, 1200px); + max-width: 95%; + width: var(--container-width, 1800px); margin: 0 auto; } diff --git a/src/components/form/time-select.tsx b/src/components/form/time-select.tsx new file mode 100644 index 0000000..1874ebf --- /dev/null +++ b/src/components/form/time-select.tsx @@ -0,0 +1,65 @@ +import {useMemo, useState} from "react"; +import {CaretUpOutlined} from "@ant-design/icons"; + +export type TimeSelectProps = { + value: number; + className?: string; + onChange: (value: number) => void; +} +type OptionItem = { + label: string; + value: number; +} +const AllTimeOption: OptionItem[] = [ + { + label: '半小时内', + value: 1 + }, + { + label: '一小时内', + value: 2 + }, + { + label: '四小时内', + value: 3 + }, + { + label: '一天内', + value: 4 + }, + { + label: '近一周', + value: 5 + }, + { + label: '所有时间', + value: 0 + } +] +const TimeSelect = (props: TimeSelectProps) => { + const selectLabel = useMemo(() => { + return AllTimeOption.find(item => item.value == props.value)?.label || '' + }, [props.value]) + const [visible, setVisible] = useState(false); + const handleClick = (item: OptionItem) => { + setVisible(false) + props.onChange(item.value) + } + + return (
setVisible(false)}> +
setVisible(true)}> +
+ {selectLabel} + +
+
+
+ {AllTimeOption.map((item, index) => { + return
handleClick(item)}>{item.label}
+ })} +
+
) +} +export default TimeSelect \ No newline at end of file diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx index 66a2c34..04c873f 100644 --- a/src/components/icons/index.tsx +++ b/src/components/icons/index.tsx @@ -3,7 +3,7 @@ import React from "react"; type IconProps = { style?: React.CSSProperties; className?: string; } export const IconNavigationArrow = ({style, className}: IconProps) => ( - @@ -16,6 +16,13 @@ export const IconNavigationArrow = ({style, className}: IconProps) => ( ) +export const IconArrowRight = ({style, className}: IconProps) => ( + + + +) + export const IconCopy = ({style, className}: IconProps) => ( ( ) export const IconDownload = ({style, className}: IconProps) => ( + fill="none" version="1.1" width="1em" height="1em" viewBox="0 0 20 24"> + ) +export const IconPin = ({style, className}: IconProps) => ( + + + + + +) export const IconDelete = ({style, className}: IconProps) => ( diff --git a/src/components/scoller/infinite-scroller.tsx b/src/components/scoller/infinite-scroller.tsx new file mode 100644 index 0000000..e25bd78 --- /dev/null +++ b/src/components/scoller/infinite-scroller.tsx @@ -0,0 +1,45 @@ +import React, {useEffect} from "react"; +import {useInViewport} from "ahooks"; + +export type InfiniteScrollerProps = { + children?: React.ReactNode; + className?: string; + rootClassName?: string; + loadingPlaceholder?: React.ReactNode; + onCallback: (page: number, prevPage) => void; + empty?: React.ReactNode; + loading?: boolean; + pagination?: { + page: number; + limit: number; + total: number; + }; +} + +export default function InfiniteScroller(props: InfiniteScrollerProps) { + const {pagination} = props; + const [inView] = useInViewport(() => document.querySelector('.data-load-control-element')) + + useEffect(() => { + if (!pagination) return; + if (inView && !props.loading && pagination.total > 0) { + const maxPage = Math.ceil((pagination.total || 0) / pagination.limit) + const currentPage = pagination.page + if (maxPage > currentPage) { + props.onCallback(currentPage + 1, currentPage) + } + } + }, [inView]) + return (
+
{props.children}
+ {props?.pagination && props.pagination.total > props.pagination.limit * props.pagination.page && (props.loadingPlaceholder || +
+
加载中...
+
)} + {props?.empty && props.pagination?.total == 0 &&
+
+ {props.empty} +
+
} +
); +} \ No newline at end of file diff --git a/src/hooks/useInfiniteScroller.tsx b/src/hooks/useInfiniteScroller.tsx new file mode 100644 index 0000000..af4010c --- /dev/null +++ b/src/hooks/useInfiniteScroller.tsx @@ -0,0 +1,3 @@ +function useInfiniteScroller(fetch: (page: number) => Promise, deps: any[]) { + +} \ No newline at end of file diff --git a/src/pages/news/components/button-news-download.tsx b/src/pages/news/components/button-news-download.tsx index bbfa8ac..6c0129c 100644 --- a/src/pages/news/components/button-news-download.tsx +++ b/src/pages/news/components/button-news-download.tsx @@ -6,6 +6,7 @@ import {useState} from "react"; import {getById} from "@/service/api/news.ts"; import {showToast} from "@/components/message.ts"; +import {IconDownload} from "@/components/icons"; /** @@ -58,11 +59,7 @@ async function downloadAsZip(list: NewsInfo[]) { }) const content = await zip.generateAsync({type: "blob"}); saveAs(content, "news.zip"); - // .then(function (content) { - // - // }).finally(() => { - // setLoading(false) - // }); + } export default function ButtonNewsDownload(props: { ids: Id[] }) { @@ -81,9 +78,15 @@ export default function ButtonNewsDownload(props: { ids: Id[] }) { } finally { setLoading(false) } - } return ( - + ) } diff --git a/src/pages/news/components/button-push-news2article.tsx b/src/pages/news/components/button-push-news2article.tsx index 312f285..14b6be3 100644 --- a/src/pages/news/components/button-push-news2article.tsx +++ b/src/pages/news/components/button-push-news2article.tsx @@ -1,9 +1,11 @@ -import {Button, Modal} from "antd"; +import {App, Button} from "antd"; import {showToast} from "@/components/message.ts"; import {useState} from "react"; import {push2article} from "@/service/api/news.ts"; +import {IconArrowRight} from "@/components/icons"; export default function ButtonPushNews2Article(props: { ids: Id[] }) { + const {modal} = App.useApp(); const [loading,setLoading] = useState(false) const handlePush = () => { setLoading(true) @@ -20,17 +22,22 @@ export default function ButtonPushNews2Article(props: { ids: Id[] }) { showToast('请选择要推送的新闻', 'warning') return } - Modal.confirm({ + modal.confirm({ title: '操作提示', content: '是否确定推入素材编辑界面?', - onOk: handlePush + onOk: handlePush, + centered: true }) } return ( + className='btn-action' + icon={} + iconPosition={'end'} + > + 推入编辑 + ) } \ No newline at end of file diff --git a/src/pages/news/components/pinned.ts b/src/pages/news/components/pinned.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/news/components/search-panel.tsx b/src/pages/news/components/search-panel.tsx index 21175e7..fd5b615 100644 --- a/src/pages/news/components/search-panel.tsx +++ b/src/pages/news/components/search-panel.tsx @@ -1,15 +1,20 @@ -import {Button, Input, Select} from "antd"; -import {useSetState} from "ahooks"; -import {useState} from "react"; +import {Input} from "antd"; +import {useBoolean, useLocalStorageState, useSetState} from "ahooks"; +import {useMemo, useState} from "react"; import useArticleTags from "@/hooks/useArticleTags.ts"; -import {SearchListTimes} from "@/pages/news/components/news-source.ts"; +import {CloseOutlined, MenuOutlined, SearchOutlined} from "@ant-design/icons"; +import TimeSelect from "@/components/form/time-select.tsx"; + +import styles from './style.module.scss' +import {clsx} from "clsx"; +import {IconPin} from "@/components/icons"; type SearchPanelProps = { onSearch?: (params: ApiArticleSearchParams) => void; } const pagination = { - limit: 10, page: 1 + limit: 12, page: 1 } const DEFAULT_STATE = { tag_level_1_id: -1, @@ -18,96 +23,164 @@ const DEFAULT_STATE = { } export default function SearchPanel({onSearch}: SearchPanelProps) { const tags = useArticleTags(); + const [panelVisible, {setTrue, setFalse}] = useBoolean(false) const [params, setParams] = useSetState({ - pagination + pagination, }); + const [prevSearchName, setPrevSearchName] = useState() const [state, setState] = useSetState<{ tag_level_1_id: number; tag_level_2_id: number; subOptions: (string | number)[] }>({...DEFAULT_STATE}) + const [pinnedTag, setPinnedTag] = useLocalStorageState( + 'user-pinned-tag-list', + { + defaultValue: [], + }, + ); // 二级分类 const [subOptions, setSubOptions] = useState([]) const onFinish = () => { + if (params.title == prevSearchName || (!params.title && !prevSearchName)) return + params.title = prevSearchName; + setParams({title: prevSearchName}) onSearch?.({ ...params, - tag_level_1_id: state.tag_level_1_id > 0?state.tag_level_1_id:undefined, - tag_level_2_id: state.tag_level_2_id > 0?state.tag_level_2_id:undefined, + title: prevSearchName, + tag_level_1_id: state.tag_level_1_id > 0 ? state.tag_level_1_id : undefined, + tag_level_2_id: state.tag_level_2_id > 0 ? state.tag_level_2_id : undefined, pagination }) } - // 重置 - const onReset = () => { - setParams({pagination, title: ''}) - setState({...DEFAULT_STATE}) - setSubOptions([]) - onSearch?.({pagination}) + const handleTimeFilter = (time_flag: number) => { + const searchParams = { + ...params, + time_flag, + pagination + } + setParams(searchParams) + onSearch?.(searchParams) } + const handleFilter = (_params: Partial) => { + const searchParams = { + ...params, + ..._params, + pagination + } + setParams(searchParams) + setState({ + ...state, + tag_level_1_id: _params.tag_level_1_id || -1, + tag_level_2_id: _params.tag_level_2_id || -1, + }) + onSearch?.(searchParams) + } + const pinnedList = useMemo(() => { + if (tags?.length > 0) { + const pinnedList = pinnedTag && pinnedTag?.length > 0 ? pinnedTag : tags.map(s => s.value) + return pinnedList.filter(it => tags.findIndex(s => s.value == it) != -1) + .sort((a, b) => a - b) + .map(it => (tags.find(s => s.value == it) as OptionItem)) + } + return [] as OptionItem[]; + }, [pinnedTag, tags]) - return (
+ return (
setParams({title: e.target.value})} - className="w-[240px]" - placeholder={'请输入新闻标题开始查找新闻'} + value={prevSearchName} + onChange={e => setPrevSearchName(e.target.value)} + className="w-[250px] rounded-3xl" + placeholder={'请输入新闻标题关键词进行搜索'} + onPressEnter={onFinish} + onBlur={onFinish} + shape="round" + prefix={} + /> + -
- 更新时间 -