feat: 开始集成及与API联调
This commit is contained in:
parent
ef5da31ea5
commit
ca074f59b5
@ -9,6 +9,7 @@ import {Button, Popconfirm} from "antd";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
index?:number;
|
||||||
className?: string;
|
className?: string;
|
||||||
blocks: BlockContent[];
|
blocks: BlockContent[];
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
@ -17,7 +18,7 @@ type Props = {
|
|||||||
onAdd?: () => void;
|
onAdd?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ArticleBlock({className, blocks, editable, onRemove, onAdd, onChange}: Props) {
|
export default function ArticleBlock({className, blocks, editable, onRemove, onAdd, onChange,index}: Props) {
|
||||||
const handleBlockRemove = (index: number) => {
|
const handleBlockRemove = (index: number) => {
|
||||||
// 删除当前项
|
// 删除当前项
|
||||||
onChange?.(blocks.filter((_, idx) => index !== idx))
|
onChange?.(blocks.filter((_, idx) => index !== idx))
|
||||||
@ -47,7 +48,7 @@ export default function ArticleBlock({className, blocks, editable, onRemove, onA
|
|||||||
<div key={idx} className={clsx(styles.blockItem, 'flex')}>
|
<div key={idx} className={clsx(styles.blockItem, 'flex')}>
|
||||||
{
|
{
|
||||||
it.type === 'text'
|
it.type === 'text'
|
||||||
? <BlockText onChange={(block) => handleBlockChange(idx, block)} data={it}
|
? <BlockText groupIndex={index} blockIndex={idx} onChange={(block) => handleBlockChange(idx, block)} data={it}
|
||||||
editable={editable}/>
|
editable={editable}/>
|
||||||
: <BlockImage data={it} editable={editable}/>
|
: <BlockImage data={it} editable={editable}/>
|
||||||
}
|
}
|
||||||
|
@ -2,42 +2,56 @@ 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";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title?: string;
|
id?: number;
|
||||||
groups?: ArticleContentGroup[];
|
onClose?: () => void;
|
||||||
onSave?: () => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ArticleEditModal(props: Props) {
|
export default function ArticleEditModal(props: Props) {
|
||||||
|
|
||||||
const [groups, setGroups] = useState<ArticleContentGroup[]>([]);
|
const [groups, setGroups] = useState<BlockContent[][]>([]);
|
||||||
const [title, setTitle] = useState(props.title)
|
const [title, setTitle] = useState('')
|
||||||
|
|
||||||
const [state, setState] = useSetState({
|
const [state, setState] = useSetState({
|
||||||
loading: false,
|
loading: false,
|
||||||
open: false
|
open: false
|
||||||
})
|
})
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
setState({loading: true})
|
props.onClose?.()
|
||||||
props.onSave?.().finally(() => {
|
// if (props.onSave) {
|
||||||
setState({loading: false,open: false})
|
// setState({loading: true})
|
||||||
})
|
// props.onSave?.().then(() => {
|
||||||
|
// setState({loading: false, open: false})
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// console.log(groups)
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState({open: typeof(props.title) != "undefined"})
|
if(props.id){
|
||||||
setGroups(props.groups || [])
|
if(props.id > 0){
|
||||||
setTitle(props.title||'')
|
getArticleDetail(props.id).then(res => {
|
||||||
}, [props.title,props.groups])
|
setGroups(res.content_group)
|
||||||
|
setTitle(res.title)
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
setGroups([])
|
||||||
|
setTitle('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.id])
|
||||||
|
|
||||||
return (<Modal
|
return (<Modal
|
||||||
title={'编辑文章'}
|
title={'编辑文章'}
|
||||||
open={state.open}
|
open={props.id >= 0}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
keyboard={false}
|
keyboard={false}
|
||||||
width={800}
|
width={800}
|
||||||
onCancel={()=>setState({open: false})}
|
onCancel={props.onClose}
|
||||||
okButtonProps={{loading: state.loading}}
|
okButtonProps={{loading: state.loading}}
|
||||||
coOk={handleSave}
|
onOk={handleSave}
|
||||||
>
|
>
|
||||||
<div className="article-title mt-5">
|
<div className="article-title mt-5">
|
||||||
<div className="title">
|
<div className="title">
|
||||||
|
@ -5,9 +5,9 @@ import ArticleBlock from "@/components/article/block.tsx";
|
|||||||
import styles from './article.module.scss'
|
import styles from './article.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
groups: ArticleContentGroup[];
|
groups: BlockContent[][];
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
onChange?: (groups: ArticleContentGroup[]) => void;
|
onChange?: (groups: BlockContent[][]) => void;
|
||||||
}
|
}
|
||||||
export default function ArticleGroup({groups, editable, onChange}: Props) {
|
export default function ArticleGroup({groups, editable, onChange}: Props) {
|
||||||
/**
|
/**
|
||||||
@ -15,7 +15,7 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
|
|||||||
* @param insertIndex 插入的位置,-1表示插入到末尾
|
* @param insertIndex 插入的位置,-1表示插入到末尾
|
||||||
*/
|
*/
|
||||||
const handleAddGroup = ( insertIndex: number = -1) => {
|
const handleAddGroup = ( insertIndex: number = -1) => {
|
||||||
const newGroup: ArticleContentGroup = {blocks: []}
|
const newGroup: BlockContent[] = []
|
||||||
const _groups = [...groups]
|
const _groups = [...groups]
|
||||||
if (insertIndex == -1 || insertIndex >= groups.length) { // -1或者越界表示新增
|
if (insertIndex == -1 || insertIndex >= groups.length) { // -1或者越界表示新增
|
||||||
_groups.push(newGroup)
|
_groups.push(newGroup)
|
||||||
@ -27,11 +27,12 @@ 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.blocks}
|
editable={editable} key={index} blocks={g}
|
||||||
onChange={(blocks) => {
|
onChange={(blocks) => {
|
||||||
groups[index].blocks = blocks
|
groups[index] = blocks
|
||||||
onChange?.([...groups])
|
onChange?.([...groups])
|
||||||
}}
|
}}
|
||||||
|
index={index}
|
||||||
onAdd={() => {
|
onAdd={() => {
|
||||||
handleAddGroup?.(index + 1)
|
handleAddGroup?.(index + 1)
|
||||||
}}
|
}}
|
||||||
@ -45,6 +46,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])} blocks={[]}/>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -1,24 +1,28 @@
|
|||||||
import React from "react";
|
import React, {useMemo, useRef, useState} from "react";
|
||||||
import styles from './article.module.scss'
|
|
||||||
import {Button, Input, Upload} from "antd";
|
import {Button, Input, Upload} from "antd";
|
||||||
|
import {TextAreaRef} from "antd/es/input/TextArea";
|
||||||
|
|
||||||
|
import styles from './article.module.scss'
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlockImage({data,editable}: Props) {
|
export function BlockImage({data, editable}: Props) {
|
||||||
return <div className={styles.image}>
|
return <div className={styles.image}>
|
||||||
{editable ? <div>
|
{editable ? <div>
|
||||||
<Upload accept="image/*">
|
<Upload accept="image/*">
|
||||||
<div className={styles.uploadImage} >
|
<div className={styles.uploadImage}>
|
||||||
{ data.content ? <>
|
{data.content ? <>
|
||||||
<img src={data.content}/>
|
<img src={data.content}/>
|
||||||
<div className={styles.uploadTips}>
|
<div className={styles.uploadTips}>
|
||||||
<span>编辑</span>
|
<span>更换图片</span>
|
||||||
</div>
|
</div>
|
||||||
</> : <div className={styles.imagePlaceholder}>
|
</> : <div className={styles.imagePlaceholder}>
|
||||||
<Button>选择图片</Button>
|
<Button>选择图片</Button>
|
||||||
@ -29,14 +33,65 @@ export function BlockImage({data,editable}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlockText({data,editable,onChange}: Props) {
|
export function BlockText({data, editable, onChange, groupIndex,blockIndex}: Props) {
|
||||||
|
const inputRef = useRef<TextAreaRef | null>(null);
|
||||||
|
// 内容分割
|
||||||
|
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}>
|
return <div className={styles.text}>
|
||||||
{editable ? <div>
|
{editable ? <div className="relative">
|
||||||
{/*<textarea className={styles.textarea} defaultValue={data.content}/>*/}
|
{/*<textarea*/}
|
||||||
<Input.TextArea onChange={e => {
|
{/* className={"ant-input ant-input-borderless w-full min-h-[40px] max-h-[120px] overflow-auto p-2"}*/}
|
||||||
onChange?.({type:'text',content:e.target.value})
|
{/* value={data.content}*/}
|
||||||
// console.log(e)
|
{/* onChange={e=>onChange?.({type:'text',content:e.target.value})}*/}
|
||||||
}} placeholder={'请输入文本'} value={data.content} autoSize={{minRows:3}} variant={"borderless"} />
|
{/*></textarea>*/}
|
||||||
</div>: <p className="p-2">{data.content}</p>}
|
<Input.TextArea
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={e => {
|
||||||
|
onChange?.({type: 'text', content: e.target.value})
|
||||||
|
}}
|
||||||
|
placeholder={'请输入文本'} onBlur={handleTextBlur} value={data.content} autoSize={{minRows: 3}}
|
||||||
|
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>
|
</div>
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ import {useState} from "react";
|
|||||||
|
|
||||||
import InputContainer from "@/components/form/input-container.tsx";
|
import InputContainer from "@/components/form/input-container.tsx";
|
||||||
import {clsx} from "clsx";
|
import {clsx} from "clsx";
|
||||||
|
import {sendSmsCode} from "@/service/api/user.ts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onChange?: (code: string) => void;
|
onChange?: (code: string) => void;
|
||||||
@ -23,10 +24,11 @@ export function useSmsCode() {
|
|||||||
const sendCode = (phone?:string,interval = 60) => {
|
const sendCode = (phone?:string,interval = 60) => {
|
||||||
if (countdown > 0 || sending || !phone) return;
|
if (countdown > 0 || sending || !phone) return;
|
||||||
setSending(true)
|
setSending(true)
|
||||||
setTimeout(() => {
|
sendSmsCode(phone).then(()=>{
|
||||||
setTargetDate(Date.now() + interval * 1000)
|
setTargetDate(Date.now() + interval * 1000)
|
||||||
|
}).finally(()=>{
|
||||||
setSending(false)
|
setSending(false)
|
||||||
}, 500)
|
})
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
sendCode,
|
sendCode,
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
import Avatar from "@/assets/images/avatar.png";
|
// import Avatar from "@/assets/images/avatar.png";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const UserAvatar = ( {className,style}: { style?: React.CSSProperties;className?: string }) => {
|
export const UserAvatar = ({className, style}: { style?: React.CSSProperties; className?: string }) => {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<img src={Avatar} style={style} className={className}/>
|
{/* <img src={Avatar} style={style} className={className}/> */}
|
||||||
|
<svg style={style} className={className} viewBox="0 0 1024 1024" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" width="128" height="128">
|
||||||
|
<path
|
||||||
|
d="M21.333 512a490.667 490.667 0 1 0 981.334 0 490.667 490.667 0 1 0-981.334 0z"
|
||||||
|
fill="hsl(214, 100%, 80%)"/>
|
||||||
|
<path
|
||||||
|
d="M155.733 849.067c8.534-27.734 19.2-46.934 32-57.6C204.8 774.4 403.2 710.4 428.8 682.667c25.6-27.734 19.2-59.734 14.933-74.667-4.266-14.933-49.066-59.733-59.733-106.667-27.733-27.733-34.133-78.933-21.333-87.466-12.8-76.8-6.4-108.8 8.533-132.267s27.733-83.2 87.467-98.133 87.466 12.8 125.866 10.666C620.8 192 620.8 183.467 640 192c19.2 8.533 2.133 53.333 19.2 93.867 17.067 40.533 10.667 61.866 6.4 123.733 21.333 34.133-6.4 78.933-21.333 91.733C633.6 554.667 590.933 595.2 586.667 608c-2.134 12.8-10.667 72.533 57.6 104.533 68.266 32 170.666 61.867 198.4 78.934 17.066 12.8 27.733 32 27.733 57.6C772.267 953.6 652.8 1004.8 512 1002.667c-140.8-2.134-260.267-53.334-356.267-153.6z"
|
||||||
|
fill="#F0EEFF"/>
|
||||||
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -9,6 +9,8 @@ import {Popconfirm} from "antd";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
video: VideoInfo,
|
video: VideoInfo,
|
||||||
|
editable?: boolean;
|
||||||
|
sortable?: boolean;
|
||||||
index?: number;
|
index?: number;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
@ -16,17 +18,22 @@ type Props = {
|
|||||||
onPlay?: () => void;
|
onPlay?: () => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
id:number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VideoListItem = ({index,id, video, onPlay, onRemove, checked, onCheckedChange,onEdit,active}: Props) => {
|
export const VideoListItem = (
|
||||||
|
{
|
||||||
|
index, id, video, onPlay, onRemove, checked,
|
||||||
|
onCheckedChange, onEdit, active, editable,
|
||||||
|
sortable
|
||||||
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
attributes, listeners,
|
attributes, listeners,
|
||||||
setNodeRef, transform
|
setNodeRef, transform
|
||||||
} = useSortable({resizeObserverConfig: {}, id})
|
} = useSortable({resizeObserverConfig: {}, id})
|
||||||
|
|
||||||
|
|
||||||
const [state, setState] = useSetState<{checked?:boolean}>({})
|
const [state, setState] = useSetState<{ checked?: boolean }>({})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState({checked})
|
setState({checked})
|
||||||
}, [checked])
|
}, [checked])
|
||||||
@ -38,34 +45,41 @@ export const VideoListItem = ({index,id, video, onPlay, onRemove, checked, onChe
|
|||||||
<div
|
<div
|
||||||
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{id}</div>
|
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{id}</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':''}`}>
|
<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' : ''}`}>
|
||||||
<div className={'video-title leading-7 flex-1'}>{video.id} - {video.title}</div>
|
<div className={'video-title leading-7 flex-1'}>{video.id} - {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>
|
||||||
<div className="operation flex items-center ml-2 gap-3 text-lg text-gray-400">
|
{editable &&
|
||||||
<button className="hover:text-blue-500" {...attributes} {...listeners}>
|
<div className="operation flex items-center ml-2 gap-3 text-lg text-gray-400">
|
||||||
<MenuOutlined/>
|
{!active ? <button className="hover:text-blue-500 cursor-move" {...attributes} {...listeners}>
|
||||||
</button>
|
<MenuOutlined/>
|
||||||
{onPlay && <button className="hover:text-blue-500" onClick={onPlay} style={{fontSize: '1.3em'}}><IconPlay/>
|
</button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>}
|
||||||
</button>}
|
{onPlay &&
|
||||||
{onEdit && <button className="hover:text-blue-500" onClick={onEdit} style={{fontSize: '1.1em'}}><IconEdit/>
|
<button className="hover:text-blue-500" onClick={onPlay} style={{fontSize: '1.3em'}}><IconPlay/>
|
||||||
</button>}
|
</button>}
|
||||||
<button className="hover:text-blue-300" onClick={() => {
|
{onEdit &&
|
||||||
if (onCheckedChange) {
|
<button className="hover:text-blue-500" onClick={onEdit} style={{fontSize: '1.1em'}}><IconEdit/>
|
||||||
onCheckedChange(!state.checked)
|
</button>}
|
||||||
} else {
|
<button className="hover:text-blue-300" onClick={() => {
|
||||||
setState({checked: !state.checked})
|
if (onCheckedChange) {
|
||||||
}
|
onCheckedChange(!state.checked)
|
||||||
}}><CheckCircleFilled className={clsx({'text-blue-500': state.checked})}/></button>
|
} else {
|
||||||
{onRemove && <Popconfirm
|
setState({checked: !state.checked})
|
||||||
title="提示"
|
}
|
||||||
description={<div style={{minWidth: 150}}><span>请确认删除此视频?</span></div>}
|
}}><CheckCircleFilled className={clsx({'text-blue-500': state.checked})}/></button>
|
||||||
onConfirm={onRemove}
|
{onRemove && <Popconfirm
|
||||||
okText="删除"
|
title="提示"
|
||||||
cancelText="取消"
|
description={<div style={{minWidth: 150}}><span>请确认删除此视频?</span></div>}
|
||||||
><button className="hover:text-blue-500"><MinusCircleFilled/></button></Popconfirm>}
|
onConfirm={onRemove}
|
||||||
</div>
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<button className="hover:text-blue-500"><MinusCircleFilled/></button>
|
||||||
|
</Popconfirm>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -32,7 +32,7 @@ const initialState: AuthProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 状态 reducer
|
// 状态 reducer
|
||||||
const authReducer = (prevState: AuthProps, {payload}:AuthAction ) => {
|
const authReducer = (prevState: AuthProps, {payload}: AuthAction) => {
|
||||||
return {
|
return {
|
||||||
...prevState,
|
...prevState,
|
||||||
...payload,
|
...payload,
|
||||||
@ -45,50 +45,43 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
|
|||||||
// MOCK INIT DATA
|
// MOCK INIT DATA
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
if (!token) {
|
if (token) {
|
||||||
dispatch({
|
const result = localStorage.getItem(AppConfig.AUTHED_PERSON_DATA_KEY)
|
||||||
payload: {
|
if (result) {
|
||||||
isInitialized: true,
|
const user = JSON.parse(result) as UserProfile
|
||||||
}
|
dispatch({
|
||||||
})
|
payload: {
|
||||||
return 'initialized'
|
isInitialized: true,
|
||||||
}
|
isLoggedIn: true,
|
||||||
getUserInfo().then(user => {
|
user,
|
||||||
dispatch({
|
token
|
||||||
action: 'init',
|
|
||||||
payload: {
|
|
||||||
isInitialized: true,
|
|
||||||
isLoggedIn: !!user,
|
|
||||||
user: {
|
|
||||||
...user,
|
|
||||||
role: getCurrentRole()
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
return 'initialized'
|
||||||
}).finally(() => {
|
}
|
||||||
dispatch({
|
}
|
||||||
payload: {
|
dispatch({
|
||||||
isInitialized: true,
|
payload: {
|
||||||
}
|
isInitialized: true,
|
||||||
})
|
user: null,
|
||||||
|
token: null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return 'initialized'
|
return 'initialized'
|
||||||
}
|
}
|
||||||
// 登录
|
// 登录
|
||||||
const login = async (code: string, state: string) => {
|
const login = async (code: string, state: string) => {
|
||||||
const user = await auth(code, state)
|
const ret = await auth(code, state)
|
||||||
// 保存token
|
// 保存token
|
||||||
setAuthToken(user.token, user.expiration_time ? (new Date(user.expiration_time)).getTime() : -1);
|
setAuthToken(ret.token, ret.user_info, -1);
|
||||||
|
|
||||||
//
|
//
|
||||||
dispatch({
|
dispatch({
|
||||||
action: 'login',
|
action: 'login',
|
||||||
payload: {
|
payload: {
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
user: {
|
token: ret.token,
|
||||||
...user,
|
user: ret.user_info
|
||||||
role: getCurrentRole()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
42
src/hooks/useArticleTags.ts
Normal file
42
src/hooks/useArticleTags.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import {useCallback, useEffect, useState} from "react";
|
||||||
|
import {getAllCategory} from "@/service/api/article.ts";
|
||||||
|
|
||||||
|
|
||||||
|
const ArticleTags: OptionItem[] = [];
|
||||||
|
export default function useArticleTags() {
|
||||||
|
const [tags, _setTags] = useState([]);
|
||||||
|
const setTags = useCallback(() => {
|
||||||
|
_setTags([
|
||||||
|
{
|
||||||
|
label: '全部',
|
||||||
|
value: -1,
|
||||||
|
},
|
||||||
|
...ArticleTags
|
||||||
|
])
|
||||||
|
}, [])
|
||||||
|
useEffect(() => {
|
||||||
|
if (ArticleTags.length === 0) {
|
||||||
|
getAllCategory().then(res => {
|
||||||
|
ArticleTags.length = 0;
|
||||||
|
res.tags.forEach(t => {
|
||||||
|
const item = {
|
||||||
|
label: t.tag_name,
|
||||||
|
value: t.tag_id,
|
||||||
|
children: t.sons && t.sons.length > 0 ? t.sons.map(s => ({
|
||||||
|
label: s.tag_name,
|
||||||
|
value: s.tag_id
|
||||||
|
})) : []
|
||||||
|
};
|
||||||
|
ArticleTags.push(item)
|
||||||
|
})
|
||||||
|
setTags()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
// 清除
|
||||||
|
setTags([])
|
||||||
|
ArticleTags.length = 0;
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
return tags
|
||||||
|
}
|
@ -13,14 +13,20 @@ const useAuth = () => {
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAuthToken = (token: string | null, expiry_time = -1) => {
|
|
||||||
|
const clearAuth = () => {
|
||||||
|
localStorage.removeItem(AppConfig.AUTH_TOKEN_KEY);
|
||||||
|
localStorage.removeItem(AppConfig.AUTHED_PERSON_DATA_KEY);
|
||||||
|
}
|
||||||
|
export const setAuthToken = (token: string | null,profileData:UserProfile, expiry_time = -1) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
localStorage.removeItem(AppConfig.AUTH_TOKEN_KEY);
|
clearAuth();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localStorage.setItem(AppConfig.AUTH_TOKEN_KEY, JSON.stringify({
|
localStorage.setItem(AppConfig.AUTH_TOKEN_KEY, JSON.stringify({
|
||||||
token, expiry_time
|
token, expiry_time
|
||||||
}));
|
}));
|
||||||
|
localStorage.setItem(AppConfig.AUTHED_PERSON_DATA_KEY, JSON.stringify(profileData));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAuthToken = () => {
|
export const getAuthToken = () => {
|
||||||
@ -29,7 +35,7 @@ export const getAuthToken = () => {
|
|||||||
try {
|
try {
|
||||||
const {token, expiry_time} = JSON.parse(data) as { token: string, expiry_time: number };
|
const {token, expiry_time} = JSON.parse(data) as { token: string, expiry_time: number };
|
||||||
if (expiry_time != -1 && expiry_time < Date.now()) {
|
if (expiry_time != -1 && expiry_time < Date.now()) {
|
||||||
localStorage.removeItem(AppConfig.AUTH_TOKEN_KEY);
|
clearAuth();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
|
@ -108,6 +108,7 @@ export default function CreateIndex() {
|
|||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
setEditNews({title:v.title, groups: [...ArticleGroupList]})
|
setEditNews({title:v.title, groups: [...ArticleGroupList]})
|
||||||
}}
|
}}
|
||||||
|
editable
|
||||||
/>))}
|
/>))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {Button, DatePicker, Form, Input, 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 {ListTimes} from "@/pages/news/components/news-source.ts";
|
||||||
|
|
||||||
type SearchParams = {
|
type SearchParams = {
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
@ -15,8 +17,9 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) {
|
|||||||
timeRange: string;
|
timeRange: string;
|
||||||
keywords: string;
|
keywords: string;
|
||||||
searching: boolean;
|
searching: boolean;
|
||||||
|
time: string;
|
||||||
}>({
|
}>({
|
||||||
keywords: "", searching: false, timeRange: ""
|
keywords: "", searching: false, timeRange: "", time: '-1'
|
||||||
})
|
})
|
||||||
const onFinish = (values: any) => {
|
const onFinish = (values: any) => {
|
||||||
setState({searching: true})
|
setState({searching: true})
|
||||||
@ -29,26 +32,39 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (<div className={'search-panel'}>
|
return (<div className={'search-panel'}>
|
||||||
<div className="flex justify-end items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="search-form">
|
<div className="search-form">
|
||||||
<Form className={""} layout="inline" onFinish={onFinish}>
|
<Form className={""} layout="inline" onFinish={onFinish}>
|
||||||
<Form.Item name="keywords">
|
<Form.Item name="keywords">
|
||||||
<Input className="w-[250px]" placeholder={'请输入搜索信息'}/>
|
<Input className="w-[200px]" placeholder={'请输入搜索信息'}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={'更新时间'} name="timeRange">
|
<Form.Item label={'更新时间'} name="date" className="w-[250px]">
|
||||||
<DatePicker.RangePicker />
|
<Select
|
||||||
</Form.Item>
|
defaultValue={state.time} options={ListTimes}
|
||||||
<Form.Item>
|
optionRender={(option) => (
|
||||||
<Space size={10}>
|
<div className="flex items-center">
|
||||||
<Button type={'primary'} htmlType={'submit'}>搜索</Button>
|
<span role="icon" className={`radio-icon`}></span>
|
||||||
<Button htmlType={'reset'}>重置</Button>
|
<span role="listitem" aria-label={String(option.label)}>{option.label}</span>
|
||||||
</Space>
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{/*<Form.Item label={'更新时间'} name="timeRange">*/}
|
||||||
|
{/* <DatePicker.RangePicker />*/}
|
||||||
|
{/*</Form.Item>*/}
|
||||||
|
{/*<Form.Item>*/}
|
||||||
|
{/* <Space size={10}>*/}
|
||||||
|
{/* <Button type={'primary'} htmlType={'submit'}>搜索</Button>*/}
|
||||||
|
{/* <Button htmlType={'reset'}>重置</Button>*/}
|
||||||
|
{/* </Space>*/}
|
||||||
|
{/*</Form.Item>*/}
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
<Space size={10}>
|
<Space size={10}>
|
||||||
<Button loading={state.searching} type={'primary'}
|
<Button
|
||||||
onClick={onBtnStartClick}>一键推流</Button>
|
loading={state.searching} type={'primary'}
|
||||||
|
onClick={onBtnStartClick} icon={<PlayCircleOutlined/>}
|
||||||
|
>一键推流</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
|
@ -10,20 +10,28 @@ type VideoItemProps = {
|
|||||||
onLive?: boolean;
|
onLive?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
|
onCheckedChange?: (checked:boolean) => void;
|
||||||
}
|
}
|
||||||
export default function VideoItem(props: VideoItemProps) {
|
export default function VideoItem(props: VideoItemProps) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
checked: false
|
checked: false
|
||||||
})
|
})
|
||||||
return <div className={'video-item bg-white p-2 rounded relative group'}>
|
const handleCheckedChange = (checked:boolean) => {
|
||||||
|
setState({checked})
|
||||||
|
if (props.onCheckedChange) {
|
||||||
|
props.onCheckedChange(checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={'video-item bg-gray-100 hover:drop-shadow-md rounded overflow-hidden relative group'}>
|
||||||
<div className={`controls absolute top-1 right-1 z-[2] p-1 rounded items-center gap-2 bg-white/80 ${state.checked?'flex':'hidden'} group-hover:flex`}>
|
<div className={`controls absolute top-1 right-1 z-[2] p-1 rounded items-center gap-2 bg-white/80 ${state.checked?'flex':'hidden'} group-hover:flex`}>
|
||||||
<span onClick={props.onRemove} className={'cursor-pointer text-blue-500 text-2xl cursor-pointer'}><IconDelete /></span>
|
<span onClick={props.onRemove} className={'cursor-pointer text-blue-500 text-2xl cursor-pointer'}><IconDelete /></span>
|
||||||
{!props.onLive && <Checkbox onChange={e=>setState({checked: e.target.checked})} />}
|
{!props.onLive && <Checkbox onChange={e=>handleCheckedChange(e.target.checked)} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="cover" onClick={props.onClick}>
|
<div className="cover" onClick={props.onClick}>
|
||||||
<Image className={'w-full rounded cursor-pointer'} preview={false} src={ImageCover}/>
|
<Image className={'w-full cursor-pointer'} preview={false} src={ImageCover}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm py-2 px-3">
|
||||||
<div className="title my-1 cursor-pointer" onClick={props.onClick}>把丰碑立在人民心中</div>
|
<div className="title my-1 cursor-pointer" onClick={props.onClick}>把丰碑立在人民心中</div>
|
||||||
<div className="info flex justify-between gap-2 text-sm">
|
<div className="info flex justify-between gap-2 text-sm">
|
||||||
<div className="video-time-info text-gray-500">
|
<div className="video-time-info text-gray-500">
|
||||||
@ -31,7 +39,7 @@ export default function VideoItem(props: VideoItemProps) {
|
|||||||
<span className="ml-1">16小时前</span>
|
<span className="ml-1">16小时前</span>
|
||||||
</div>
|
</div>
|
||||||
{props.onLive && <div className="live-info">
|
{props.onLive && <div className="live-info">
|
||||||
<Tag color="processing">已在直播间</Tag>
|
<Tag color="processing" className="mr-0">已在直播间</Tag>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,6 +9,7 @@ import VideoDetail from "@/pages/library/components/video-detail.tsx";
|
|||||||
export default function LibraryIndex() {
|
export default function LibraryIndex() {
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const [videoData,] = useState(getEmptyPageData<VideoInfo>())
|
const [videoData,] = useState(getEmptyPageData<VideoInfo>())
|
||||||
|
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||||
const handleRemove = (video: VideoInfo) => {
|
const handleRemove = (video: VideoInfo) => {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: '删除提示',
|
title: '删除提示',
|
||||||
@ -18,16 +19,29 @@ export default function LibraryIndex() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const handleLive = async () => {
|
||||||
|
if (checkedIdArray.length == 0) return;
|
||||||
|
modal.confirm({
|
||||||
|
title: '推流提示',
|
||||||
|
content: '是否确定一键推流选中新闻视频?',
|
||||||
|
onOk: () => {
|
||||||
|
console.log('OK');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
const [detailVideo, setDetailVideo] = useState<VideoInfo>()
|
const [detailVideo, setDetailVideo] = useState<VideoInfo>()
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<div className={'container py-20'}>
|
<div className={'container py-20'}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<div className="search-form-container mb-5">
|
<div className="search-form-container mb-5">
|
||||||
<SearchForm onSearch={async () => {
|
<SearchForm
|
||||||
}}/>
|
onSearch={async () => {
|
||||||
|
}}
|
||||||
|
onBtnStartClick={handleLive}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<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) => (
|
{videoData.list.map((it, idx) => (
|
||||||
<VideoItem
|
<VideoItem
|
||||||
@ -35,6 +49,11 @@ export default function LibraryIndex() {
|
|||||||
videoInfo={it}
|
videoInfo={it}
|
||||||
onRemove={() => handleRemove(it)}
|
onRemove={() => handleRemove(it)}
|
||||||
onClick={() => setDetailVideo(it)}
|
onClick={() => setDetailVideo(it)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setCheckedIdArray(idArray => {
|
||||||
|
return checked ? idArray.concat(it.id) : idArray.filter(id => id != it.id);
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -43,6 +62,6 @@ export default function LibraryIndex() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VideoDetail video={detailVideo} onClose={() => setDetailVideo(undefined)} />
|
<VideoDetail video={detailVideo} onClose={() => setDetailVideo(undefined)}/>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import React, {useRef, useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {Button, message, Modal} from "antd";
|
import {Button, message, Modal} from "antd";
|
||||||
import {SortableContext, arrayMove} from '@dnd-kit/sortable';
|
import {SortableContext, arrayMove} from '@dnd-kit/sortable';
|
||||||
import {DndContext} from "@dnd-kit/core";
|
import {DndContext} from "@dnd-kit/core";
|
||||||
@ -10,6 +10,7 @@ export default function LiveIndex() {
|
|||||||
const [videoData, setVideoData] = useState<VideoInfo[]>(MockVideoDataList)
|
const [videoData, setVideoData] = useState<VideoInfo[]>(MockVideoDataList)
|
||||||
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 processDeleteVideo = async (_idArray: number[]) => {
|
const processDeleteVideo = async (_idArray: number[]) => {
|
||||||
message.info('删除成功!!!' + _idArray.join(''));
|
message.info('删除成功!!!' + _idArray.join(''));
|
||||||
}
|
}
|
||||||
@ -20,6 +21,15 @@ export default function LiveIndex() {
|
|||||||
onOk: () => processDeleteVideo(checkedIdArray)
|
onOk: () => processDeleteVideo(checkedIdArray)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const handleConfirm = () => {
|
||||||
|
modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '是否采纳全部编辑操作?',
|
||||||
|
onOk: () => {
|
||||||
|
message.info('编辑成功!!!');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (<div className="container py-10 page-live">
|
return (<div className="container py-10 page-live">
|
||||||
@ -36,13 +46,18 @@ export default function LiveIndex() {
|
|||||||
<div className="video-list-container flex-1">
|
<div className="video-list-container flex-1">
|
||||||
<div className=" bg-white py-8 px-6 rounded py-1">
|
<div className=" bg-white py-8 px-6 rounded py-1">
|
||||||
<div className="live-control flex justify-between mb-8">
|
<div className="live-control flex justify-between mb-8">
|
||||||
<div className="flex gap-2">
|
{editable ?<>
|
||||||
<Button type="primary">开始直播</Button>
|
<div className="flex gap-2">
|
||||||
<Button>暂停直播</Button>
|
<Button type="primary" onClick={handleConfirm}>确定</Button>
|
||||||
</div>
|
<Button onClick={()=>setEditable(false)}>退出</Button>
|
||||||
<div>
|
</div>
|
||||||
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
<div>
|
||||||
</div>
|
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
||||||
|
</div>
|
||||||
|
</>: <div>
|
||||||
|
<Button type="primary" onClick={()=>setEditable(true)}>编辑</Button>
|
||||||
|
</div>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<DndContext onDragEnd={(e) => {
|
<DndContext onDragEnd={(e) => {
|
||||||
const {active, over} = e;
|
const {active, over} = e;
|
||||||
@ -80,6 +95,7 @@ export default function LiveIndex() {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onRemove={() => processDeleteVideo([v.id])}
|
onRemove={() => processDeleteVideo([v.id])}
|
||||||
|
editable={editable}
|
||||||
/>))}
|
/>))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import {Button, Input, Select, Table, TableColumnsType, TableProps} from "antd";
|
import {Button, Cascader, Input, Select, Table, TableColumnsType, TableProps, Typography} from "antd";
|
||||||
import {SearchOutlined} from "@ant-design/icons";
|
import {SearchOutlined} from "@ant-design/icons";
|
||||||
import {Card} from "@/components/card";
|
import {Card} from "@/components/card";
|
||||||
import React from "react";
|
import React, {useState} from "react";
|
||||||
import {NewsSources} from "@/pages/news/components/news-source.ts";
|
import {NewsSources} from "@/pages/news/components/news-source.ts";
|
||||||
import {useRequest, useSetState} 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 {ArticleGroupList} from "@/_local/mock-data.ts";
|
||||||
|
import useArticleTags from "@/hooks/useArticleTags.ts";
|
||||||
|
import {getArticleList} from "@/service/api/article.ts";
|
||||||
|
|
||||||
|
|
||||||
const dataList: NewsInfo[] = [
|
const dataList: NewsInfo[] = [
|
||||||
@ -36,17 +38,21 @@ const rowSelection: TableProps<NewsInfo>['rowSelection'] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function NewEdit() {
|
export default function NewEdit() {
|
||||||
const [editNews, setEditNews] = useSetState<{
|
const [editId, setEditId] = useState(-1)
|
||||||
title?: string;
|
const articleTags = useArticleTags()
|
||||||
groups?: ArticleContentGroup[];
|
|
||||||
}>({})
|
|
||||||
const [params, setParams] = useSetState({
|
const [params, setParams] = useSetState({
|
||||||
source: NewsSources.map(s => s.value),
|
source: NewsSources.map(s => s.value),
|
||||||
search: '',
|
search: '',
|
||||||
page: 1
|
page: 1,
|
||||||
|
limit: 10
|
||||||
})
|
})
|
||||||
const {data} = useRequest(async () => {
|
const {data} = useRequest(async () => {
|
||||||
return [...dataList]
|
return getArticleList({
|
||||||
|
pagination:{
|
||||||
|
page: params.page,
|
||||||
|
limit: params.limit
|
||||||
|
}
|
||||||
|
})
|
||||||
}, {
|
}, {
|
||||||
refreshDeps: [params]
|
refreshDeps: [params]
|
||||||
})
|
})
|
||||||
@ -75,34 +81,38 @@ export default function NewEdit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const columns: TableColumnsType<NewsInfo> = [
|
const columns: TableColumnsType<ListArticleItem> = [
|
||||||
{
|
{
|
||||||
title: '标题',
|
title: '标题',
|
||||||
|
minWidth:300,
|
||||||
dataIndex: 'title',
|
dataIndex: 'title',
|
||||||
// render: (text: string) => <a>{text}</a>,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '内容',
|
title: '内容',
|
||||||
dataIndex: 'content',
|
dataIndex: 'summary',
|
||||||
|
render: (value) => (<Typography.Paragraph style={{marginBottom:0}} ellipsis={{
|
||||||
|
rows: 2,expandable: true,symbol:'More'
|
||||||
|
}}>{value}</Typography.Paragraph>)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '来源',
|
title: '来源',
|
||||||
dataIndex: 'source',
|
minWidth:150,
|
||||||
|
dataIndex: 'media_name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '时间',
|
title: '时间',
|
||||||
|
width:150,
|
||||||
dataIndex: 'time',
|
dataIndex: 'time',
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
return formatTime(record.time, 'YYYY-MM-DD HH:mm')
|
return formatTime(record.publish_time, 'YYYY-MM-DD HH:mm')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
width:80,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (_, record) => (<Button type="link" onClick={() => {
|
render: (_, record) => (<Button type="link" onClick={() => {
|
||||||
setEditNews({
|
setEditId(record.id)
|
||||||
title: record.title, groups: [...ArticleGroupList]
|
|
||||||
})
|
|
||||||
}}>编辑</Button>),
|
}}>编辑</Button>),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -115,54 +125,64 @@ export default function NewEdit() {
|
|||||||
onPressEnter={(e) => {
|
onPressEnter={(e) => {
|
||||||
setParams({search: e.target.value})
|
setParams({search: e.target.value})
|
||||||
}}
|
}}
|
||||||
type="text" className="rounded px-3 w-[220px]"
|
type="text" className="rounded px-3 w-[250px]"
|
||||||
suffix={<SearchOutlined/>}
|
suffix={<SearchOutlined/>}
|
||||||
placeholder="请输入你先搜索的关键词"
|
placeholder="请输入你先搜索的关键词"
|
||||||
/>
|
/>
|
||||||
<span className="ml-5 text-sm">来源</span>
|
<span className="ml-5 text-sm">来源</span>
|
||||||
<Select
|
<Cascader
|
||||||
value={params.source}
|
options={articleTags}
|
||||||
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="请选择你要筛选的新闻"
|
placeholder="请选择你要筛选的新闻"
|
||||||
optionRender={(option) => (
|
className="w-[250px]"
|
||||||
<div className="flex items-center">
|
onChange={e=>{
|
||||||
<span role="icon" className={`checkbox-icon`}></span>
|
console.log('e.target.value',e)
|
||||||
<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>
|
|
||||||
}}
|
}}
|
||||||
|
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>
|
</div>
|
||||||
<Button type="primary" onClick={() => {
|
<Button type="primary" onClick={() => setEditId(0)}>手动新增</Button>
|
||||||
setEditNews({title: '', groups: []})
|
|
||||||
}}>手动新增</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="news-list-container mt-5">
|
<div className="news-list-container mt-5">
|
||||||
<Table<NewsInfo>
|
<Table<ListArticleItem>
|
||||||
rowSelection={{type: 'checkbox', ...rowSelection}}
|
rowSelection={{type: 'checkbox', ...rowSelection}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data as any}
|
dataSource={data?.list||[]}
|
||||||
rowKey={'id'}
|
rowKey={'id'}
|
||||||
|
bordered
|
||||||
pagination={{
|
pagination={{
|
||||||
position: ['bottomLeft'],
|
position: ['bottomLeft'],
|
||||||
simple: true,
|
simple: true,
|
||||||
defaultCurrent: 1,
|
defaultCurrent: params.page,
|
||||||
total: 5000004,
|
total: data?.pagination.total || 0,
|
||||||
pageSize: 20,
|
pageSize: params.limit,
|
||||||
showSizeChanger: false,
|
showSizeChanger: false,
|
||||||
rootClassName: 'simple-pagination',
|
rootClassName: 'simple-pagination',
|
||||||
onChange: (page) => setParams({page})
|
onChange: (page) => setParams({page})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ArticleEditModal title={editNews.title} groups={editNews.groups}/>
|
<ArticleEditModal id={editId} onClose={()=>setEditId(-1)} />
|
||||||
</Card>
|
</Card>
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
@ -33,7 +33,7 @@ export default function FormLogin() {
|
|||||||
navigate(params.get('from') || '/')
|
navigate(params.get('from') || '/')
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
setError(e.message)
|
setError(e.message)
|
||||||
}).finally(()=>setLoading(false));
|
}).finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (<div className="form">
|
return (<div className="form">
|
||||||
@ -54,7 +54,7 @@ export default function FormLogin() {
|
|||||||
<div
|
<div
|
||||||
className="border border-gray-300 rounded-3xl mt-2 flex items-center px-3 focus-within:border-blue-500 focus-within:shadow focus-within:shadow-blue-200">
|
className="border border-gray-300 rounded-3xl mt-2 flex items-center px-3 focus-within:border-blue-500 focus-within:shadow focus-within:shadow-blue-200">
|
||||||
<UserOutlined/>
|
<UserOutlined/>
|
||||||
<Input size={'large'} variant={'borderless'} placeholder="请输入账号" />
|
<Input size={'large'} variant={'borderless'} placeholder="请输入账号"/>
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="password">
|
<Form.Item name="password">
|
||||||
@ -83,8 +83,9 @@ export default function FormLogin() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button disabled={disabled || loading} loading={loading} type="primary" size={'large'} htmlType="submit" block shape={'round'}>
|
<Button disabled={disabled || loading} loading={loading} type="primary" size={'large'} htmlType="submit"
|
||||||
{login?'登录中':'立即登录'}
|
block shape={'round'}>
|
||||||
|
{login ? '登录中' : '立即登录'}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import FormLogin from "./components/form-login.tsx";
|
import FormLogin from "./components/form-login.tsx";
|
||||||
|
import useAuth from "@/hooks/useAuth.ts";
|
||||||
|
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||||
|
import {useEffect} from "react";
|
||||||
|
|
||||||
export default function UserIndex(){
|
export default function UserIndex(){
|
||||||
|
const {user} = useAuth();
|
||||||
|
const navigate = useNavigate() ;
|
||||||
|
const [param] = useSearchParams()
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
navigate(param.get('from') || '/')
|
||||||
|
}
|
||||||
|
}, [user])
|
||||||
return (<div className={styles.main}>
|
return (<div className={styles.main}>
|
||||||
<div className={styles.boxLogin}>
|
<div className={styles.boxLogin}>
|
||||||
<FormLogin />
|
<FormLogin />
|
||||||
|
@ -9,21 +9,22 @@ import {UserAvatar} from "@/components/icons/user-avatar.tsx";
|
|||||||
import {DashboardNavigation} from "@/routes/layout/dashboard-navigation.tsx";
|
import {DashboardNavigation} from "@/routes/layout/dashboard-navigation.tsx";
|
||||||
|
|
||||||
import useAuth from "@/hooks/useAuth.ts";
|
import useAuth from "@/hooks/useAuth.ts";
|
||||||
|
import {hidePhone} from "@/util/strings.ts";
|
||||||
|
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
const NavigationUserContainer = () => {
|
const NavigationUserContainer = () => {
|
||||||
const {logout} = useAuth()
|
const {logout,user} = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const items: MenuProps['items'] = [
|
const items: MenuProps['items'] = [
|
||||||
{
|
{
|
||||||
key: '1',
|
key: 'profile',
|
||||||
label: '个人中心',
|
label: '个人中心',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: 'logout',
|
||||||
label: <div onClick={()=>{
|
label: <div onClick={()=>{
|
||||||
logout().then(()=>navigate('/user'))
|
logout().then(()=>navigate('/user'))
|
||||||
}}>退出</div>,
|
}}>退出</div>,
|
||||||
@ -31,9 +32,9 @@ const NavigationUserContainer = () => {
|
|||||||
];
|
];
|
||||||
return (<div className={"flex items-center justify-between gap-2"}>
|
return (<div className={"flex items-center justify-between gap-2"}>
|
||||||
<Dropdown menu={{items}} placement="bottom" arrow>
|
<Dropdown menu={{items}} placement="bottom" arrow>
|
||||||
<div className="flex items-center hover:bg-gray-200 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"/>
|
||||||
<span className={"username ml-2 text-sm"}>180xxxx7788</span>
|
<span className={"username ml-2 text-sm"}>{hidePhone(user?.nickname)}</span>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>)
|
</div>)
|
||||||
|
31
src/service/api/article.ts
Normal file
31
src/service/api/article.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {post} from "@/service/request.ts";
|
||||||
|
|
||||||
|
export function getAllCategory() {
|
||||||
|
return post<{ tags: ArticleCategory[] }>({url: '/spider/tags'})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getArticleList(data: ApiArticleSearchParams & ApiRequestPageParams) {
|
||||||
|
return post<DataList<ListArticleItem>>({url: '/article/search', data})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 【本期不做】
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
export function deleteArticle(id: Id) {
|
||||||
|
throw new Error('Not implement')
|
||||||
|
return post<{ article: any }>({url: '/article/delete/' + id})
|
||||||
|
}
|
||||||
|
export function getArticleDetail(id: Id) {
|
||||||
|
return post<ArticleDetail>({url: '/article/detail/' + id})
|
||||||
|
}
|
||||||
|
export function saveArticle(title:string,content_group: BlockContent[][],id: number) {
|
||||||
|
return post<{ content: string }>({
|
||||||
|
url: '/spider/article',
|
||||||
|
data:{
|
||||||
|
title,
|
||||||
|
content_group,
|
||||||
|
id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
13
src/service/api/common.ts
Normal file
13
src/service/api/common.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {post} from "@/service/request.ts";
|
||||||
|
|
||||||
|
export function getOssPolicy(scene = 'workbench') {
|
||||||
|
return post<TOSSPolicy>({
|
||||||
|
data: {scene},
|
||||||
|
baseURL: '/api/v1/common/get_oss_policy',
|
||||||
|
url: `/api/v1/common/get_oss_policy`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadToOss(file: File, policy: TOSSPolicy) {
|
||||||
|
|
||||||
|
}
|
@ -1,27 +1,22 @@
|
|||||||
import {sleep} from "@/util/basic.ts";
|
import {post} from "@/service/request.ts";
|
||||||
|
|
||||||
const mockUser: UserProfile = {
|
|
||||||
id: 1,
|
|
||||||
token: '123',
|
|
||||||
email: 'admin@qq.com',
|
|
||||||
username: 'admin',
|
|
||||||
role: 'normal',
|
|
||||||
expiration_time: '2025-12-31 23:23:23',
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUserInfo() {
|
|
||||||
await sleep(500);
|
|
||||||
return mockUser;
|
|
||||||
// return get<UserProfile>('/userinfo')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用 sso 返回的code、state换取登录凭证或用户信息
|
* 登录并返回 token
|
||||||
* @param code
|
* @param code
|
||||||
* @param state
|
* @param phone
|
||||||
*/
|
*/
|
||||||
export async function auth(_username: string, _password: string) {
|
export async function auth(phone: string, code: string) {
|
||||||
await sleep(1500);
|
return post<{
|
||||||
return mockUser;
|
token: string;
|
||||||
//return post<UserProfile>('/auth', {code, state})
|
user_info: UserProfile
|
||||||
|
}>({
|
||||||
|
url: '/login',
|
||||||
|
data: {code, phone}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendSmsCode(phone: string) {
|
||||||
|
return post({
|
||||||
|
url:'/smscode', data:{phone}
|
||||||
|
})
|
||||||
}
|
}
|
@ -7,74 +7,86 @@ const JSON_FORMAT: string = 'application/json';
|
|||||||
const REQUEST_TIMEOUT = 300000; // 超时时长5min
|
const REQUEST_TIMEOUT = 300000; // 超时时长5min
|
||||||
|
|
||||||
const Axios = axios.create({
|
const Axios = axios.create({
|
||||||
baseURL: AppConfig.API_PREFIX || '/api',
|
timeout: REQUEST_TIMEOUT,
|
||||||
timeout: REQUEST_TIMEOUT,
|
headers: {'Content-Type': JSON_FORMAT}
|
||||||
headers: {'Content-Type': JSON_FORMAT}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 请求前拦截
|
// 请求前拦截
|
||||||
Axios.interceptors.request.use(config => {
|
Axios.interceptors.request.use(config => {
|
||||||
const token = getAuthToken();
|
const token = getAuthToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers['Token'] = `${token}`;
|
config.headers['Token'] = `${token}`;
|
||||||
}
|
}
|
||||||
if (config.data && config.data instanceof FormData) {
|
if (config.data && config.data instanceof FormData) {
|
||||||
config.headers['Content-Type'] = 'multipart/form-data';
|
config.headers['Content-Type'] = 'multipart/form-data';
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}, err => {
|
}, err => {
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
export function request<T>(url: string, method: RequestMethod, data: AllType = null, getOriginResult = false) {
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
export function request<T>(options: RequestOption) {
|
||||||
Axios.request<APIResponse<T>>({
|
return new Promise<T>((resolve, reject) => {
|
||||||
url,
|
const {url, method, data, baseURL, getOriginResult} = options;
|
||||||
method,
|
|
||||||
data,
|
Axios.request<APIResponse<T>>({
|
||||||
}).then(res => {
|
url,
|
||||||
if (res.status != 200) {
|
method: method || 'get',
|
||||||
reject(new BizError("Service Internal Exception,Please Try Later!", res.status))
|
data,
|
||||||
return;
|
baseURL: baseURL || AppConfig.API_PREFIX,
|
||||||
}
|
}).then(res => {
|
||||||
if (getOriginResult) {
|
if (res.status != 200) {
|
||||||
resolve(res.data as unknown as T)
|
reject(new BizError("Service Internal Exception,Please Try Later!", res.status))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// const
|
if (getOriginResult) {
|
||||||
const {code, message, data,request_id} = res.data
|
resolve(res.data as unknown as T)
|
||||||
if (code == 0) {
|
return;
|
||||||
resolve(data as unknown as T)
|
}
|
||||||
} else {
|
// const
|
||||||
reject(new BizError(message, code,request_id, data as unknown as AllType))
|
const {code, message, data, request_id} = res.data
|
||||||
}
|
if (code == 0) {
|
||||||
}).catch(e => {
|
resolve(data as unknown as T)
|
||||||
reject(new BizError(e.message, 500))
|
} else {
|
||||||
})
|
reject(new BizError(message, code, request_id, data as unknown as AllType))
|
||||||
})
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
reject(new BizError(e.message, 500))
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function post<T>(url: string, data: AllType = {}, returnOrigin = false) {
|
export function post<T>(params: RequestOption) {
|
||||||
return request<T>(url, 'post', data, returnOrigin)
|
return request<T>({
|
||||||
|
...params,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get<T>(url: string, data: AllType = null, returnOrigin = false) {
|
export function get<T>(params: RequestOption) {
|
||||||
if (data) {
|
if (params.data) {
|
||||||
url += (url.indexOf('?') === -1 ? '?' : '&') + stringify(data)
|
params.url += (params.url.indexOf('?') === -1 ? '?' : '&') + stringify(params.data)
|
||||||
}
|
}
|
||||||
return request<T>(url, 'get', data, returnOrigin)
|
return request<T>({
|
||||||
|
...params,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function put<T>(url: string, data: AllType = {}) {
|
export function put<T>(params: RequestOption) {
|
||||||
return request<T>(url, 'put', data)
|
return request<T>({
|
||||||
|
...params,
|
||||||
|
method: 'put'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFileBlob(url: string) {
|
export function getFileBlob(url: string) {
|
||||||
return new Promise<Blob>((resolve, reject) => {
|
return new Promise<Blob>((resolve, reject) => {
|
||||||
fetch(url).then(res => res.blob()).then(res => {
|
fetch(url).then(res => res.blob()).then(res => {
|
||||||
resolve(res)
|
resolve(res)
|
||||||
}).catch(reject);
|
}).catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -12,4 +12,4 @@ export class BizError extends Error {
|
|||||||
this.code = code;
|
this.code = code;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
77
src/types/api.d.ts
vendored
77
src/types/api.d.ts
vendored
@ -1,18 +1,38 @@
|
|||||||
// 请求方式
|
declare interface ApiRequestPageParams {
|
||||||
declare type RequestMethod = 'get' | 'post' | 'put' | 'delete'
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 接口返回数据类型
|
declare interface ApiArticleSearchParams {
|
||||||
declare interface APIResponse<T> {
|
// 1级标签id
|
||||||
/**
|
tag_level_1_id?: number;
|
||||||
* 错误码,0:成功,其他失败
|
// 2级标签id 没有则为0
|
||||||
*/
|
tag_level_2_id?: number;
|
||||||
code: number;
|
// 标题
|
||||||
data?: T;
|
title?: string;
|
||||||
/**
|
}
|
||||||
* 非0情况下,提示信息
|
|
||||||
*/
|
declare interface DataList<T> {
|
||||||
message: string;
|
pagination: {
|
||||||
request_id: string;
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
list: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseArticleCategory {
|
||||||
|
//标签id
|
||||||
|
tag_id: number;
|
||||||
|
//标签名称
|
||||||
|
tag_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArticleCategory extends BaseArticleCategory {
|
||||||
|
sons: BaseArticleCategory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface VideoInfo {
|
declare interface VideoInfo {
|
||||||
@ -26,5 +46,32 @@ declare interface VideoInfo {
|
|||||||
description: string;
|
description: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
create_time: number;
|
create_time: number;
|
||||||
checked?:boolean
|
checked?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BasicArticleInfo {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
publish_time: string;
|
||||||
|
media_name: string;
|
||||||
|
media_id: number;
|
||||||
|
fanwen_column_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文章
|
||||||
|
*/
|
||||||
|
declare interface ListArticleItem extends BasicArticleInfo {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 爬虫新闻
|
||||||
|
*/
|
||||||
|
declare interface ListCrawlerNewsItem extends BasicArticleInfo {
|
||||||
|
cover: string;
|
||||||
|
// 新闻来源
|
||||||
|
data_source_name: string;
|
||||||
|
// 内部文章关联id
|
||||||
|
internal_article_id: number;
|
||||||
|
}
|
38
src/types/auth.d.ts
vendored
38
src/types/auth.d.ts
vendored
@ -1,25 +1,29 @@
|
|||||||
declare type UserRole = string
|
declare type UserRole = string
|
||||||
declare type UserProfile = {
|
|
||||||
id: string | number;
|
declare interface UserProfile {
|
||||||
token: string;
|
id: number;
|
||||||
email: string;
|
phone: string;
|
||||||
username: string;
|
nickname: string;
|
||||||
role: UserRole;
|
realname: string;
|
||||||
expiration_time?:number | string
|
status: number;
|
||||||
|
ctime: number;
|
||||||
|
utime: number;
|
||||||
|
dtime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
declare interface AuthProps {
|
declare interface AuthProps {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
isInitialized?: boolean;
|
isInitialized?: boolean;
|
||||||
user?: UserProfile | null;
|
user?: UserProfile | null;
|
||||||
token?: string | null;
|
token?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare type AuthContextType = {
|
declare type AuthContextType = {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
isInitialized?: boolean;
|
isInitialized?: boolean;
|
||||||
user?: UserProfile | null | undefined;
|
user?: UserProfile | null | undefined;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
login: (username:string,password:string) => Promise<void>;
|
login: (phone: string, code: string) => Promise<void>;
|
||||||
updateUser: (user:Partial<UserProfile>) => Promise<void>;
|
updateUser: (user: Partial<UserProfile>) => Promise<void>;
|
||||||
};
|
};
|
29
src/types/core.d.ts
vendored
29
src/types/core.d.ts
vendored
@ -1,3 +1,5 @@
|
|||||||
|
declare type Id = number | string;
|
||||||
|
|
||||||
declare interface RecordList<T> {
|
declare interface RecordList<T> {
|
||||||
list: T[];
|
list: T[];
|
||||||
pagination: {
|
pagination: {
|
||||||
@ -10,22 +12,25 @@ declare interface RecordList<T> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
declare interface OptionItem {
|
declare interface OptionItem {
|
||||||
label: string;value: string;
|
label: string;
|
||||||
|
value: string|number;
|
||||||
children?: OptionItem[];
|
children?: OptionItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface BlockContent {
|
declare interface BlockContent {
|
||||||
type: 'text' | 'image';
|
type: 'text' | 'image';
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface ArticleContentGroup {
|
declare interface ArticleContentGroup {
|
||||||
groupId?: number;
|
groupId?: number;
|
||||||
blocks: BlockContent[];
|
blocks: BlockContent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface ArticleInfo {
|
declare interface ArticleDetail {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: ArticleContentGroup[]
|
content_group: BlockContent[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface NewsInfo {
|
declare interface NewsInfo {
|
||||||
@ -35,4 +40,22 @@ declare interface NewsInfo {
|
|||||||
content: string;
|
content: string;
|
||||||
source: string;
|
source: string;
|
||||||
time: string|number;
|
time: string|number;
|
||||||
|
}
|
||||||
|
declare interface TOSSPolicy {
|
||||||
|
//Oss access id
|
||||||
|
access_id: string;
|
||||||
|
//上传 host
|
||||||
|
host: string;
|
||||||
|
//Oss 上传 policy
|
||||||
|
policy: string;
|
||||||
|
//Oss 上传的鉴权签名
|
||||||
|
signature: string;
|
||||||
|
//该签名和policy有效期截至时间
|
||||||
|
expire: int;
|
||||||
|
//上传的 路径,在最后的 / 后面接上文件名称 文件名称 处理成 K8efE3imYn.docx,文件名长度16
|
||||||
|
dir: string;
|
||||||
|
//允许上传的文件最大大小,单位字节
|
||||||
|
max_size: int;
|
||||||
|
//允许上传的文件最大大小,单位字节
|
||||||
|
allow: string;
|
||||||
}
|
}
|
24
src/types/request.d.ts
vendored
Normal file
24
src/types/request.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 请求方式
|
||||||
|
declare type RequestMethod = 'get' | 'post' | 'put' | 'delete'
|
||||||
|
declare type RequestOption = {
|
||||||
|
url: string;
|
||||||
|
method?: RequestMethod;
|
||||||
|
data?: AllType | null;
|
||||||
|
getOriginResult?: boolean;
|
||||||
|
baseURL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接口返回数据类型
|
||||||
|
declare interface APIResponse<T> {
|
||||||
|
/**
|
||||||
|
* 错误码,0:成功,其他失败
|
||||||
|
*/
|
||||||
|
code: number;
|
||||||
|
/**
|
||||||
|
* 非0情况下,提示信息
|
||||||
|
*/
|
||||||
|
msg: string;
|
||||||
|
cost: string;
|
||||||
|
trace_id: string;
|
||||||
|
data?: T;
|
||||||
|
}
|
2
src/vite-env.d.ts
vendored
2
src/vite-env.d.ts
vendored
@ -8,6 +8,8 @@ declare const APP_SITE_URL: string;
|
|||||||
declare const AppConfig: {
|
declare const AppConfig: {
|
||||||
// 登录凭证 token key
|
// 登录凭证 token key
|
||||||
AUTH_TOKEN_KEY: string;
|
AUTH_TOKEN_KEY: string;
|
||||||
|
// 登录用户信息 key
|
||||||
|
AUTHED_PERSON_DATA_KEY: string;
|
||||||
API_PREFIX: string;
|
API_PREFIX: string;
|
||||||
};
|
};
|
||||||
declare const AppMode: 'test' | 'production' | 'development';
|
declare const AppMode: 'test' | 'production' | 'development';
|
||||||
|
@ -10,8 +10,9 @@ export default defineConfig(({mode}) => {
|
|||||||
define: {
|
define: {
|
||||||
AppConfig: JSON.stringify({
|
AppConfig: JSON.stringify({
|
||||||
SITE_URL: process.env.APP_SITE_URL || null,
|
SITE_URL: process.env.APP_SITE_URL || null,
|
||||||
API_PREFIX: process.env.APP_API_PREFIX || '/api',
|
API_PREFIX: process.env.APP_API_PREFIX || '/mgmt/v1/metahuman',
|
||||||
AUTH_TOKEN_KEY: process.env.AUTH_TOKEN_KEY || 'ai-chat-token',
|
AUTH_TOKEN_KEY: process.env.AUTH_TOKEN_KEY || 'digital-person-token',
|
||||||
|
AUTHED_PERSON_DATA_KEY: process.env.AUTHED_PERSON_DATA_KEY || 'digital-person-user-info',
|
||||||
}),
|
}),
|
||||||
AppMode: JSON.stringify(mode)
|
AppMode: JSON.stringify(mode)
|
||||||
},
|
},
|
||||||
@ -30,11 +31,11 @@ export default defineConfig(({mode}) => {
|
|||||||
server: {
|
server: {
|
||||||
port: 10021,
|
port: 10021,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/query': {
|
'/mgmt': {
|
||||||
target: 'http://101.132.145.21:606', //
|
target: 'http://192.168.0.231:9999', //\
|
||||||
changeOrigin: true,
|
// changeOrigin: true,
|
||||||
ws: true,
|
// ws: true,
|
||||||
//rewrite: (path) => path.replace(/^\/api/, '')
|
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user