feat: update api
This commit is contained in:
parent
ca074f59b5
commit
f946e9d4f7
@ -24,6 +24,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"qs": "^6.12.1",
|
"qs": "^6.12.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
@ -18,7 +18,6 @@ body {
|
|||||||
min-width: 1000px;
|
min-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dashboard-layout {
|
.dashboard-layout {
|
||||||
background-color: var(--main-bg-color);
|
background-color: var(--main-bg-color);
|
||||||
}
|
}
|
||||||
@ -29,7 +28,7 @@ body {
|
|||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
padding: 0px 30px;
|
padding: 0px 20px;
|
||||||
&.active{
|
&.active{
|
||||||
@apply text-blue-500;
|
@apply text-blue-500;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,9 @@
|
|||||||
.image {
|
.image {
|
||||||
@apply border border-blue-200 p-2 flex-1 rounded focus:border-blue-200;
|
@apply border border-blue-200 p-2 flex-1 rounded focus:border-blue-200;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
|
&:hover{
|
||||||
|
@apply border-blue-500;
|
||||||
|
}
|
||||||
:global{
|
:global{
|
||||||
.ant-upload-wrapper{
|
.ant-upload-wrapper{
|
||||||
display: block;
|
display: block;
|
||||||
@ -58,7 +61,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
@apply border border-blue-200 overflow-hidden flex-1 rounded focus:border-blue-200;
|
@apply border border-blue-200 overflow-hidden flex-1 rounded focus:border-blue-200 transition;
|
||||||
|
&:hover{
|
||||||
|
@apply border-blue-500;
|
||||||
|
}
|
||||||
|
&:focus-within{
|
||||||
|
@apply border-blue-500 shadow-md;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
|
@ -23,6 +23,7 @@ export default function ArticleBlock({className, blocks, editable, onRemove, onA
|
|||||||
// 删除当前项
|
// 删除当前项
|
||||||
onChange?.(blocks.filter((_, idx) => index !== idx))
|
onChange?.(blocks.filter((_, idx) => index !== idx))
|
||||||
}
|
}
|
||||||
|
const firstTextBlockIndex = blocks.findIndex(it => it.type === 'text')
|
||||||
// 新增
|
// 新增
|
||||||
const handleAddBlock = (type: 'text' | 'image', insertIndex: number = -1) => {
|
const handleAddBlock = (type: 'text' | 'image', insertIndex: number = -1) => {
|
||||||
const newBlock: BlockContent = type === 'text' ? {type: 'text', content: ''} : {type: 'image', content: ''};
|
const newBlock: BlockContent = type === 'text' ? {type: 'text', content: ''} : {type: 'image', content: ''};
|
||||||
@ -44,16 +45,18 @@ export default function ArticleBlock({className, blocks, editable, onRemove, onA
|
|||||||
return <div className={styles.blockContainer}>
|
return <div className={styles.blockContainer}>
|
||||||
<div className={clsx(className || '', styles.block,' hover:bg-blue-10')}>
|
<div className={clsx(className || '', styles.block,' hover:bg-blue-10')}>
|
||||||
<div className={styles.blockBody}>
|
<div className={styles.blockBody}>
|
||||||
{blocks.map((it, idx) => (
|
{blocks.map((it, idx) => {
|
||||||
<div key={idx} className={clsx(styles.blockItem, 'flex')}>
|
const isFirstTextBlock = index == 0 && it.type ==='text' && firstTextBlockIndex == idx
|
||||||
|
return (<div key={idx}>
|
||||||
|
<div className={clsx(isFirstTextBlock?'':styles.blockItem, 'flex')}>
|
||||||
{
|
{
|
||||||
it.type === 'text'
|
it.type === 'text'
|
||||||
? <BlockText groupIndex={index} blockIndex={idx} onChange={(block) => handleBlockChange(idx, block)} data={it}
|
? <BlockText isFirstBlock={isFirstTextBlock} onChange={(block) => handleBlockChange(idx, block)} data={it}
|
||||||
editable={editable}/>
|
editable={editable}/>
|
||||||
: <BlockImage data={it} editable={editable}/>
|
: <BlockImage data={it} editable={editable}/>
|
||||||
}
|
}
|
||||||
{editable && <div className="create-container ml-2 flex flex-col justify-between">
|
{editable && <div className="create-container ml-2 flex flex-col justify-between">
|
||||||
<Popconfirm
|
{isFirstTextBlock?<span></span>:<Popconfirm
|
||||||
title="提示"
|
title="提示"
|
||||||
description={<div style={{minWidth: 150}}>
|
description={<div style={{minWidth: 150}}>
|
||||||
<span>请确认删除此{it.type === 'text' ? '文本' : '图片'}?</span>
|
<span>请确认删除此{it.type === 'text' ? '文本' : '图片'}?</span>
|
||||||
@ -67,7 +70,7 @@ export default function ArticleBlock({className, blocks, editable, onRemove, onA
|
|||||||
title={`删除此${it.type === 'text' ? '文本' : '图片'}`}>
|
title={`删除此${it.type === 'text' ? '文本' : '图片'}`}>
|
||||||
<IconDelete style={{fontSize: 18}}/>
|
<IconDelete style={{fontSize: 18}}/>
|
||||||
</span>
|
</span>
|
||||||
</Popconfirm>
|
</Popconfirm>}
|
||||||
<div>
|
<div>
|
||||||
<span onClick={() => handleAddBlock('text', idx + 1)}
|
<span onClick={() => handleAddBlock('text', idx + 1)}
|
||||||
className="article-action-icon" title="新增文本"><IconAddText
|
className="article-action-icon" title="新增文本"><IconAddText
|
||||||
@ -78,7 +81,10 @@ export default function ArticleBlock({className, blocks, editable, onRemove, onA
|
|||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
))}
|
{isFirstTextBlock && <div className={'text-red-500 text-right pr-6 mt-1 text-sm'}>该编辑框内容由数字人播报</div>}
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
)}
|
||||||
{editable && blocks.length == 0 &&
|
{editable && blocks.length == 0 &&
|
||||||
<div style={{minHeight: 80}} className="flex items-center justify-center">
|
<div style={{minHeight: 80}} className="flex items-center justify-center">
|
||||||
<div className="flex gap-5">
|
<div className="flex gap-5">
|
||||||
|
@ -2,7 +2,7 @@ import {Input, Modal} from "antd";
|
|||||||
import ArticleGroup from "@/components/article/group.tsx";
|
import ArticleGroup from "@/components/article/group.tsx";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {useSetState} from "ahooks";
|
import {useSetState} from "ahooks";
|
||||||
import {getArticleDetail} from "@/service/api/article.ts";
|
import {getById} from "@/service/api/article.ts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id?: number;
|
id?: number;
|
||||||
@ -32,7 +32,7 @@ export default function ArticleEditModal(props: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(props.id){
|
if(props.id){
|
||||||
if(props.id > 0){
|
if(props.id > 0){
|
||||||
getArticleDetail(props.id).then(res => {
|
getById(props.id).then(res => {
|
||||||
setGroups(res.content_group)
|
setGroups(res.content_group)
|
||||||
setTitle(res.title)
|
setTitle(res.title)
|
||||||
})
|
})
|
||||||
|
@ -27,7 +27,9 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
|
|||||||
return <div className={styles.group}>
|
return <div className={styles.group}>
|
||||||
{groups.map((g, index) => (
|
{groups.map((g, index) => (
|
||||||
<ArticleBlock
|
<ArticleBlock
|
||||||
editable={editable} key={index} blocks={g}
|
editable={editable}
|
||||||
|
key={index}
|
||||||
|
blocks={g}
|
||||||
onChange={(blocks) => {
|
onChange={(blocks) => {
|
||||||
groups[index] = blocks
|
groups[index] = blocks
|
||||||
onChange?.([...groups])
|
onChange?.([...groups])
|
||||||
@ -46,6 +48,6 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{groups.length == 0 && editable &&
|
{groups.length == 0 && editable &&
|
||||||
<ArticleBlock editable onChange={blocks => onChange?.([blocks])} blocks={[]}/>}
|
<ArticleBlock editable onChange={blocks => onChange?.([blocks])} index={0} blocks={[{type:'text',content:''}]}/>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -1,23 +1,71 @@
|
|||||||
import React, {useMemo, useRef, useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {Button, Input, Upload} from "antd";
|
import {Button, Input, Spin, Upload, UploadProps} from "antd";
|
||||||
import {TextAreaRef} from "antd/es/input/TextArea";
|
|
||||||
|
|
||||||
import styles from './article.module.scss'
|
import styles from './article.module.scss'
|
||||||
|
import {getOssPolicy} from "@/service/api/common.ts";
|
||||||
|
import {showToast} from "@/components/message.ts";
|
||||||
|
import {clsx} from "clsx";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
data: BlockContent;
|
data: BlockContent;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
groupIndex?: number;
|
|
||||||
blockIndex?: number;
|
|
||||||
onChange?: (data: BlockContent) => void;
|
onChange?: (data: BlockContent) => void;
|
||||||
|
isFirstBlock?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlockImage({data, editable}: Props) {
|
const MimeTypes = ['image/jpeg', 'image/png', 'image/jpg']
|
||||||
|
const Data: { uploadConfig?: TOSSPolicy } = {}
|
||||||
|
|
||||||
|
export function BlockImage({data, editable, onChange}: Props) {
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<number>(-1)
|
||||||
|
// oss上传文件所需的数据
|
||||||
|
const getUploadData: UploadProps['data'] = (file) => ({
|
||||||
|
key: file.url,
|
||||||
|
OSSAccessKeyId: Data.uploadConfig?.access_id,
|
||||||
|
policy: Data.uploadConfig?.policy,
|
||||||
|
Signature: Data.uploadConfig?.signature,
|
||||||
|
});
|
||||||
|
const beforeUpload = async (file: any) => {
|
||||||
|
try {
|
||||||
|
// 因为有超时问题,所以每次上传都重新获取参数
|
||||||
|
Data.uploadConfig = await getOssPolicy();
|
||||||
|
const suffix = file.name.slice(file.name.lastIndexOf('.'));
|
||||||
|
const filename = Date.now().toString(16) + suffix;
|
||||||
|
file.url = Data.uploadConfig.dir + filename;
|
||||||
|
} catch (e) {
|
||||||
|
// 设置错误状态
|
||||||
|
file.status = 'error'
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理图片上传后的状态
|
||||||
|
const onUploadChange = async (info) => {
|
||||||
|
if (info.fileList.length == 0) return;
|
||||||
|
const file = info.fileList[0];
|
||||||
|
console.log('onChange', file);
|
||||||
|
if (file.status == 'done') {
|
||||||
|
setLoading(-1)
|
||||||
|
onChange?.({type: 'image', content: Data.uploadConfig?.host + file.url})
|
||||||
|
} else if (file.status == 'error') {
|
||||||
|
setLoading(-1)
|
||||||
|
showToast('上传图片失败,请重试', 'warning')
|
||||||
|
} else if (file.status == 'uploading') {
|
||||||
|
setLoading(file.percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
return <div className={styles.image}>
|
return <div className={styles.image}>
|
||||||
{editable ? <div>
|
{editable ? <div>
|
||||||
<Upload accept="image/*">
|
<Spin spinning={loading >= 0} percent={loading == 0 ? 'auto' : loading}>
|
||||||
|
<Upload
|
||||||
|
multiple={false} maxCount={1} data={getUploadData}
|
||||||
|
showUploadList={false} accept={MimeTypes.join(',')}
|
||||||
|
action={() => (Data.uploadConfig!.host)}
|
||||||
|
beforeUpload={beforeUpload} onChange={onUploadChange}
|
||||||
|
>
|
||||||
<div className={styles.uploadImage}>
|
<div className={styles.uploadImage}>
|
||||||
{data.content ? <>
|
{data.content ? <>
|
||||||
<img src={data.content}/>
|
<img src={data.content}/>
|
||||||
@ -29,69 +77,22 @@ export function BlockImage({data, editable}: Props) {
|
|||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
</Spin>
|
||||||
</div> : <div className={styles.uploadImage}><img src={data.content}/></div>}
|
</div> : <div className={styles.uploadImage}><img src={data.content}/></div>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlockText({data, editable, onChange, groupIndex,blockIndex}: Props) {
|
export function BlockText({data, editable, onChange, isFirstBlock}: Props) {
|
||||||
const inputRef = useRef<TextAreaRef | null>(null);
|
return <div className='flex-1'>
|
||||||
// 内容分割
|
<div className={clsx(styles.text, isFirstBlock?'border-red-400 hover:border-red-500 focus-within:border-red-500':'')}>
|
||||||
const contentSentence = useMemo(() => {
|
|
||||||
const textContent = data.content
|
|
||||||
if (!/[.|。]/.test(textContent)) {
|
|
||||||
return [textContent];
|
|
||||||
}
|
|
||||||
const firstSentence = textContent.split(/[.|。]/)[0]!
|
|
||||||
// 获取第一个句子
|
|
||||||
return [textContent.substring(0, firstSentence.length + 1), textContent.substring(firstSentence.length + 1)]
|
|
||||||
}, [data.content])
|
|
||||||
|
|
||||||
const [editorMode, setEditMode] = useState({
|
|
||||||
preview: true
|
|
||||||
})
|
|
||||||
const handleTextBlur = () => {
|
|
||||||
setEditMode({preview: true})
|
|
||||||
}
|
|
||||||
return <div className={styles.text}>
|
|
||||||
{editable ? <div className="relative">
|
{editable ? <div className="relative">
|
||||||
{/*<textarea*/}
|
|
||||||
{/* className={"ant-input ant-input-borderless w-full min-h-[40px] max-h-[120px] overflow-auto p-2"}*/}
|
|
||||||
{/* value={data.content}*/}
|
|
||||||
{/* onChange={e=>onChange?.({type:'text',content:e.target.value})}*/}
|
|
||||||
{/*></textarea>*/}
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
ref={inputRef}
|
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
onChange?.({type: 'text', content: e.target.value})
|
onChange?.({type: 'text', content: e.target.value})
|
||||||
}}
|
}}
|
||||||
placeholder={'请输入文本'} onBlur={handleTextBlur} value={data.content} autoSize={{minRows: 3}}
|
placeholder={'请输入文本'} value={data.content} autoSize={{minRows: 3, maxRows: 8}}
|
||||||
variant={"borderless"}/>
|
variant={"borderless"}/>
|
||||||
{groupIndex == 0 && blockIndex == 0 &&
|
|
||||||
<div className="ant-input ant-input-borderless absolute bg-white inset-0 cursor-text overflow-auto"
|
|
||||||
onClick={() => {
|
|
||||||
inputRef.current!.focus({cursor: 'end'});
|
|
||||||
setEditMode({preview: false})
|
|
||||||
}} style={editorMode.preview && data.content?.length > 0 ? {
|
|
||||||
padding: '4px 11px'
|
|
||||||
} : {
|
|
||||||
opacity: 0,
|
|
||||||
pointerEvents: 'none'
|
|
||||||
}}>
|
|
||||||
{contentSentence.map((sentence, index) => {
|
|
||||||
return <span key={index}
|
|
||||||
className={`${index == 0 ? 'text-red-500' : ''}`}>{sentence}{index == 0 ? '(本句由数字人播报)' : ''}</span>
|
|
||||||
})}
|
|
||||||
</div>}
|
|
||||||
{/*<span style={{*/}
|
|
||||||
{/* top:4,*/}
|
|
||||||
{/* left:11,*/}
|
|
||||||
{/* zIndex:1*/}
|
|
||||||
{/*}} className="pointer-events-none select-none absolute text-red-500 ant-input">{firstSentence}</span>*/}
|
|
||||||
{/*<textarea className="ant-input ant-input-borderless min-h-[40px] max-h-[120px] overflow-auto p-2"*/}
|
|
||||||
{/* onKeyUp={e=>{*/}
|
|
||||||
{/* const text = (e.target as HTMLDivElement).textContent;*/}
|
|
||||||
{/* onChange?.({type:'text',content:text})*/}
|
|
||||||
{/*}}>{data.content}</textarea>*/}
|
|
||||||
</div> : <p className="p-2">{data.content}</p>}
|
</div> : <p className="p-2">{data.content}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
31
src/components/message.ts
Normal file
31
src/components/message.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {message} from "antd";
|
||||||
|
|
||||||
|
export function showToast(content: string, type?: 'success' | 'info' | 'warning' | 'error') {
|
||||||
|
|
||||||
|
message.open({
|
||||||
|
type,
|
||||||
|
content,
|
||||||
|
className: 'aui-toast'
|
||||||
|
}).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showLoading(content = 'Loading...') {
|
||||||
|
const key = 'globalLoading_' + (new Date().getTime());
|
||||||
|
message.open({
|
||||||
|
key,
|
||||||
|
type: 'loading',
|
||||||
|
content,
|
||||||
|
}).then();
|
||||||
|
return {
|
||||||
|
update(content: string,type?: 'success' | 'info' | 'warning' | 'error'){
|
||||||
|
message.open({
|
||||||
|
key,
|
||||||
|
content,
|
||||||
|
type
|
||||||
|
}).then();
|
||||||
|
},
|
||||||
|
close(){
|
||||||
|
message.destroy(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,7 @@ export const VideoListItem = (
|
|||||||
{
|
{
|
||||||
index, id, video, onPlay, onRemove, checked,
|
index, id, video, onPlay, onRemove, checked,
|
||||||
onCheckedChange, onEdit, active, editable,
|
onCheckedChange, onEdit, active, editable,
|
||||||
sortable
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
attributes, listeners,
|
attributes, listeners,
|
||||||
@ -47,9 +47,9 @@ export const VideoListItem = (
|
|||||||
</div>}
|
</div>}
|
||||||
<div
|
<div
|
||||||
className={`video-item-info flex gap-2 flex-1 bg-gray-100 min-h-[40px] rounded-lg p-3 shadow-blue-500 ${active ? 'video-item-shadow' : ''}`}>
|
className={`video-item-info flex gap-2 flex-1 bg-gray-100 min-h-[40px] rounded-lg p-3 shadow-blue-500 ${active ? 'video-item-shadow' : ''}`}>
|
||||||
<div className={'video-title leading-7 flex-1'}>{video.id} - {video.title}</div>
|
<div className={'video-title leading-7 flex-1'}>{video.title}</div>
|
||||||
<div className={'video-item-cover'}>
|
<div className={'video-item-cover'}>
|
||||||
<img className="w-[100px] rounded-md" src={video.cover} alt={video.title}/>
|
<img className="w-[100px] rounded-md" src={video.cover || ''} alt={video.title}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{editable &&
|
{editable &&
|
||||||
|
@ -4,16 +4,14 @@ import {getAllCategory} from "@/service/api/article.ts";
|
|||||||
|
|
||||||
const ArticleTags: OptionItem[] = [];
|
const ArticleTags: OptionItem[] = [];
|
||||||
export default function useArticleTags() {
|
export default function useArticleTags() {
|
||||||
const [tags, _setTags] = useState([]);
|
const [tags, _setTags] = useState<OptionItem[]>([]);
|
||||||
const setTags = useCallback(() => {
|
const setTags = useCallback(() => {
|
||||||
_setTags([
|
_setTags([
|
||||||
{
|
|
||||||
label: '全部',
|
|
||||||
value: -1,
|
|
||||||
},
|
|
||||||
...ArticleTags
|
...ArticleTags
|
||||||
])
|
])
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ArticleTags.length === 0) {
|
if (ArticleTags.length === 0) {
|
||||||
getAllCategory().then(res => {
|
getAllCategory().then(res => {
|
||||||
@ -31,11 +29,8 @@ export default function useArticleTags() {
|
|||||||
})
|
})
|
||||||
setTags()
|
setTags()
|
||||||
})
|
})
|
||||||
}
|
}else{
|
||||||
return () => {
|
setTags()
|
||||||
// 清除
|
|
||||||
setTags([])
|
|
||||||
ArticleTags.length = 0;
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
return tags
|
return tags
|
||||||
|
@ -5,7 +5,7 @@ import App from './App.tsx'
|
|||||||
import '@/assets/index.scss'
|
import '@/assets/index.scss'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
// <React.StrictMode>
|
||||||
<App/>
|
<App/>
|
||||||
</React.StrictMode>,
|
// </React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Button, message, Modal} from "antd";
|
import {Button, message, Modal} from "antd";
|
||||||
import React, {useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
|
|
||||||
import {ArticleGroupList, MockVideoDataList} from "@/_local/mock-data";
|
import {ArticleGroupList, MockVideoDataList} from "@/_local/mock-data";
|
||||||
import {DndContext} from "@dnd-kit/core";
|
import {DndContext} from "@dnd-kit/core";
|
||||||
@ -9,6 +9,7 @@ import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
|||||||
import {useSetState} from "ahooks";
|
import {useSetState} from "ahooks";
|
||||||
import {CheckCircleFilled} from "@ant-design/icons";
|
import {CheckCircleFilled} from "@ant-design/icons";
|
||||||
import {clsx} from "clsx";
|
import {clsx} from "clsx";
|
||||||
|
import {getList} from "@/service/api/video.ts";
|
||||||
|
|
||||||
|
|
||||||
export default function CreateIndex() {
|
export default function CreateIndex() {
|
||||||
@ -17,10 +18,17 @@ export default function CreateIndex() {
|
|||||||
groups?: ArticleContentGroup[];
|
groups?: ArticleContentGroup[];
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const [videoData, setVideoData] = useState<VideoInfo[]>(MockVideoDataList)
|
const [videoData, setVideoData] = useState<VideoInfo[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getList({}).then((ret) => {
|
||||||
|
setVideoData(ret.list)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [modal, contextHolder] = Modal.useModal()
|
const [modal, contextHolder] = Modal.useModal()
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||||
const [state,setState] = useSetState({
|
const [state, setState] = useSetState({
|
||||||
checkedAll: false
|
checkedAll: false
|
||||||
})
|
})
|
||||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||||
@ -42,14 +50,14 @@ export default function CreateIndex() {
|
|||||||
videoRef.current!.src = video.play_url
|
videoRef.current!.src = video.play_url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleAllCheckedChange = ()=>{
|
const handleAllCheckedChange = () => {
|
||||||
// setVideoData(list=>{
|
// setVideoData(list=>{
|
||||||
// list.map(s=>{
|
// list.map(s=>{
|
||||||
// s.checked = !state.checkedAll
|
// s.checked = !state.checkedAll
|
||||||
// })
|
// })
|
||||||
// return list
|
// return list
|
||||||
// })
|
// })
|
||||||
setCheckedIdArray(state.checkedAll?[]:videoData.map(v=>v.id))
|
setCheckedIdArray(state.checkedAll ? [] : videoData.map(v => v.id))
|
||||||
setState({
|
setState({
|
||||||
checkedAll: !state.checkedAll
|
checkedAll: !state.checkedAll
|
||||||
})
|
})
|
||||||
@ -65,7 +73,8 @@ export default function CreateIndex() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
||||||
<button className="ml-5 hover:text-blue-300 text-gray-400 text-lg" onClick={handleAllCheckedChange}>
|
<button className="ml-5 hover:text-blue-300 text-gray-400 text-lg"
|
||||||
|
onClick={handleAllCheckedChange}>
|
||||||
<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>
|
<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -106,7 +115,7 @@ export default function CreateIndex() {
|
|||||||
}}
|
}}
|
||||||
onPlay={() => playVideo(v)}
|
onPlay={() => playVideo(v)}
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
setEditNews({title:v.title, groups: [...ArticleGroupList]})
|
setEditNews({title: v.title, groups: [...ArticleGroupList]})
|
||||||
}}
|
}}
|
||||||
editable
|
editable
|
||||||
/>))}
|
/>))}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {Button, Form, Input, Select, Space} from "antd";
|
import {Button, Form, Input, Select, Space} from "antd";
|
||||||
import {useSetState} from "ahooks";
|
import {useSetState} from "ahooks";
|
||||||
import {PlayCircleOutlined} from "@ant-design/icons";
|
import {PlayCircleOutlined} from "@ant-design/icons";
|
||||||
import {ListTimes} from "@/pages/news/components/news-source.ts";
|
import {SearchListTimes} from "@/pages/news/components/news-source.ts";
|
||||||
|
|
||||||
type SearchParams = {
|
type SearchParams = {
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
@ -40,7 +40,7 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={'更新时间'} name="date" className="w-[250px]">
|
<Form.Item label={'更新时间'} name="date" className="w-[250px]">
|
||||||
<Select
|
<Select
|
||||||
defaultValue={state.time} options={ListTimes}
|
defaultValue={state.time} options={SearchListTimes}
|
||||||
optionRender={(option) => (
|
optionRender={(option) => (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span role="icon" className={`radio-icon`}></span>
|
<span role="icon" className={`radio-icon`}></span>
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Modal, Pagination} from "antd";
|
import {Modal, Pagination} from "antd";
|
||||||
|
import {useRequest} from "ahooks";
|
||||||
|
|
||||||
import {getEmptyPageData} from "@/hooks/usePagination.ts"
|
|
||||||
import VideoItem from "@/pages/library/components/video-item.tsx";
|
import VideoItem from "@/pages/library/components/video-item.tsx";
|
||||||
import SearchForm from "@/pages/library/components/search-form.tsx";
|
import SearchForm from "@/pages/library/components/search-form.tsx";
|
||||||
import VideoDetail from "@/pages/library/components/video-detail.tsx";
|
import VideoDetail from "@/pages/library/components/video-detail.tsx";
|
||||||
|
import {getList} from "@/service/api/video.ts";
|
||||||
|
|
||||||
export default function LibraryIndex() {
|
export default function LibraryIndex() {
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const [videoData,] = useState(getEmptyPageData<VideoInfo>())
|
|
||||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||||
|
const {data} = useRequest(()=>getList({}),{
|
||||||
|
|
||||||
|
})
|
||||||
const handleRemove = (video: VideoInfo) => {
|
const handleRemove = (video: VideoInfo) => {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: '删除提示',
|
title: '删除提示',
|
||||||
@ -43,7 +46,7 @@ export default function LibraryIndex() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded p-5">
|
<div className="bg-white rounded p-5">
|
||||||
<div className={'video-list-container grid gap-5 grid-cols-4'}>
|
<div className={'video-list-container grid gap-5 grid-cols-4'}>
|
||||||
{videoData.list.map((it, idx) => (
|
{data?.list?.map((it, idx) => (
|
||||||
<VideoItem
|
<VideoItem
|
||||||
onLive={idx == 2} key={it.id}
|
onLive={idx == 2} key={it.id}
|
||||||
videoInfo={it}
|
videoInfo={it}
|
||||||
|
@ -4,10 +4,9 @@ import {SortableContext, arrayMove} from '@dnd-kit/sortable';
|
|||||||
import {DndContext} from "@dnd-kit/core";
|
import {DndContext} from "@dnd-kit/core";
|
||||||
|
|
||||||
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
import {VideoListItem} from "@/components/video/video-list-item.tsx";
|
||||||
import {MockVideoDataList} from "@/_local/mock-data.ts";
|
|
||||||
|
|
||||||
export default function LiveIndex() {
|
export default function LiveIndex() {
|
||||||
const [videoData, setVideoData] = useState<VideoInfo[]>(MockVideoDataList)
|
const [videoData, setVideoData] = useState<VideoInfo[]>()
|
||||||
const [modal, contextHolder] = Modal.useModal()
|
const [modal, contextHolder] = Modal.useModal()
|
||||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||||
const [editable,setEditable] = useState<boolean>(false)
|
const [editable,setEditable] = useState<boolean>(false)
|
||||||
|
126
src/pages/news/components/article-cascader.tsx
Normal file
126
src/pages/news/components/article-cascader.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import {Cascader} from "antd";
|
||||||
|
import React, {useEffect, useMemo} from "react";
|
||||||
|
|
||||||
|
|
||||||
|
const prevSelectValues: Id[][] = [];
|
||||||
|
|
||||||
|
function buildValues(options: OptionItem[], selectedValues: Id[][], allValue = -1) {
|
||||||
|
const values: Id[][] = []
|
||||||
|
selectedValues.forEach(item => {
|
||||||
|
if (item.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.length == 1) {
|
||||||
|
if (item[0] == allValue) {
|
||||||
|
values.push([allValue]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 只有1个值 表示选择了一级分类下的所有二级分类
|
||||||
|
const op = options.find(option => option.value === item[0]);
|
||||||
|
if (!op || !op.children || op.children.length === 0) {
|
||||||
|
// 没有找到或者没有二级分类
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 只有一级分类
|
||||||
|
op.children.forEach(child => {
|
||||||
|
values.push([item[0], child.value]);
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
values.push(item)
|
||||||
|
})
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取两个数组的差集
|
||||||
|
function getValuesDiff(values: Id[][], prevValues: Id[][]) {
|
||||||
|
if (values.length != prevValues.length) {
|
||||||
|
const moreItems = values.length > prevValues.length ? values : prevValues;
|
||||||
|
const lessItems = values.length > prevValues.length ? prevValues : values;
|
||||||
|
const lessItemsKeys = lessItems.map(s => s.join('-'));
|
||||||
|
for (let i = 0; i < moreItems.length; i++) {
|
||||||
|
const item = moreItems[i], index = lessItemsKeys.indexOf(item.join('-'));
|
||||||
|
if (index === -1) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllValue(options: OptionItem[]) {
|
||||||
|
const values: Id[][] = []
|
||||||
|
options.forEach(option => {
|
||||||
|
if (option.children && option.children.length > 0) {
|
||||||
|
option.children.forEach(child => {
|
||||||
|
values.push([option.value, child.value]);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
values.push([option.value]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArticleCascader(props: {
|
||||||
|
options: OptionItem[];
|
||||||
|
onChange: (values: Id[][]) => void;
|
||||||
|
}) {
|
||||||
|
const [selectValues, _setSelectValues] = React.useState<Id[][]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
// 清除上一次的选中值
|
||||||
|
prevSelectValues.length = 0;
|
||||||
|
}, [])
|
||||||
|
const allOptionValue = useMemo(() => {
|
||||||
|
return getAllValue(props.options)
|
||||||
|
}, [props.options])
|
||||||
|
|
||||||
|
const setSelectValues = (value: Id[][]) => {
|
||||||
|
_setSelectValues(value)
|
||||||
|
//console.log(value,value.some(s=>s[0] == -1))
|
||||||
|
props.onChange?.(value)
|
||||||
|
}
|
||||||
|
const handleChange = (values: Id[][]) => {
|
||||||
|
// const fullValues = buildValues(props.options, values)
|
||||||
|
// const diffValue = getValuesDiff(fullValues, prevSelectValues);
|
||||||
|
// const isIncrease = fullValues.length > prevSelectValues.length;
|
||||||
|
// prevSelectValues.length = 0;
|
||||||
|
//
|
||||||
|
// if(values.length == 0){
|
||||||
|
// setSelectValues([])
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// // 判断操作的是否是全部
|
||||||
|
// if(diffValue?.length == 1 && diffValue[0] == -1){
|
||||||
|
// if(isIncrease) prevSelectValues.push(...allOptionValue);
|
||||||
|
// setSelectValues(isIncrease ? [...allOptionValue] : [])
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// // if(fullValues.length != allOptionValue.length){
|
||||||
|
// // setSelectValues(fullValues.filter(s=>s.length == 1 && s[0] != -1))
|
||||||
|
// // }else{
|
||||||
|
// //
|
||||||
|
// // }
|
||||||
|
//
|
||||||
|
// if(fullValues.filter(s=>s.length > 1 || s[0] != -1).length == allOptionValue.length - 1){
|
||||||
|
// prevSelectValues.push(...allOptionValue);
|
||||||
|
// setSelectValues( [...allOptionValue])
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// prevSelectValues.push(...fullValues);
|
||||||
|
setSelectValues(values.filter(s=>s.length > 1 || s[0] != -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<Cascader
|
||||||
|
options={props.options}
|
||||||
|
className="article-cascader min-w-[230px]"
|
||||||
|
placeholder="请选择你要筛选的新闻来源"
|
||||||
|
value={selectValues}
|
||||||
|
onChange={handleChange}
|
||||||
|
displayRender={label => label.join('-')}
|
||||||
|
expandTrigger="click"
|
||||||
|
multiple
|
||||||
|
maxTagCount="responsive"
|
||||||
|
|
||||||
|
/>)
|
||||||
|
}
|
39
src/pages/news/components/button-news-download.tsx
Normal file
39
src/pages/news/components/button-news-download.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {Button} from "antd";
|
||||||
|
import JSZip from "jszip"
|
||||||
|
import {saveAs} from "file-saver";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {showToast} from "@/components/message.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export default function ButtonNewsDownload(props: { ids: Id[] }) {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const onDownloadClick = (ids: Id[]) => {
|
||||||
|
if (props.ids.length === 0) {
|
||||||
|
showToast('请选择要推送的新闻', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(true)
|
||||||
|
const zip = new JSZip();
|
||||||
|
ids.forEach(id => {
|
||||||
|
zip.file(`${id}.html`, `<html>
|
||||||
|
<head>
|
||||||
|
<title>${id}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="max-width: 90%;width:1000px;margin:30px auto;">
|
||||||
|
<h1>title ${id}</h1>
|
||||||
|
<p>content ${id}</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`)
|
||||||
|
})
|
||||||
|
zip.generateAsync({type: "blob"}).then(function (content) {
|
||||||
|
saveAs(content, "news.zip");
|
||||||
|
}).finally(() => {
|
||||||
|
setLoading(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button loading={loading} onClick={() => onDownloadClick(props.ids)}>下载</Button>
|
||||||
|
)
|
||||||
|
}
|
36
src/pages/news/components/button-push-news2article.tsx
Normal file
36
src/pages/news/components/button-push-news2article.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {Button, Modal} from "antd";
|
||||||
|
import {showToast} from "@/components/message.ts";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {push2article} from "@/service/api/news.ts";
|
||||||
|
|
||||||
|
export default function ButtonPushNews2Article(props: { ids: Id[] }) {
|
||||||
|
const [loading,setLoading] = useState(false)
|
||||||
|
const handlePush = () => {
|
||||||
|
setLoading(true)
|
||||||
|
push2article(props.ids).then(() => {
|
||||||
|
showToast('推送成功', 'success')
|
||||||
|
}).catch(() => {
|
||||||
|
showToast('推送失败', 'error')
|
||||||
|
}).finally(() => {
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onPushClick = () => {
|
||||||
|
if (props.ids.length === 0) {
|
||||||
|
showToast('请选择要推送的新闻', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title: '操作提示',
|
||||||
|
content: '是否确定推入素材编辑界面?',
|
||||||
|
onOk: handlePush
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={'primary'}
|
||||||
|
loading={loading}
|
||||||
|
onClick={onPushClick}
|
||||||
|
>推入素材编辑界面</Button>
|
||||||
|
)
|
||||||
|
}
|
31
src/pages/news/components/button-push2video.tsx
Normal file
31
src/pages/news/components/button-push2video.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {Button, Modal} from "antd";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {showToast} from "@/components/message.ts";
|
||||||
|
import {push2article} from "@/service/api/news.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export default function ButtonPush2Video(props: { ids: Id[]}){
|
||||||
|
const [loading,setLoading] = useState(false)
|
||||||
|
const handlePush = ()=>{
|
||||||
|
setLoading(true)
|
||||||
|
push2article(props.ids).then(()=>{
|
||||||
|
showToast('一键推流成功,已成功推入数字人视频生成,请前往数字人视频生成页面查看!', 'success')
|
||||||
|
}).finally(()=>{
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onPushClick = ()=>{
|
||||||
|
if (props.ids.length === 0) {
|
||||||
|
showToast('请选择要开播的新闻', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title:'操作提示',
|
||||||
|
content: '是否确定一键开播选中新闻?',
|
||||||
|
onOk: handlePush
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button type="primary" loading={loading} onClick={onPushClick}>一键开播</Button>
|
||||||
|
)
|
||||||
|
}
|
57
src/pages/news/components/edit-search-form.tsx
Normal file
57
src/pages/news/components/edit-search-form.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import {Button, Input} from "antd";
|
||||||
|
import {SearchOutlined} from "@ant-design/icons";
|
||||||
|
import ArticleCascader from "@/pages/news/components/article-cascader.tsx";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {useSetState} from "ahooks";
|
||||||
|
import useArticleTags from "@/hooks/useArticleTags.ts";
|
||||||
|
|
||||||
|
export default function EditSearchForm(props: {
|
||||||
|
onSubmit: (values: ApiArticleSearchParams) => void;
|
||||||
|
}) {
|
||||||
|
const articleTags = useArticleTags()
|
||||||
|
const [tags, setTags] = useState<Id[][]>([]);
|
||||||
|
const [params, setParams] = useSetState<ApiArticleSearchParams>({
|
||||||
|
pagination: {limit: 10, page: 1},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
params.tags = tags.length == 0 ? undefined : tags.map(it => {
|
||||||
|
if (Array.isArray(it)) {
|
||||||
|
return {
|
||||||
|
level1: it[0],
|
||||||
|
level2: it.length == 2 ? it[1] : 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
level1: it, level2: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
props.onSubmit({
|
||||||
|
...params,
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
limit: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="search-form-input flex gap-2 items-center">
|
||||||
|
<Input
|
||||||
|
onChange={(e) => {
|
||||||
|
setParams({title: e.target.value})
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
type="text" className="rounded px-3 w-[250px]"
|
||||||
|
suffix={<SearchOutlined/>}
|
||||||
|
placeholder="请输入你先搜索的关键词"
|
||||||
|
/>
|
||||||
|
<span className="ml-5 text-sm">来源</span>
|
||||||
|
<ArticleCascader
|
||||||
|
options={articleTags}
|
||||||
|
onChange={setTags}
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={handleSubmit}>搜索</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
人民日报客户端
|
人民日报客户端
|
||||||
环球时报客户端
|
环球时报客户端
|
||||||
@ -31,150 +30,80 @@
|
|||||||
中国商务部
|
中国商务部
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export const NewsSources:OptionItem[] = [
|
export const NewsSources: OptionItem[] = [
|
||||||
{
|
{
|
||||||
label: '全部',
|
label: '全部',
|
||||||
value: 'all',
|
value: -1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '人民日报',
|
label: '人民日报',
|
||||||
value: 'people',
|
value: 1,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
label: '要闻',
|
label: '要闻',
|
||||||
value: 'important'
|
value: 101
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '国际',
|
label: '国际',
|
||||||
value: 'international'
|
value: 102
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '国内',
|
label: '国内',
|
||||||
value: 'domestic'
|
value: 103
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '社会',
|
label: '社会',
|
||||||
value: 'society'
|
value: 104
|
||||||
},
|
}
|
||||||
{
|
|
||||||
label: '娱乐',
|
|
||||||
value: 'entertainment'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '军事',
|
|
||||||
value: 'military'
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '环球时报',
|
label: '环球时报',
|
||||||
value: 'global'
|
value: 2,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '要闻',
|
||||||
|
value: 201
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '新华社',
|
label: '国际',
|
||||||
value: 'xh-net'
|
value: 202
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '央视新闻',
|
label: '国内',
|
||||||
value: 'cctv'
|
value: 203
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '解放军报',
|
label: '社会',
|
||||||
value: '81'
|
value: 204
|
||||||
},
|
}
|
||||||
{
|
]
|
||||||
label: '澎湃新闻',
|
|
||||||
value: 'the-paper'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '海客新闻',
|
|
||||||
value: 'haike'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '中新经纬',
|
|
||||||
value: 'zxjw'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '央视体育',
|
|
||||||
value: 'cctv-sports'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '参考消息',
|
|
||||||
value: 'can-kao'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '百姓关注',
|
|
||||||
value: 'baixin'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '大象新闻',
|
|
||||||
value: 'dx-news'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '四川观察',
|
|
||||||
value: 'sc-news'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '新京报',
|
|
||||||
value: 'xjb'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '北京日报',
|
|
||||||
value: 'bjrb'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '中国纪检监察报',
|
|
||||||
value: 'jx-news'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '腾讯网',
|
|
||||||
value: 'qq'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '红网',
|
|
||||||
value: 'hong-news'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '新湖南客户端',
|
|
||||||
value: 'xhn'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '晨视频客户端',
|
|
||||||
value: 'chen-video'
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const ListTimes = [
|
export const SearchListTimes = [
|
||||||
{
|
{
|
||||||
label: '半小时',
|
label: '半小时内',
|
||||||
value: '30'
|
value: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '一小时',
|
label: '一小时内',
|
||||||
value: '60'
|
value: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '两小时',
|
label: '四小时内',
|
||||||
value: '120'
|
value: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '四小时',
|
label: '一天内',
|
||||||
value: '240'
|
value: 4
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '近一天',
|
|
||||||
value: '1440'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '近一周',
|
label: '近一周',
|
||||||
value: '10080'
|
value: 5
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '近一月',
|
|
||||||
value: '43800'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '全部',
|
label: '全部',
|
||||||
value: '-1'
|
value: 0
|
||||||
}
|
}
|
||||||
]
|
]
|
@ -1,52 +1,73 @@
|
|||||||
import {Button, Form, Input, Select, Space} from "antd";
|
import {Button, Input, Select} from "antd";
|
||||||
import {useSetState} from "ahooks";
|
import {useSetState} from "ahooks";
|
||||||
import {ListTimes, NewsSources} from "@/pages/news/components/news-source.ts";
|
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
|
import useArticleTags from "@/hooks/useArticleTags.ts";
|
||||||
|
|
||||||
type SearchParams = {
|
import {SearchListTimes} from "@/pages/news/components/news-source.ts";
|
||||||
search: string;
|
|
||||||
date: string;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchPanelProps = {
|
type SearchPanelProps = {
|
||||||
onSearch?: (params: SearchParams) => Promise<void>;
|
onSearch?: (params: ApiArticleSearchParams) => void;
|
||||||
|
}
|
||||||
|
const pagination = {
|
||||||
|
limit: 10, page: 1
|
||||||
}
|
}
|
||||||
export default function SearchPanel({onSearch}: SearchPanelProps) {
|
export default function SearchPanel({onSearch}: SearchPanelProps) {
|
||||||
|
const tags = useArticleTags();
|
||||||
|
const [params, setParams] = useSetState<ApiArticleSearchParams>({
|
||||||
|
pagination
|
||||||
|
});
|
||||||
|
|
||||||
const [state, setState] = useSetState<{
|
const [state, setState] = useSetState<{
|
||||||
time: string;
|
source: string | number;
|
||||||
source: string;
|
subOptions: (string | number)[]
|
||||||
searching: boolean;
|
|
||||||
subOptions: string[]
|
|
||||||
}>({
|
}>({
|
||||||
time: '-1',
|
source: -1,
|
||||||
source: 'all',
|
|
||||||
searching: false,
|
|
||||||
subOptions: []
|
subOptions: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 二级分类
|
// 二级分类
|
||||||
const [subOptions, setSubOptions] = useState<OptionItem[]>([])
|
const [subOptions, setSubOptions] = useState<OptionItem[]>([])
|
||||||
const onFinish = (values: any) => {
|
const onFinish = () => {
|
||||||
setState({searching: true})
|
if(state.source != -1){
|
||||||
onSearch?.({
|
params.tags = [];
|
||||||
search: values.search,
|
state.subOptions.forEach(level2 => {
|
||||||
date: values.date.join('-'),
|
params.tags!.push({
|
||||||
source: state.source
|
level1: state.source,
|
||||||
}).finally(() => {
|
level2
|
||||||
setState({searching: false})
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
params.tags = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch?.({
|
||||||
|
...params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 重置
|
||||||
|
const onReset = () => {
|
||||||
|
setParams({pagination, title: ''})
|
||||||
|
setState({source: -1,subOptions: []})
|
||||||
|
setSubOptions([])
|
||||||
|
onSearch?.({pagination})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div className={'search-panel'}>
|
return (<div className={'search-panel'}>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="search-form">
|
<div className="search-form flex items-center gap-4">
|
||||||
<Form className={""} layout="inline" onFinish={onFinish}>
|
<Input
|
||||||
<Form.Item name="search" className="w-[200px]">
|
value={params.title}
|
||||||
<Input placeholder={'请输入搜索信息'}/>
|
onChange={e => setParams({title: e.target.value})}
|
||||||
</Form.Item>
|
className="w-[240px]"
|
||||||
<Form.Item label={'更新时间'} name="date" className="w-[250px]">
|
placeholder={'请输入新闻标题开始查找新闻'}
|
||||||
|
/>
|
||||||
|
<div className={'flex items-center ml-2'}>
|
||||||
|
<span className="text-sm whitespace-nowrap mr-1">更新时间</span>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={state.time} options={ListTimes}
|
className="w-[150px]"
|
||||||
|
value={params.time_flag || 0}
|
||||||
|
onChange={value => setParams({time_flag: value})}
|
||||||
|
options={SearchListTimes}
|
||||||
optionRender={(option) => (
|
optionRender={(option) => (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span role="icon" className={`radio-icon`}></span>
|
<span role="icon" className={`radio-icon`}></span>
|
||||||
@ -54,37 +75,37 @@ export default function SearchPanel({onSearch}: SearchPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</div>
|
||||||
<Form.Item>
|
<Button type={'primary'} onClick={onFinish}>搜索</Button>
|
||||||
<Space size={20}>
|
<Button onClick={onReset}>重置</Button>
|
||||||
<Button type={'primary'} htmlType={'submit'}>搜索</Button>
|
|
||||||
<Button htmlType={'reset'}>重置</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="filter-container flex items-start mt-5">
|
<div className="filter-container flex items-start mt-5">
|
||||||
<div className={'mt-2.5'}>新闻来源:</div>
|
|
||||||
<div className="list-container flex-1">
|
<div className="list-container flex-1">
|
||||||
<div className="news-source-lv-1 flex flex-wrap">
|
<div className="news-source-lv-1 flex flex-wrap">
|
||||||
|
<div
|
||||||
|
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.source == -1 ? 'bg-blue-500 text-white' : 'hover:bg-gray-100'}`}
|
||||||
|
onClick={() => {
|
||||||
|
setState({source: -1, subOptions: []})
|
||||||
|
setSubOptions([])
|
||||||
|
}}>全部</div>
|
||||||
{
|
{
|
||||||
NewsSources.map(it => (
|
tags.filter(s=>s.value !== 999999).map(it => (
|
||||||
<div
|
<div
|
||||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.source == 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 ${state.source == it.value ? 'bg-blue-500 text-white' : 'hover:bg-gray-100'}`}
|
||||||
key={it.value}
|
key={it.value}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setState({source: it.value,subOptions:[]})
|
setState({source: it.value, subOptions: []})
|
||||||
setSubOptions(it.children||[])
|
setSubOptions(it.children || [])
|
||||||
}}>{it.label}</div>)
|
}}>{it.label}</div>)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{subOptions.length > 0 && <div className="news-source-lv-2 bg-gray-100 p-2 rounded mt-2 flex flex-wrap">
|
{state.source != -1 && subOptions.length > 0 && <div className="news-source-lv-2 bg-gray-100 p-2 rounded mt-2 flex flex-wrap">
|
||||||
{
|
{
|
||||||
subOptions.map(it => (
|
subOptions.map(it => (
|
||||||
<div
|
<div
|
||||||
className={`filter-item whitespace-nowrap px-2 py-1 mt-1 text-sm mr-1 cursor-pointer rounded ${state.subOptions.includes(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 ${state.subOptions.includes(it.value) ? 'bg-blue-500 text-white' : 'hover:bg-gray-100'}`}
|
||||||
key={it.value}
|
key={it.value}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const options = [...state.subOptions]
|
const options = [...state.subOptions]
|
||||||
@ -93,7 +114,7 @@ export default function SearchPanel({onSearch}: SearchPanelProps) {
|
|||||||
} else {
|
} else {
|
||||||
options.push(it.value)
|
options.push(it.value)
|
||||||
}
|
}
|
||||||
setState({subOptions:options})
|
setState({subOptions: options})
|
||||||
}}>{it.label}</div>)
|
}}>{it.label}</div>)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,107 +1,47 @@
|
|||||||
import {Button, Cascader, Input, Select, Table, TableColumnsType, TableProps, Typography} from "antd";
|
import {Button, Pagination, Table, TableColumnsType, TableProps, Typography} from "antd";
|
||||||
import {SearchOutlined} from "@ant-design/icons";
|
|
||||||
import {Card} from "@/components/card";
|
import {Card} from "@/components/card";
|
||||||
import React, {useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {NewsSources} from "@/pages/news/components/news-source.ts";
|
import {useRequest} from "ahooks";
|
||||||
import {useRequest, useSetState} from "ahooks";
|
|
||||||
import {formatTime} from "@/util/strings.ts";
|
import {formatTime} from "@/util/strings.ts";
|
||||||
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
||||||
import {ArticleGroupList} from "@/_local/mock-data.ts";
|
import {getList} from "@/service/api/article.ts";
|
||||||
import useArticleTags from "@/hooks/useArticleTags.ts";
|
import EditSearchForm from "@/pages/news/components/edit-search-form.tsx";
|
||||||
import {getArticleList} from "@/service/api/article.ts";
|
import ButtonPush2Video from "@/pages/news/components/button-push2video.tsx";
|
||||||
|
|
||||||
|
|
||||||
const dataList: NewsInfo[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: '习近平抵达巴西利亚开始对巴西进行国事访问',
|
|
||||||
content: '当地时间11月19日下午,国家主席习近平乘专机抵达巴西利亚,开始对巴西进行国事访问。',
|
|
||||||
source: '环球时报',
|
|
||||||
time: 1732333214,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: '习近平向2024年世界互联网大会乌镇峰会开幕视频致贺',
|
|
||||||
content: '新华社北京11月20日电 11月20日,国家主席习近平向2024年世界互联网大会乌镇峰会开幕视频致贺。',
|
|
||||||
source: '环球时报',
|
|
||||||
time: 1732333214,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
// rowSelection object indicates the need for row selection
|
|
||||||
const rowSelection: TableProps<NewsInfo>['rowSelection'] = {
|
|
||||||
onChange: (selectedRowKeys: React.Key[], selectedRows: NewsInfo[]) => {
|
|
||||||
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
|
|
||||||
},
|
|
||||||
getCheckboxProps: (record: NewsInfo) => ({
|
|
||||||
name: record.title,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function NewEdit() {
|
export default function NewEdit() {
|
||||||
const [editId, setEditId] = useState(-1)
|
const [editId, setEditId] = useState(-1)
|
||||||
const articleTags = useArticleTags()
|
const [selectedRowKeys, setSelectedRowKeys] = useState<Id[]>([])
|
||||||
const [params, setParams] = useSetState({
|
const [params, setParams] = useState<ApiArticleSearchParams>({
|
||||||
source: NewsSources.map(s => s.value),
|
pagination: {
|
||||||
search: '',
|
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 10
|
limit: 10
|
||||||
})
|
|
||||||
const {data} = useRequest(async () => {
|
|
||||||
return getArticleList({
|
|
||||||
pagination:{
|
|
||||||
page: params.page,
|
|
||||||
limit: params.limit
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, {
|
const {data} = useRequest(() => getList(params), {refreshDeps: [params]})
|
||||||
refreshDeps: [params]
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSelectChange = (values: string[]) => {
|
|
||||||
if (values.length == 0) {
|
|
||||||
setParams({source: []})
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lastValue = values[values.length - 1];
|
|
||||||
const source = NewsSources.map(s => s.value) || [];
|
|
||||||
const isChecked = values.length > params.source.length; // 是选中还是取消选中
|
|
||||||
if (lastValue == 'all') {
|
|
||||||
setParams({source})
|
|
||||||
} else if (isChecked && values.length == source.length - 1 && !values.includes('all')) { // 除全部之外已经都选了 则直接勾选所有
|
|
||||||
setParams({source})
|
|
||||||
} else {
|
|
||||||
const diffValues = params.source.filter(s => !values.includes(s));
|
|
||||||
// 取消的是全部 则取消所有勾选
|
|
||||||
if (params.source.length > 0 && params.source.length > values.length && diffValues.includes('all')) {
|
|
||||||
setParams({source: []})
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setParams({source: values.filter(s => s != 'all')})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const columns: TableColumnsType<ListArticleItem> = [
|
const columns: TableColumnsType<ListArticleItem> = [
|
||||||
{
|
{
|
||||||
title: '标题',
|
title: '标题',
|
||||||
minWidth:300,
|
minWidth: 300,
|
||||||
dataIndex: 'title',
|
dataIndex: 'title',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '内容',
|
title: '内容',
|
||||||
dataIndex: 'summary',
|
dataIndex: 'summary',
|
||||||
render: (value) => (<Typography.Paragraph style={{marginBottom:0}} ellipsis={{
|
render: (value) => (<Typography.Paragraph style={{marginBottom: 0}} ellipsis={{
|
||||||
rows: 2,expandable: true,symbol:'More'
|
rows: 2, expandable: true, symbol: 'More'
|
||||||
}}>{value}</Typography.Paragraph>)
|
}}>{value}</Typography.Paragraph>)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '来源',
|
title: '来源',
|
||||||
minWidth:150,
|
minWidth: 150,
|
||||||
dataIndex: 'media_name',
|
dataIndex: 'media_name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '时间',
|
title: '时间',
|
||||||
width:150,
|
width: 150,
|
||||||
dataIndex: 'time',
|
dataIndex: 'time',
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
return formatTime(record.publish_time, 'YYYY-MM-DD HH:mm')
|
return formatTime(record.publish_time, 'YYYY-MM-DD HH:mm')
|
||||||
@ -109,7 +49,7 @@ export default function NewEdit() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width:80,
|
width: 80,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (_, record) => (<Button type="link" onClick={() => {
|
render: (_, record) => (<Button type="link" onClick={() => {
|
||||||
setEditId(record.id)
|
setEditId(record.id)
|
||||||
@ -117,72 +57,44 @@ export default function NewEdit() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const rowSelection: TableProps<ListArticleItem>['rowSelection'] = {
|
||||||
|
onChange: (selectedRowKeys: Id[]) => {
|
||||||
|
setSelectedRowKeys(selectedRowKeys)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (<div className="container pb-5 news-edit">
|
return (<div className="container pb-5 news-edit">
|
||||||
<Card className="search-panel-container my-5">
|
<Card className="search-panel-container my-5">
|
||||||
<div className="search-form flex gap-5 justify-between">
|
<div className="search-form flex gap-5 justify-between">
|
||||||
<div className="search-form-input flex gap-2 items-center">
|
<EditSearchForm onSubmit={setParams}/>
|
||||||
<Input
|
|
||||||
onPressEnter={(e) => {
|
|
||||||
setParams({search: e.target.value})
|
|
||||||
}}
|
|
||||||
type="text" className="rounded px-3 w-[250px]"
|
|
||||||
suffix={<SearchOutlined/>}
|
|
||||||
placeholder="请输入你先搜索的关键词"
|
|
||||||
/>
|
|
||||||
<span className="ml-5 text-sm">来源</span>
|
|
||||||
<Cascader
|
|
||||||
options={articleTags}
|
|
||||||
placeholder="请选择你要筛选的新闻"
|
|
||||||
className="w-[250px]"
|
|
||||||
onChange={e=>{
|
|
||||||
console.log('e.target.value',e)
|
|
||||||
}}
|
|
||||||
displayRender={label => label.join('-')}
|
|
||||||
expandTrigger="hover"
|
|
||||||
multiple
|
|
||||||
maxTagCount="responsive"
|
|
||||||
/>
|
|
||||||
{/*<Select*/}
|
|
||||||
{/* value={params.source}*/}
|
|
||||||
{/* className="min-w-[300px] select-no-wrap select-hide-checked max-w-[300px] "*/}
|
|
||||||
{/* options={NewsSources} popupClassName="select-hide-checked"*/}
|
|
||||||
{/* mode="multiple" showSearch={false}*/}
|
|
||||||
{/* onChange={handleSelectChange}*/}
|
|
||||||
{/* placeholder="请选择你要筛选的新闻"*/}
|
|
||||||
{/* optionRender={(option) => (*/}
|
|
||||||
{/* <div className="flex items-center">*/}
|
|
||||||
{/* <span role="icon" className={`checkbox-icon`}></span>*/}
|
|
||||||
{/* <span role="listitem" aria-label={String(option.label)}>{option.label}</span>*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
{/* labelRender={(props) => {*/}
|
|
||||||
{/* if (props.value == 'all') return <span>全部</span>*/}
|
|
||||||
{/* return <span>{props.label}</span>*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/*/>*/}
|
|
||||||
</div>
|
|
||||||
<Button type="primary" onClick={() => setEditId(0)}>手动新增</Button>
|
<Button type="primary" onClick={() => setEditId(0)}>手动新增</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="news-list-container mt-5">
|
<div className="news-list-container mt-5">
|
||||||
<Table<ListArticleItem>
|
<Table<ListArticleItem>
|
||||||
rowSelection={{type: 'checkbox', ...rowSelection}}
|
rowSelection={{type: 'checkbox', ...rowSelection}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.list||[]}
|
dataSource={data?.list || []}
|
||||||
rowKey={'id'}
|
rowKey={'id'}
|
||||||
bordered
|
bordered
|
||||||
pagination={{
|
pagination={false}
|
||||||
position: ['bottomLeft'],
|
|
||||||
simple: true,
|
|
||||||
defaultCurrent: params.page,
|
|
||||||
total: data?.pagination.total || 0,
|
|
||||||
pageSize: params.limit,
|
|
||||||
showSizeChanger: false,
|
|
||||||
rootClassName: 'simple-pagination',
|
|
||||||
onChange: (page) => setParams({page})
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
{data?.pagination.total > 0 && <div className="footer flex justify-between items-center mt-5">
|
||||||
|
<Pagination
|
||||||
|
current={params.pagination.page}
|
||||||
|
total={data?.pagination.total}
|
||||||
|
pageSize={10}
|
||||||
|
showSizeChanger={false}
|
||||||
|
simple={true}
|
||||||
|
rootClassName={'simple-pagination'}
|
||||||
|
onChange={(page) => setParams(prev=>({
|
||||||
|
...prev,
|
||||||
|
pagination: {page, limit: 10}
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<ButtonPush2Video ids={selectedRowKeys} />
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<ArticleEditModal id={editId} onClose={()=>setEditId(-1)} />
|
<ArticleEditModal id={editId} onClose={() => setEditId(-1)}/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
@ -1,115 +1,138 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Button, Checkbox, Modal, Pagination, Space} from "antd";
|
import {Checkbox, Empty, Modal, Pagination, Space} from "antd";
|
||||||
|
import {useRequest, useSetState} from "ahooks";
|
||||||
|
|
||||||
import {Card} from "@/components/card";
|
import {Card} from "@/components/card";
|
||||||
|
import {getList} from "@/service/api/article.ts";
|
||||||
|
|
||||||
import SearchPanel from "@/pages/news/components/search-panel.tsx";
|
import SearchPanel from "@/pages/news/components/search-panel.tsx";
|
||||||
|
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
import {getById} from "@/service/api/news.ts";
|
||||||
|
import {showLoading} from "@/components/message.ts";
|
||||||
function onVideoCreateClick(){}
|
import {formatTime} from "@/util/strings.ts";
|
||||||
function onVideoDownloadClick(){}
|
import ButtonPushNews2Article from "@/pages/news/components/button-push-news2article.tsx";
|
||||||
|
import ButtonNewsDownload from "@/pages/news/components/button-news-download.tsx";
|
||||||
|
|
||||||
export default function NewsIndex() {
|
export default function NewsIndex() {
|
||||||
const [list,] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
||||||
|
const [params, setParams] = useState<ApiArticleSearchParams>({
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
limit: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
const [checkedId, setCheckedId] = useState<number[]>([])
|
const [checkedId, setCheckedId] = useState<number[]>([])
|
||||||
const [activeNews, setActiveNews] = useState<NewsInfo>()
|
const [activeNews, setActiveNews] = useState<NewsInfo>()
|
||||||
|
|
||||||
const [state, setState] = useState<{
|
const [state, setState] = useState<{
|
||||||
checkAll?: boolean;
|
checkAll?: boolean;
|
||||||
}>({})
|
}>({})
|
||||||
|
const {data} = useRequest(() => getList(params), {
|
||||||
|
refreshDeps: [params],
|
||||||
|
onSuccess: () => {
|
||||||
|
setCheckedId([])
|
||||||
|
setState({checkAll:false})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleViewNewsDetail = (id: number) => {
|
||||||
|
const {update, close} = showLoading('获取新闻详情...')
|
||||||
|
getById(id).then(res => {
|
||||||
|
close()
|
||||||
|
setActiveNews(res)
|
||||||
|
}).catch(() => {
|
||||||
|
update('获取新闻详情失败', 'info')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (<div className={'container pb-5'}>
|
return (<div className={'container pb-5'}>
|
||||||
<Card className="search-panel-container my-5">
|
<Card className="search-panel-container my-5">
|
||||||
<SearchPanel/>
|
<SearchPanel onSearch={setParams}/>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="news-list-container">
|
<Card className="news-list-container">
|
||||||
<Modal open={!!activeNews} width={1000} footer={null} onCancel={() => setActiveNews(undefined)}>
|
<Modal open={!!activeNews} width={1000} footer={null} onCancel={() => setActiveNews(undefined)}>
|
||||||
<div className="news-detail px-3 pb-5">
|
<div className="news-detail px-3 pb-5">
|
||||||
<div className="new-title text-2xl">{activeNews?.title}</div>
|
<div className="new-title text-2xl">{activeNews?.title}</div>
|
||||||
<div className="info mt-2 mb-5 text-sm flex gap-3">
|
<div className="info mt-2 mb-5 text-sm flex gap-3">
|
||||||
<span className="source">
|
<span className="source text-blue-700">{activeNews?.media_name}</span>
|
||||||
<a className="text-blue-700 hover:underline"
|
<span className="create-time text-gray-400">{formatTime(activeNews?.publish_time)}</span>
|
||||||
href="https://www.peopleapp.com/column/30047416072-500005929952"
|
|
||||||
target="_blank">人民日报客户端</a>
|
|
||||||
</span>
|
|
||||||
<span className="create-time text-gray-400">2024-11-20 11:11:11</span>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-auto leading-7 text-base" style={{maxHeight: 1000}}>
|
|
||||||
<p>当地时间11月19日下午,国家主席习近平乘专机抵达巴西利亚,开始对巴西进行国事访问。</p>
|
|
||||||
<p>专机抵达巴西利亚空军基地时,巴西总统府首席部长科斯塔、巴西利亚空军基地司令米格尔、司法部长莱万多夫斯基、总统府机构关系部长帕迪利亚等高级官员在机场热情迎接,代表卢拉总统和巴西政府热烈欢迎习近平主席到访。</p>
|
|
||||||
<p>几十名巴塔拉艺术家演奏热情奔放的巴西特色鼓乐。</p>
|
|
||||||
<p>蔡奇、王毅等陪同人员同机抵达。</p>
|
|
||||||
<p>习近平乘车从机场赴下榻饭店途中,当地华侨华人、中资机构和留学生代表在道路旁挥舞中巴两国国旗,高举“欢迎习近平主席访问巴西”“中巴友谊万岁!”等红色横幅,热烈欢迎习近平到访。</p>
|
|
||||||
<p>习近平是在结束二十国集团领导人第十九次峰会活动后离开里约热内卢抵达巴西利亚的。</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="overflow-auto leading-7 text-base"
|
||||||
|
style={{maxHeight: 1000}} dangerouslySetInnerHTML={{__html: activeNews?.content || ''}}></div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className="controls flex justify-between mb-5">
|
<div className="controls flex justify-between mb-1">
|
||||||
<div>
|
<div>
|
||||||
<Checkbox checked={state.checkAll} onChange={e=>{
|
<Checkbox checked={state.checkAll} onChange={e => {
|
||||||
setState({checkAll: e.target.checked})
|
setState({checkAll: e.target.checked})
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
setCheckedId([...list])
|
setCheckedId(data.list.map(item => item.id))
|
||||||
} else {
|
} else {
|
||||||
setCheckedId([])
|
setCheckedId([])
|
||||||
}
|
}
|
||||||
}}>全选</Checkbox>
|
}}>全选</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<Space size={10}>
|
<Space size={10}>
|
||||||
<Button type={'primary'}
|
<ButtonPushNews2Article ids={checkedId}/>
|
||||||
onClick={onVideoCreateClick}>推入视频生成界面</Button>
|
<ButtonNewsDownload ids={checkedId}/>
|
||||||
<Button onClick={onVideoDownloadClick}>下载</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.newsList}>
|
<div className={styles.newsList}>
|
||||||
{list.map(id => (
|
{data?.list?.map(item => (
|
||||||
<div key={id} className={`py-3 flex items-start border-b border-gray-100 group`}>
|
<div key={item.id} className={`py-3 flex items-start border-b border-gray-100 group`}>
|
||||||
<div
|
<div
|
||||||
className={`checkbox mt-[2px] mr-2 ${checkedId.includes(id) ? '' : 'opacity-0'} group-hover:opacity-100`}>
|
className={`checkbox mt-[2px] mr-2 ${checkedId.includes(item.id) ? '' : 'opacity-0'} group-hover:opacity-100`}>
|
||||||
<Checkbox checked={checkedId.includes(id)} onChange={() => {
|
<Checkbox checked={checkedId.includes(item.id)} onChange={() => {
|
||||||
if (checkedId.includes(id)) {
|
if (checkedId.includes(item.id)) {
|
||||||
setCheckedId(checkedId.filter(item => item != id))
|
setCheckedId(checkedId.filter(id => id != item.id))
|
||||||
} else {
|
} else {
|
||||||
setCheckedId([...checkedId, id])
|
setCheckedId([...checkedId, item.id])
|
||||||
}
|
}
|
||||||
}}/>
|
}}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="news-content">
|
<div className="news-content">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="title text-lg cursor-pointer" onClick={() => {
|
<div className="title text-lg cursor-pointer" onClick={() => {
|
||||||
setActiveNews({
|
handleViewNewsDetail(item.id)
|
||||||
id: 1,
|
}}>{item.id}{item.title}</div>
|
||||||
title: '习近平抵达巴西利亚开始对巴西进行国事访问',
|
{item.internal_article_id > 0 &&
|
||||||
content: '', cover: "", source: "", time: ""
|
<div className="text-sm text-blue-500">已加入编辑界面</div>}
|
||||||
})
|
|
||||||
}}>习近平抵达巴西利亚开始对巴西进行国事访问
|
|
||||||
</div>
|
|
||||||
{id == 1 && <div className="text-sm text-blue-500">已加入编辑界面</div>}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="content flex gap-3 mt-2 mb-3">
|
<div className="content flex gap-3 mt-2 mb-3">
|
||||||
<div className="cover border border-gray-100 flex items-center rounded overflow-hidden"
|
{item.cover && <div
|
||||||
|
className="cover border border-gray-100 flex items-center rounded overflow-hidden"
|
||||||
style={{width: 100, height: 100}}>
|
style={{width: 100, height: 100}}>
|
||||||
<img className="w-full h-full object-cover"
|
<img className="w-full h-full object-cover" src={item.cover}/>
|
||||||
src={'https://file.wx.wm-app.xyz/os/picture/20241119160600.png'}/>
|
</div>}
|
||||||
</div>
|
|
||||||
<div className="text text-gray-600 text-sm leading-6 flex-1 text-justify">
|
<div className="text text-gray-600 text-sm leading-6 flex-1 text-justify">
|
||||||
当地时间11月19日下午,国家主席习近平乘专机抵达巴西利亚,开始对巴西进行国事访问。<br/>
|
{item.summary}
|
||||||
专机抵达巴西利亚空军基地时,巴西总统府首席部长科斯塔、巴西利亚空军基地司令米格尔、司法部长莱万多夫斯基、总统府机构关系部长帕迪利亚等高级官员在机场热情迎接,代表卢拉总统和巴西政府热烈欢迎习近平主席到访。
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="info text-gray-300 flex items-center justify-between gap-3 text-sm">
|
<div className="info text-gray-300 flex items-center justify-between gap-3 text-sm">
|
||||||
<div>来源: <span>新华社</span></div>
|
<div>来源: <span>{item.media_name}</span></div>
|
||||||
{/*<Divider type="vertical" />*/}
|
{/*<Divider type="vertical" />*/}
|
||||||
<div>发布时间: <span>2024-11-18 10:10:12</span></div>
|
<div>发布时间: <span>{item.publish_time}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center mt-10">
|
{data?.pagination.total > 0 ? <div className="flex justify-center mt-10">
|
||||||
<Pagination defaultCurrent={1} total={50}/>
|
<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>
|
</div>
|
||||||
|
}
|
||||||
</Card>
|
</Card>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
@ -30,7 +30,7 @@ const NavigationUserContainer = () => {
|
|||||||
}}>退出</div>,
|
}}>退出</div>,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (<div className={"flex items-center justify-between gap-2"}>
|
return (<div className={"flex items-center justify-between gap-2 ml-10"}>
|
||||||
<Dropdown menu={{items}} placement="bottom" arrow>
|
<Dropdown menu={{items}} placement="bottom" arrow>
|
||||||
<div className="flex items-center hover:bg-gray-100 px-2 py-1 cursor-pointer rounded">
|
<div className="flex items-center hover:bg-gray-100 px-2 py-1 cursor-pointer rounded">
|
||||||
<UserAvatar className="user-avatar size-8"/>
|
<UserAvatar className="user-avatar size-8"/>
|
||||||
@ -46,9 +46,11 @@ export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
|
|||||||
<div className="logo-container">
|
<div className="logo-container">
|
||||||
<LogoText/>
|
<LogoText/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
<DashboardNavigation/>
|
<DashboardNavigation/>
|
||||||
<NavigationUserContainer/>
|
<NavigationUserContainer/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="app-content flex-1 box-sizing">
|
<div className="app-content flex-1 box-sizing">
|
||||||
<div className="content-container">
|
<div className="content-container">
|
||||||
{children}
|
{children}
|
||||||
|
@ -5,32 +5,32 @@ import {NavLink} from "react-router-dom";
|
|||||||
const NavItems = [
|
const NavItems = [
|
||||||
{
|
{
|
||||||
key: 'news',
|
key: 'news',
|
||||||
name: '新闻素材库',
|
name: '新闻素材',
|
||||||
icon: '+',
|
icon: 'news',
|
||||||
path:'/'
|
path:'/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'video',
|
key: 'video',
|
||||||
name: '新闻素材编辑',
|
name: '新闻编辑',
|
||||||
icon: '+',
|
icon: 'e',
|
||||||
path:'/edit'
|
path:'/edit'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'create',
|
key: 'create',
|
||||||
name: '数字人视频生成',
|
name: 'AI视频',
|
||||||
icon: '+',
|
icon: 'ai',
|
||||||
path:'/create'
|
path:'/create'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'library',
|
key: 'library',
|
||||||
name: '数字人视频库',
|
name: '视频库',
|
||||||
icon: '+',
|
icon: '+',
|
||||||
path:'/library'
|
path:'/library'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'live',
|
key: 'live',
|
||||||
name: '数字人直播间',
|
name: '数字人直播间',
|
||||||
icon: '+',
|
icon: 'v',
|
||||||
path:'/live'
|
path:'/live'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -38,8 +38,8 @@ const NavItems = [
|
|||||||
export function DashboardNavigation() {
|
export function DashboardNavigation() {
|
||||||
return (<div className={'flex'}>
|
return (<div className={'flex'}>
|
||||||
{NavItems.map((it, idx) => (
|
{NavItems.map((it, idx) => (
|
||||||
<NavLink to={it.path} key={it.key} className={clsx('nav-item cursor-pointer items-center')}>
|
<NavLink to={it.path} key={idx} className={clsx('nav-item cursor-pointer items-center')}>
|
||||||
<span className="menu-text ml-2">{it.name}</span>
|
<span className="menu-text ml-1">{it.name}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@ export function getAllCategory() {
|
|||||||
return post<{ tags: ArticleCategory[] }>({url: '/spider/tags'})
|
return post<{ tags: ArticleCategory[] }>({url: '/spider/tags'})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getArticleList(data: ApiArticleSearchParams & ApiRequestPageParams) {
|
export function getList(data: ApiArticleSearchParams) {
|
||||||
return post<DataList<ListArticleItem>>({url: '/article/search', data})
|
return post<DataList<ListArticleItem>>({url: '/article/search', data})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,17 +12,19 @@ export function getArticleList(data: ApiArticleSearchParams & ApiRequestPagePara
|
|||||||
* 删除 【本期不做】
|
* 删除 【本期不做】
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
export function deleteArticle(id: Id) {
|
export function deleteById(id: Id) {
|
||||||
throw new Error('Not implement')
|
throw new Error('Not implement')
|
||||||
return post<{ article: any }>({url: '/article/delete/' + id})
|
return post<{ article: any }>({url: '/article/delete/' + id})
|
||||||
}
|
}
|
||||||
export function getArticleDetail(id: Id) {
|
|
||||||
|
export function getById(id: Id) {
|
||||||
return post<ArticleDetail>({url: '/article/detail/' + id})
|
return post<ArticleDetail>({url: '/article/detail/' + id})
|
||||||
}
|
}
|
||||||
export function saveArticle(title:string,content_group: BlockContent[][],id: number) {
|
|
||||||
|
export function save(title: string, content_group: BlockContent[][], id: number) {
|
||||||
return post<{ content: string }>({
|
return post<{ content: string }>({
|
||||||
url: '/spider/article',
|
url: '/spider/article',
|
||||||
data:{
|
data: {
|
||||||
title,
|
title,
|
||||||
content_group,
|
content_group,
|
||||||
id
|
id
|
||||||
|
@ -3,8 +3,8 @@ import {post} from "@/service/request.ts";
|
|||||||
export function getOssPolicy(scene = 'workbench') {
|
export function getOssPolicy(scene = 'workbench') {
|
||||||
return post<TOSSPolicy>({
|
return post<TOSSPolicy>({
|
||||||
data: {scene},
|
data: {scene},
|
||||||
baseURL: '/api/v1/common/get_oss_policy',
|
baseURL: '/api/v1',
|
||||||
url: `/api/v1/common/get_oss_policy`
|
url: `/common/get_oss_policy`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import {post} from "@/service/request.ts";
|
||||||
|
|
||||||
|
export function getList(data: ApiArticleSearchParams & ApiRequestPageParams) {
|
||||||
|
return post<DataList<ListCrawlerNewsItem>>({url: '/article/search', data})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getById(id: Id) {
|
||||||
|
return post<NewsInfo>({url: '/spider/detail/' + id})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function push2article(ids: Id[]) {
|
||||||
|
return post({url: '/spider/push2article', data: {spider_ids: ids}})
|
||||||
|
}
|
39
src/service/api/video.ts
Normal file
39
src/service/api/video.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {post} from "@/service/request.ts";
|
||||||
|
|
||||||
|
export function getList(data: {
|
||||||
|
title?: string,
|
||||||
|
time_flag?: number;
|
||||||
|
}) {
|
||||||
|
return post<DataList<VideoInfo>>({url: '/video/list', data})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频列表的文章编辑(需要重新生成视频)
|
||||||
|
* @param title
|
||||||
|
* @param content_group
|
||||||
|
* @param article_id
|
||||||
|
*/
|
||||||
|
export function regenerate(title: string, content_group: BlockContent[][], article_id: number) {
|
||||||
|
return post<{ content: string }>({
|
||||||
|
url: '/video/regenerate',
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
content_group,
|
||||||
|
article_id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getById(id: Id) {
|
||||||
|
return post<VideoInfo>({url: '/video/detail/' + id})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteById(id: Id) {
|
||||||
|
return post({url: '/video/detail/' + id})
|
||||||
|
}
|
||||||
|
export function modifyOrder(ids: Id[]) {
|
||||||
|
return post({url: ' /video/modifyorder',data:{ids}})
|
||||||
|
}
|
||||||
|
export function push2room(ids: Id[]) {
|
||||||
|
return post({url: ' /video/push2room',data:{ids}})
|
||||||
|
}
|
26
src/types/api.d.ts
vendored
26
src/types/api.d.ts
vendored
@ -5,13 +5,18 @@ declare interface ApiRequestPageParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface ApiArticleSearchParams {
|
declare interface ApiArticleSearchParams extends ApiRequestPageParams{
|
||||||
// 1级标签id
|
// // 1级标签id
|
||||||
tag_level_1_id?: number;
|
// tag_level_1_id?: number;
|
||||||
// 2级标签id 没有则为0
|
// // 2级标签id 没有则为0
|
||||||
tag_level_2_id?: number;
|
// tag_level_2_id?: number;
|
||||||
|
tags?: {
|
||||||
|
level1: Id;
|
||||||
|
level2: Id;
|
||||||
|
}[];
|
||||||
// 标题
|
// 标题
|
||||||
title?: string;
|
title?: string;
|
||||||
|
time_flag?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface DataList<T> {
|
declare interface DataList<T> {
|
||||||
@ -35,7 +40,7 @@ interface ArticleCategory extends BaseArticleCategory {
|
|||||||
sons: BaseArticleCategory[];
|
sons: BaseArticleCategory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface VideoInfo {
|
declare interface VideoInfo1 {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
cover: string;
|
cover: string;
|
||||||
@ -75,3 +80,12 @@ declare interface ListCrawlerNewsItem extends BasicArticleInfo {
|
|||||||
// 内部文章关联id
|
// 内部文章关联id
|
||||||
internal_article_id: number;
|
internal_article_id: number;
|
||||||
}
|
}
|
||||||
|
declare interface VideoInfo {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
cover?: string;
|
||||||
|
oss_video_url: string;
|
||||||
|
duration: number;
|
||||||
|
article_id: number;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
10
src/types/core.d.ts
vendored
10
src/types/core.d.ts
vendored
@ -11,9 +11,10 @@ declare interface RecordList<T> {
|
|||||||
filter?: string;
|
filter?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface OptionItem {
|
declare interface OptionItem {
|
||||||
label: string;
|
label: string;
|
||||||
value: string|number;
|
value: string | number;
|
||||||
children?: OptionItem[];
|
children?: OptionItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,13 +35,12 @@ declare interface ArticleDetail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare interface NewsInfo {
|
declare interface NewsInfo {
|
||||||
id: number;
|
|
||||||
title: string;
|
title: string;
|
||||||
cover?: string;
|
|
||||||
content: string;
|
content: string;
|
||||||
source: string;
|
media_name: string;
|
||||||
time: string|number;
|
publish_time: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface TOSSPolicy {
|
declare interface TOSSPolicy {
|
||||||
//Oss access id
|
//Oss access id
|
||||||
access_id: string;
|
access_id: string;
|
||||||
|
@ -32,9 +32,13 @@ export default defineConfig(({mode}) => {
|
|||||||
port: 10021,
|
port: 10021,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/mgmt': {
|
'/mgmt': {
|
||||||
target: 'http://192.168.0.231:9999', //\
|
target: 'http://192.168.0.231:9999',
|
||||||
// changeOrigin: true,
|
changeOrigin: true,
|
||||||
// ws: true,
|
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
|
},
|
||||||
|
'/api': {
|
||||||
|
target: 'http://192.168.0.231:9999',
|
||||||
|
changeOrigin: true,
|
||||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
76
yarn.lock
76
yarn.lock
@ -1291,6 +1291,11 @@ copy-to-clipboard@^3.3.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
toggle-selection "^1.0.6"
|
toggle-selection "^1.0.6"
|
||||||
|
|
||||||
|
core-util-is@~1.0.0:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||||
|
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||||
|
|
||||||
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
|
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
|
||||||
version "7.0.5"
|
version "7.0.5"
|
||||||
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
|
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
|
||||||
@ -1786,6 +1791,11 @@ ignore@^5.2.0, ignore@^5.3.1:
|
|||||||
resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||||
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
|
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
|
||||||
|
|
||||||
|
immediate@~3.0.5:
|
||||||
|
version "3.0.6"
|
||||||
|
resolved "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||||
|
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
|
||||||
|
|
||||||
immutable@^5.0.2:
|
immutable@^5.0.2:
|
||||||
version "5.0.2"
|
version "5.0.2"
|
||||||
resolved "https://registry.npmmirror.com/immutable/-/immutable-5.0.2.tgz#bb8a987349a73efbe6b3b292a9cbaf1b530d296b"
|
resolved "https://registry.npmmirror.com/immutable/-/immutable-5.0.2.tgz#bb8a987349a73efbe6b3b292a9cbaf1b530d296b"
|
||||||
@ -1812,7 +1822,7 @@ inflight@^1.0.4:
|
|||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
inherits@2:
|
inherits@2, inherits@~2.0.3:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
@ -1863,6 +1873,11 @@ is-path-inside@^3.0.3:
|
|||||||
resolved "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
resolved "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
|
||||||
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
|
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
|
||||||
|
|
||||||
|
isarray@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||||
|
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
|
||||||
|
|
||||||
isexe@^2.0.0:
|
isexe@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||||
@ -1931,6 +1946,16 @@ json5@^2.2.3:
|
|||||||
resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||||
|
|
||||||
|
jszip@^3.10.1:
|
||||||
|
version "3.10.1"
|
||||||
|
resolved "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
|
||||||
|
integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
|
||||||
|
dependencies:
|
||||||
|
lie "~3.3.0"
|
||||||
|
pako "~1.0.2"
|
||||||
|
readable-stream "~2.3.6"
|
||||||
|
setimmediate "^1.0.5"
|
||||||
|
|
||||||
keyv@^4.5.3:
|
keyv@^4.5.3:
|
||||||
version "4.5.4"
|
version "4.5.4"
|
||||||
resolved "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
resolved "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
||||||
@ -1946,6 +1971,13 @@ levn@^0.4.1:
|
|||||||
prelude-ls "^1.2.1"
|
prelude-ls "^1.2.1"
|
||||||
type-check "~0.4.0"
|
type-check "~0.4.0"
|
||||||
|
|
||||||
|
lie@~3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
|
||||||
|
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
|
||||||
|
dependencies:
|
||||||
|
immediate "~3.0.5"
|
||||||
|
|
||||||
lilconfig@^2.1.0:
|
lilconfig@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
|
resolved "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
|
||||||
@ -2148,6 +2180,11 @@ package-json-from-dist@^1.0.0:
|
|||||||
resolved "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
|
resolved "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
|
||||||
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
|
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
|
||||||
|
|
||||||
|
pako@~1.0.2:
|
||||||
|
version "1.0.11"
|
||||||
|
resolved "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||||
|
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||||
|
|
||||||
parent-module@^1.0.0:
|
parent-module@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
resolved "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||||
@ -2266,6 +2303,11 @@ prelude-ls@^1.2.1:
|
|||||||
resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||||
|
|
||||||
|
process-nextick-args@~2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||||
|
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
||||||
|
|
||||||
prop-types@^15.7.2:
|
prop-types@^15.7.2:
|
||||||
version "15.8.1"
|
version "15.8.1"
|
||||||
resolved "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
@ -2715,6 +2757,19 @@ read-cache@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pify "^2.3.0"
|
pify "^2.3.0"
|
||||||
|
|
||||||
|
readable-stream@~2.3.6:
|
||||||
|
version "2.3.8"
|
||||||
|
resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
|
||||||
|
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
|
||||||
|
dependencies:
|
||||||
|
core-util-is "~1.0.0"
|
||||||
|
inherits "~2.0.3"
|
||||||
|
isarray "~1.0.0"
|
||||||
|
process-nextick-args "~2.0.0"
|
||||||
|
safe-buffer "~5.1.1"
|
||||||
|
string_decoder "~1.1.1"
|
||||||
|
util-deprecate "~1.0.1"
|
||||||
|
|
||||||
readdirp@^4.0.1:
|
readdirp@^4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
|
resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
|
||||||
@ -2797,6 +2852,11 @@ run-parallel@^1.1.9:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask "^1.2.2"
|
queue-microtask "^1.2.2"
|
||||||
|
|
||||||
|
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||||
|
version "5.1.2"
|
||||||
|
resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||||
|
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||||
|
|
||||||
sass@^1.81.0:
|
sass@^1.81.0:
|
||||||
version "1.81.0"
|
version "1.81.0"
|
||||||
resolved "https://registry.npmmirror.com/sass/-/sass-1.81.0.tgz#a9010c0599867909dfdbad057e4a6fbdd5eec941"
|
resolved "https://registry.npmmirror.com/sass/-/sass-1.81.0.tgz#a9010c0599867909dfdbad057e4a6fbdd5eec941"
|
||||||
@ -2849,6 +2909,11 @@ set-function-length@^1.2.1:
|
|||||||
gopd "^1.0.1"
|
gopd "^1.0.1"
|
||||||
has-property-descriptors "^1.0.2"
|
has-property-descriptors "^1.0.2"
|
||||||
|
|
||||||
|
setimmediate@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||||
|
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
|
||||||
|
|
||||||
shebang-command@^2.0.0:
|
shebang-command@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||||
@ -2910,6 +2975,13 @@ string-width@^5.0.1, string-width@^5.1.2:
|
|||||||
emoji-regex "^9.2.2"
|
emoji-regex "^9.2.2"
|
||||||
strip-ansi "^7.0.1"
|
strip-ansi "^7.0.1"
|
||||||
|
|
||||||
|
string_decoder@~1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||||
|
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||||
|
dependencies:
|
||||||
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
@ -3075,7 +3147,7 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
util-deprecate@^1.0.2:
|
util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user