💄 新闻素材更新
This commit is contained in:
parent
d0d84e61ed
commit
bea93d9094
@ -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%)
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,3 +175,83 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
65
src/components/form/time-select.tsx
Normal file
65
src/components/form/time-select.tsx
Normal file
@ -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<boolean>(false);
|
||||
const handleClick = (item: OptionItem) => {
|
||||
setVisible(false)
|
||||
props.onChange(item.value)
|
||||
}
|
||||
|
||||
return (<div className={`timer-select-container relative group ${props.className}`}
|
||||
onMouseLeave={() => setVisible(false)}>
|
||||
<div className={`timer-select-value flex items-center`} onMouseEnter={() => setVisible(true)}>
|
||||
<div>
|
||||
<span>{selectLabel}</span>
|
||||
<CaretUpOutlined className={'ml-2 arrow-icon rotate-180 group-hover:rotate-0'} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`timer-select-options z-10 ${visible ? 'block' : 'hidden'}`}>
|
||||
{AllTimeOption.map((item, index) => {
|
||||
return <div className={`timer-select-option-item ${item.value == props.value?'selected':''}`} key={index}
|
||||
onClick={() => handleClick(item)}>{item.label}</div>
|
||||
})}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
export default TimeSelect
|
@ -3,7 +3,7 @@ import React from "react";
|
||||
type IconProps = { style?: React.CSSProperties; className?: string; }
|
||||
|
||||
export const IconNavigationArrow = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-copy`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
<svg className={`svg-icon ${className || ''} icon-nav-arrow`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="0.9em" height="1em" viewBox="0 0 25 29">
|
||||
<path d="M24.75 14.2894L-1.24922e-06 28.5788L11.75 14.2891L0 -1.08186e-06L24.75 14.2894Z"
|
||||
fill="url(#paint0_linear_1030_7835)"/>
|
||||
@ -16,6 +16,13 @@ export const IconNavigationArrow = ({style, className}: IconProps) => (
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
export const IconArrowRight = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-arrow-right`} style={style} width="1em" height="1em"
|
||||
viewBox="0 0 20 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 11.5L9.01987e-07 23L9.49495 11.4997L1.90735e-06 -8.74228e-07L20 11.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
|
||||
export const IconCopy = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-copy`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
@ -44,14 +51,22 @@ export const IconShare = ({style, className}: IconProps) => (
|
||||
)
|
||||
export const IconDownload = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-download`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" version="1.1"
|
||||
width="1em" height="1em" viewBox="0 0 16 16">
|
||||
fill="none" version="1.1" width="1em" height="1em" viewBox="0 0 20 24">
|
||||
<path
|
||||
d="M7.88736 10.6575C7.90072 10.6745 7.9178 10.6883 7.93729 10.6978C7.95678 10.7073 7.97818 10.7123 7.99986 10.7123C8.02154 10.7123 8.04294 10.7073 8.06243 10.6978C8.08192 10.6883 8.099 10.6745 8.11236 10.6575L10.1124 8.1271C10.1856 8.03424 10.1195 7.89674 9.99986 7.89674H8.67665V1.85389C8.67665 1.77531 8.61236 1.71103 8.53379 1.71103H7.46236C7.38379 1.71103 7.3195 1.77531 7.3195 1.85389V7.89496H5.99986C5.88022 7.89496 5.81415 8.03246 5.88736 8.12532L7.88736 10.6575ZM14.5356 10.0325H13.4641C13.3856 10.0325 13.3213 10.0967 13.3213 10.1753V12.9253H2.67843V10.1753C2.67843 10.0967 2.61415 10.0325 2.53557 10.0325H1.46415C1.38557 10.0325 1.32129 10.0967 1.32129 10.1753V13.711C1.32129 14.0271 1.57665 14.2825 1.89272 14.2825H14.107C14.4231 14.2825 14.6784 14.0271 14.6784 13.711V10.1753C14.6784 10.0967 14.6141 10.0325 14.5356 10.0325Z"
|
||||
d="M16.5571 8.47059H14.2857V1.41176C14.2857 0.635294 13.6429 0 12.8571 0H7.14286C6.35714 0 5.71429 0.635294 5.71429 1.41176V8.47059H3.44286C2.17143 8.47059 1.52857 9.99529 2.42857 10.8847L8.98571 17.3647C9.54286 17.9153 10.4429 17.9153 11 17.3647L17.5571 10.8847C18.4571 9.99529 17.8286 8.47059 16.5571 8.47059ZM0 22.5882C0 23.3647 0.642857 24 1.42857 24H18.5714C19.3571 24 20 23.3647 20 22.5882C20 21.8118 19.3571 21.1765 18.5714 21.1765H1.42857C0.642857 21.1765 0 21.8118 0 22.5882Z"
|
||||
fill="currentColor"/>
|
||||
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const IconPin = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-download`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" version="1.1" width="0.6em" height="1em" viewBox="0 0 12 21">
|
||||
|
||||
<path d="M10 10.5V2.5H11V0.5H1V2.5H2V10.5L0 12.5V14.5H5.2V20.5H6.8V14.5H12V12.5L10 10.5ZM2.8 12.5L4 11.3V2.5H8V11.3L9.2 12.5H2.8Z" fill="currentColor"/>
|
||||
|
||||
</svg>
|
||||
)
|
||||
export const IconDelete = ({style, className}: IconProps) => (
|
||||
<svg className={`svg-icon ${className || ''} icon-delete`} style={style} xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em" height="1em" viewBox="0 0 1024 1024" version="1.1">
|
||||
|
45
src/components/scoller/infinite-scroller.tsx
Normal file
45
src/components/scoller/infinite-scroller.tsx
Normal file
@ -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 (<div className={`data-list-container ${props.rootClassName}`}>
|
||||
<div className={`data-list-container-inner ${props.className}`}>{props.children}</div>
|
||||
{props?.pagination && props.pagination.total > props.pagination.limit * props.pagination.page && (props.loadingPlaceholder ||
|
||||
<div className="data-load-control-element py-10 text-center">
|
||||
<div className="loading-text">加载中...</div>
|
||||
</div>)}
|
||||
{props?.empty && props.pagination?.total == 0 && <div className="flex justify-center text-center pt-20">
|
||||
<div className=" rounded-lg px-4 py-10">
|
||||
{props.empty}
|
||||
</div>
|
||||
</div>}
|
||||
</div>);
|
||||
}
|
3
src/hooks/useInfiniteScroller.tsx
Normal file
3
src/hooks/useInfiniteScroller.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
function useInfiniteScroller<T>(fetch: (page: number) => Promise<T[]>, deps: any[]) {
|
||||
|
||||
}
|
@ -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 (
|
||||
<Button loading={loading} onClick={() => onDownloadClick(props.ids)}>下载</Button>
|
||||
<Button
|
||||
className={'btn-info'}
|
||||
loading={loading} onClick={() => onDownloadClick(props.ids)}
|
||||
icon={<IconDownload className={'text-white'}/>}
|
||||
iconPosition={'end'}
|
||||
>
|
||||
<span className="text">下载</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
@ -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 (
|
||||
<Button
|
||||
type={'primary'}
|
||||
loading={loading}
|
||||
onClick={onPushClick}
|
||||
>推入素材编辑界面</Button>
|
||||
className='btn-action'
|
||||
icon={<IconArrowRight className={'text-white'} />}
|
||||
iconPosition={'end'}
|
||||
>
|
||||
<span className={'text'}>推入编辑</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
0
src/pages/news/components/pinned.ts
Normal file
0
src/pages/news/components/pinned.ts
Normal file
@ -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,92 +23,160 @@ const DEFAULT_STATE = {
|
||||
}
|
||||
export default function SearchPanel({onSearch}: SearchPanelProps) {
|
||||
const tags = useArticleTags();
|
||||
const [panelVisible, {setTrue, setFalse}] = useBoolean(false)
|
||||
const [params, setParams] = useSetState<ApiArticleSearchParams>({
|
||||
pagination
|
||||
pagination,
|
||||
});
|
||||
const [prevSearchName, setPrevSearchName] = useState<string>()
|
||||
|
||||
const [state, setState] = useSetState<{
|
||||
tag_level_1_id: number;
|
||||
tag_level_2_id: number;
|
||||
subOptions: (string | number)[]
|
||||
}>({...DEFAULT_STATE})
|
||||
const [pinnedTag, setPinnedTag] = useLocalStorageState<number[]>(
|
||||
'user-pinned-tag-list',
|
||||
{
|
||||
defaultValue: [],
|
||||
},
|
||||
);
|
||||
|
||||
// 二级分类
|
||||
const [subOptions, setSubOptions] = useState<OptionItem[]>([])
|
||||
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<ApiArticleSearchParams>) => {
|
||||
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 (<div className={'search-panel'}>
|
||||
return (<div className={`${styles.searchPanel} pt-8 pb-2`}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="search-form flex items-center gap-4">
|
||||
<Input
|
||||
value={params.title}
|
||||
onChange={e => 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={<SearchOutlined/>}
|
||||
/>
|
||||
<div className={'flex items-center ml-2'}>
|
||||
<span className="text-sm whitespace-nowrap mr-1">更新时间</span>
|
||||
<Select
|
||||
className="w-[150px]"
|
||||
<TimeSelect
|
||||
className="w-[120px] ml-1"
|
||||
value={params.time_flag || 0}
|
||||
onChange={value => setParams({time_flag: value})}
|
||||
options={SearchListTimes}
|
||||
optionRender={(option) => (
|
||||
<div className="flex items-center">
|
||||
<span role="icon" className={`radio-icon`}></span>
|
||||
<span role="listitem" aria-label={String(option.label)}>{option.label}</span>
|
||||
</div>
|
||||
)}
|
||||
onChange={handleTimeFilter}
|
||||
/>
|
||||
</div>
|
||||
<Button type={'primary'} onClick={onFinish}>搜索</Button>
|
||||
<Button onClick={onReset}>重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="filter-container flex items-start mt-5">
|
||||
<div className="list-container flex-1">
|
||||
<div className="news-source-lv-1 flex flex-wrap">
|
||||
<div className="filter-container mt-5 h-[40px]">
|
||||
<div className="list-container relative">
|
||||
<div className="justify-between flex items-center border-b pb-2 overflow-hidden">
|
||||
<div
|
||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_1_id == -1 ? 'bg-blue-500 text-white' : 'hover:bg-gray-100'}`}
|
||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_1_id == -1 ? 'selected' : ''}`}
|
||||
onClick={() => {
|
||||
setState({...DEFAULT_STATE})
|
||||
handleFilter({tag_level_1_id: -1, tag_level_2_id: -1})
|
||||
setSubOptions([])
|
||||
}}>全部</div>
|
||||
{
|
||||
tags.filter(s=>s.value !== 999999).map(it => (
|
||||
<div
|
||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_1_id == it.value ? 'bg-blue-500 text-white' : 'hover:bg-gray-100'}`}
|
||||
}}>全部
|
||||
</div>
|
||||
|
||||
<div className="pinned-tag-list flex flex-wrap overflow-hidden flex-1 min-w-0 h-[32px]">
|
||||
{pinnedList.filter(s => (Number(s.value) !== 999999)).map(it => (
|
||||
<span
|
||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_1_id == it.value ? 'selected' : ''}`}
|
||||
key={it.value}
|
||||
onClick={() => {
|
||||
setState({tag_level_1_id: Number(it.value),tag_level_2_id:-1})
|
||||
handleFilter({tag_level_1_id: Number(it.value), tag_level_2_id: -1})
|
||||
setSubOptions(it.children || [])
|
||||
}}>{it.label}</div>)
|
||||
}}>{it.label}</span>)
|
||||
)}
|
||||
</div>
|
||||
<div className="pinned-menu ">
|
||||
<span className={'cursor-pointer block hover:text-blue-500'} onClick={setTrue}>
|
||||
<MenuOutlined style={{fontSize: 20}}/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 固定新闻来源 */}
|
||||
<div className={clsx(styles.pinnedManagePanel, panelVisible ? 'block' : 'hidden')}>
|
||||
<div className="header flex justify-between">
|
||||
<div className="title font-bold">新闻来源</div>
|
||||
<span className={'cursor-pointer block hover:text-blue-500'} onClick={setFalse}><CloseOutlined
|
||||
style={{fontSize: 20}}/></span>
|
||||
</div>
|
||||
<div className="tags-list-container">
|
||||
{
|
||||
tags.filter(s => s.value !== 999999).map(it => (
|
||||
<div
|
||||
className={`filter-item border flex items-center px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded`}
|
||||
key={it.value}
|
||||
onClick={() => {
|
||||
const value = Number(it.value)
|
||||
if (pinnedTag && pinnedTag.includes(value)) {
|
||||
setPinnedTag(pinnedTag.filter(s => s != value))
|
||||
} else {
|
||||
setPinnedTag([...(pinnedTag || []), value])
|
||||
}
|
||||
}}>
|
||||
<span>{it.label}</span>
|
||||
{pinnedTag?.includes(Number(it.value)) &&
|
||||
<span className={'ml-2'}><IconPin/></span>}
|
||||
</div>)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{state.tag_level_1_id != -1 && subOptions.length > 0 && <div className="news-source-lv-2 bg-gray-100 p-2 rounded mt-2 flex flex-wrap">
|
||||
</div>
|
||||
{/* 二级目录 */}
|
||||
{state.tag_level_1_id != -1 && subOptions.length > 0 &&
|
||||
<div
|
||||
className="absolute news-source-lv-2 flex items-center absolute left-0 right-0">
|
||||
{
|
||||
subOptions.map(it => (
|
||||
<div
|
||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.tag_level_2_id == it.value ? 'bg-blue-500 text-white' : 'hover:bg-gray-100'}`}
|
||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded text-gray-400 ${state.tag_level_2_id == it.value ? 'text-black' : 'hover:text-gray-600'}`}
|
||||
key={it.value}
|
||||
onClick={() => {
|
||||
setState({tag_level_2_id: Number(it.value)})
|
||||
handleFilter({tag_level_1_id:state.tag_level_1_id,tag_level_2_id: Number(it.value)})
|
||||
}}>{it.label}</div>)
|
||||
)
|
||||
}
|
||||
|
31
src/pages/news/components/style.module.scss
Normal file
31
src/pages/news/components/style.module.scss
Normal file
@ -0,0 +1,31 @@
|
||||
.searchPanel {
|
||||
:global {
|
||||
.filter-item {
|
||||
@apply relative mr-5;
|
||||
&:after {
|
||||
@apply absolute bottom-1 left-0 w-full;
|
||||
height: 6px;
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
&.selected:after {
|
||||
background: linear-gradient(to top, rgba(28, 122, 255,1),rgba(28, 122, 255,0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pinnedManagePanel {
|
||||
@apply absolute bg-white top-0 px-4 pt-2 pb-4 rounded shadow-md z-10;
|
||||
inset-inline: -20px;
|
||||
|
||||
:global {
|
||||
.btn-panel {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
.tags-list-container {
|
||||
@apply flex flex-wrap gap-2 mt-2;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +1,20 @@
|
||||
import {useState} from "react";
|
||||
import {Checkbox, Empty, Modal, Pagination, Space} from "antd";
|
||||
import {Checkbox, Divider, Empty, Modal, Pagination} from "antd";
|
||||
import {useRequest} from "ahooks";
|
||||
|
||||
import {Card} from "@/components/card";
|
||||
|
||||
import SearchPanel from "@/pages/news/components/search-panel.tsx";
|
||||
import styles from './style.module.scss'
|
||||
import {getById,getList} from "@/service/api/news.ts";
|
||||
import {getById, getList} from "@/service/api/news.ts";
|
||||
import {showLoading} from "@/components/message.ts";
|
||||
import {formatTime} from "@/util/strings.ts";
|
||||
import ButtonPushNews2Article from "@/pages/news/components/button-push-news2article.tsx";
|
||||
import ButtonNewsDownload from "@/pages/news/components/button-news-download.tsx";
|
||||
import {clsx} from "clsx";
|
||||
import InfiniteScroller from "@/components/scoller/infinite-scroller.tsx";
|
||||
|
||||
export default function NewsIndex() {
|
||||
|
||||
const [params, setParams] = useState<ApiArticleSearchParams>({
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 10
|
||||
}
|
||||
pagination: {page: 1, limit: 12}
|
||||
})
|
||||
const [checkedId, setCheckedId] = useState<Id[]>([])
|
||||
const [activeNews, setActiveNews] = useState<NewsInfo>()
|
||||
@ -26,11 +22,21 @@ export default function NewsIndex() {
|
||||
const [state, setState] = useState<{
|
||||
checkAll?: boolean;
|
||||
}>({})
|
||||
const {data} = useRequest(() => getList(params), {
|
||||
|
||||
const [data, setData] = useState<DataList<ListCrawlerNewsItem>>();
|
||||
const {loading} = useRequest(() => getList(params), {
|
||||
refreshDeps: [params],
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data) => {
|
||||
if (params.pagination.page === 1) {
|
||||
setData(_data)
|
||||
setCheckedId([])
|
||||
setState({checkAll:false})
|
||||
setState({checkAll: false})
|
||||
} else {
|
||||
setData({
|
||||
pagination: _data.pagination,
|
||||
list: [...(data?.list || []), ..._data.list]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -44,94 +50,110 @@ export default function NewsIndex() {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return (<div className={'container pb-5'}>
|
||||
<Card className="search-panel-container my-5">
|
||||
<SearchPanel onSearch={setParams}/>
|
||||
</Card>
|
||||
<Card className="news-list-container">
|
||||
{activeNews && <Modal open={true} width={1000} footer={null} onCancel={() => setActiveNews(undefined)}>
|
||||
<div className="news-detail px-3 pb-5">
|
||||
<div className="new-title text-2xl">{activeNews?.title}</div>
|
||||
<div className="info mt-2 mb-5 text-sm flex gap-3">
|
||||
<span className="source text-blue-700">{activeNews?.media_name}</span>
|
||||
<span className="create-time text-gray-400">{formatTime(activeNews?.publish_time)}</span>
|
||||
</div>
|
||||
<div className="overflow-auto leading-7 text-base"
|
||||
style={{maxHeight: 1000}} dangerouslySetInnerHTML={{__html: activeNews?.content || ''}}></div>
|
||||
</div>
|
||||
</Modal>}
|
||||
<div className="controls flex justify-between mb-1">
|
||||
<div>
|
||||
<Checkbox checked={state.checkAll} onChange={e => {
|
||||
setState({checkAll: e.target.checked})
|
||||
if (e.target.checked) {
|
||||
const handleCheckAll = (checked: boolean) => {
|
||||
setState({checkAll: checked})
|
||||
if (checked) {
|
||||
setCheckedId(data?.list?.map(item => item.id) || [])
|
||||
} else {
|
||||
setCheckedId([])
|
||||
}
|
||||
}}>全选</Checkbox>
|
||||
}
|
||||
|
||||
|
||||
return (<div className={'container pb-5'}>
|
||||
<SearchPanel onSearch={setParams}/>
|
||||
{activeNews && <Modal open={true} centered width={1000} footer={null} onCancel={() => setActiveNews(undefined)}>
|
||||
<div className="news-detail px-3 pb-5">
|
||||
<div className="px-4 py-6 bg-white">
|
||||
<div className="new-title text-2xl">{activeNews?.title}</div>
|
||||
<Divider className={'my-4'}/>
|
||||
<div className="info mt-2 mb-5 text-sm flex gap-3">
|
||||
<span className="source text-blue-400">{activeNews?.media_name}</span>
|
||||
<span className="create-time text-gray-400">{formatTime(activeNews?.publish_time)}</span>
|
||||
</div>
|
||||
<Space size={10}>
|
||||
<ButtonPushNews2Article ids={checkedId}/>
|
||||
<ButtonNewsDownload ids={checkedId}/>
|
||||
</Space>
|
||||
<div className="overflow-auto leading-7 text-base"
|
||||
style={{maxHeight: 500}} dangerouslySetInnerHTML={{__html: activeNews?.content || ''}}></div>
|
||||
</div>
|
||||
<div className={styles.newsList}>
|
||||
</div>
|
||||
</Modal>}
|
||||
<div className="news-list-container">
|
||||
<div className="controls flex justify-end mb-3 gap-2">
|
||||
<div className={'text-blue-500'}>已选 {checkedId.length} 条</div>
|
||||
<div>
|
||||
<span className={'inline-block cursor-pointer mr-2'} onClick={() => {
|
||||
handleCheckAll(!state.checkAll)
|
||||
}}>全选</span>
|
||||
<Checkbox checked={state.checkAll} onChange={e => {
|
||||
handleCheckAll(e.target.checked)
|
||||
}}></Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<InfiniteScroller
|
||||
className="grid grid-cols-3 gap-4 lg:grid-cols-4"
|
||||
pagination={data?.pagination}
|
||||
loading={loading}
|
||||
onCallback={(page) => {
|
||||
setParams({...params, pagination: {...params.pagination, page}})
|
||||
}}
|
||||
empty={<Empty/>}
|
||||
>
|
||||
{data?.list?.map(item => (
|
||||
<div key={item.id} className={`py-3 flex items-start border-b border-gray-100 group`}>
|
||||
<div key={item.id}
|
||||
className={clsx(`p-4 flex items-start group rounded border border-transparent`, {
|
||||
'bg-news-to-edit': item.internal_article_id > 0,
|
||||
'bg-white': !item.internal_article_id && !checkedId.includes(item.id),
|
||||
'bg-blue-500/20 border-[#d9eaff]': checkedId.includes(item.id)
|
||||
})}>
|
||||
|
||||
<div className="news-content flex-1">
|
||||
<div className="title h-[60px] line-clamp-2 text-lg cursor-pointer hover:text-blue-500"
|
||||
onClick={() => {
|
||||
handleViewNewsDetail(item.id)
|
||||
}}>{item.title}</div>
|
||||
<div className="content flex gap-3 mt-2 mb-3">
|
||||
<div
|
||||
className={`checkbox mt-[2px] mr-2 ${checkedId.includes(item.id) ? '' : 'opacity-0'} group-hover:opacity-100`}>
|
||||
{item.internal_article_id > 0 ? <span className={"inline-block w-[16px] " }></span> :<Checkbox checked={checkedId.includes(item.id)} onChange={() => {
|
||||
className="text text-gray-600 text-sm h-[70px] line-clamp-3 overflow-hidden text-ellipsis text-justify leading-6 flex-1 text-justify break-all text-wrap">
|
||||
{item.summary}
|
||||
</div>
|
||||
{item.cover && <div
|
||||
className="cover border border-gray-100 flex items-center rounded overflow-hidden"
|
||||
style={{width: 100, height: 70}}>
|
||||
<img className="w-full h-full object-cover" src={item.cover}/>
|
||||
</div>}
|
||||
</div>
|
||||
<div className="info text-gray-400 mt-4 text-sm">
|
||||
<div>来源: <span>{item.media_name}</span></div>
|
||||
<div className="extras flex items-center justify-between gap-3">
|
||||
<div><span>{formatTime(item.publish_time)}</span></div>
|
||||
<div
|
||||
className={`checkbox mt-[2px]`}>
|
||||
{item.internal_article_id > 0 ?
|
||||
<span className={"inline-block w-[16px] "}></span> :
|
||||
<Checkbox checked={checkedId.includes(item.id)} onChange={() => {
|
||||
if (checkedId.includes(item.id)) {
|
||||
setCheckedId(checkedId.filter(id => id != item.id))
|
||||
} else {
|
||||
setCheckedId([...checkedId, item.id])
|
||||
}
|
||||
}}/> }
|
||||
</div>
|
||||
<div className="news-content flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="title text-lg cursor-pointer" onClick={() => {
|
||||
handleViewNewsDetail(item.id)
|
||||
}}>{item.title}</div>
|
||||
{item.internal_article_id > 0 &&
|
||||
<div className="text-sm text-blue-500">已加入编辑界面</div>}
|
||||
</div>
|
||||
<div className="content flex gap-3 mt-2 mb-3">
|
||||
{item.cover && <div
|
||||
className="cover border border-gray-100 flex items-center rounded overflow-hidden"
|
||||
style={{width: 100, height: 100}}>
|
||||
<img className="w-full h-full object-cover" src={item.cover}/>
|
||||
</div>}
|
||||
<div className="text text-gray-600 text-sm leading-6 flex-1 text-justify">
|
||||
{item.summary}
|
||||
}}/>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="info text-gray-300 flex items-center justify-between gap-3 text-sm">
|
||||
<div>来源: <span>{item.media_name}</span></div>
|
||||
{/*<Divider type="vertical" />*/}
|
||||
<div>发布时间: <span>{formatTime(item.publish_time)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</InfiniteScroller>
|
||||
|
||||
|
||||
</div>
|
||||
<div className="page-action">
|
||||
<div>
|
||||
<ButtonNewsDownload ids={checkedId}/>
|
||||
</div>
|
||||
<div>
|
||||
<ButtonPushNews2Article ids={checkedId}/>
|
||||
</div>
|
||||
|
||||
{data?.pagination && data?.pagination.total > 0 ? <div className="flex justify-center mt-10">
|
||||
<Pagination
|
||||
current={params.pagination.page}
|
||||
total={data?.pagination.total}
|
||||
pageSize={data?.pagination.limit}
|
||||
showSizeChanger={false}
|
||||
simple={true}
|
||||
rootClassName={'simple-pagination'}
|
||||
onChange={(page) => setParams(prev=>({...prev,pagination: {page, limit: 10}}))}
|
||||
/>
|
||||
</div> : <div className="py-10">
|
||||
<Empty />
|
||||
</div>
|
||||
}
|
||||
</Card>
|
||||
</div>)
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.newsList{
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import {createBrowserRouter, RouterProvider,} from "react-router-dom";
|
||||
import {Suspense,} from "react";
|
||||
import {ConfigProvider} from "antd";
|
||||
import {ConfigProvider,App} from "antd";
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
// for date-picker i18n
|
||||
import 'dayjs/locale/zh-cn';
|
||||
@ -32,11 +32,14 @@ const AppRouter = () => {
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
}}
|
||||
>
|
||||
<App>
|
||||
<Suspense fallback={<Loader/>}>
|
||||
<RouterProvider future={{v7_startTransition: true}} router={router}/>
|
||||
</Suspense>
|
||||
</App>
|
||||
</ConfigProvider>)
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,9 @@ const themeConfig = {
|
||||
'primary':'#7356f6',
|
||||
'primary-blue':'rgb(64, 150, 255)',
|
||||
'primary-bg': 'rgb(244, 247, 252)',
|
||||
'info':'rgba(238, 245, 255, 1)',
|
||||
'news-to-edit':'#ececec',
|
||||
|
||||
'active': '#FFE0E0',
|
||||
'primary-red':'#F5222D',
|
||||
'primary-red-70':'rgba(245,34,45,0.7)',
|
||||
|
Loading…
x
Reference in New Issue
Block a user