💄 新闻素材更新

This commit is contained in:
LittleBoy 2024-12-22 00:09:15 +08:00 committed by Coding
parent d0d84e61ed
commit bea93d9094
15 changed files with 521 additions and 170 deletions

View File

@ -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,6 +160,7 @@
max-height: calc(100vh - var(--app-header-header) - 200px);
overflow: auto;
}
.video-player {
.video-js {
@apply w-full h-full;
@ -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;
}
}
}

View File

@ -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;
}

View 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

View File

@ -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">

View 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>);
}

View File

@ -0,0 +1,3 @@
function useInfiniteScroller<T>(fetch: (page: number) => Promise<T[]>, deps: any[]) {
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

View 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,
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>
}}>
</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={() => {
handleFilter({tag_level_1_id: Number(it.value), tag_level_2_id: -1})
setSubOptions(it.children || [])
}}>{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 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'}`}
className={`filter-item border flex items-center px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded`}
key={it.value}
onClick={() => {
setState({tag_level_1_id: Number(it.value),tag_level_2_id:-1})
setSubOptions(it.children || [])
}}>{it.label}</div>)
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>)
)
}

View 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;
}
}
}

View File

@ -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 {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})
} else {
setData({
pagination: _data.pagination,
list: [...(data?.list || []), ..._data.list]
})
}
}
})
@ -44,45 +50,86 @@ 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 {
@ -90,48 +137,23 @@ export default function NewsIndex() {
}
}}/>}
</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>)
}

View File

@ -1,3 +0,0 @@
.newsList{
}

View File

@ -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>)
}

View File

@ -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)',