feat: update api

This commit is contained in:
LittleBoy 2024-12-13 20:11:51 +08:00
parent ca074f59b5
commit f946e9d4f7
34 changed files with 910 additions and 535 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +18,14 @@ 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({
@ -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>

View File

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

View File

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

View File

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

View 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"
/>)
}

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

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

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

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

View File

@ -1,4 +1,3 @@
/* /*
@ -34,147 +33,77 @@
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
} }
] ]

View File

@ -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,22 +75,22 @@ 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}
@ -80,7 +101,7 @@ export default function SearchPanel({onSearch}: SearchPanelProps) {
) )
} }
</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

View File

@ -1,85 +1,25 @@
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> = [
{ {
@ -117,50 +57,16 @@ 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">
@ -170,17 +76,23 @@ export default function NewEdit() {
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>

View File

@ -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>1119西西访</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">
1119西西访<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>)
} }

View File

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

View File

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

View File

@ -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,14 +12,16 @@ 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: {

View File

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

View File

@ -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
View 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
View File

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

8
src/types/core.d.ts vendored
View File

@ -11,6 +11,7 @@ declare interface RecordList<T> {
filter?: string; filter?: string;
}; };
} }
declare interface OptionItem { declare interface OptionItem {
label: string; label: string;
value: string | number; value: string | number;
@ -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;

View File

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

View File

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