feat: 开始集成及与API联调
This commit is contained in:
parent
ef5da31ea5
commit
ca074f59b5
@ -9,6 +9,7 @@ import {Button, Popconfirm} from "antd";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
index?:number;
|
||||
className?: string;
|
||||
blocks: BlockContent[];
|
||||
editable?: boolean;
|
||||
@ -17,7 +18,7 @@ type Props = {
|
||||
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) => {
|
||||
// 删除当前项
|
||||
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')}>
|
||||
{
|
||||
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}/>
|
||||
: <BlockImage data={it} editable={editable}/>
|
||||
}
|
||||
|
@ -2,42 +2,56 @@ import {Input, Modal} from "antd";
|
||||
import ArticleGroup from "@/components/article/group.tsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useSetState} from "ahooks";
|
||||
import {getArticleDetail} from "@/service/api/article.ts";
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
groups?: ArticleContentGroup[];
|
||||
onSave?: () => Promise<void>;
|
||||
id?: number;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function ArticleEditModal(props: Props) {
|
||||
|
||||
const [groups, setGroups] = useState<ArticleContentGroup[]>([]);
|
||||
const [title, setTitle] = useState(props.title)
|
||||
const [groups, setGroups] = useState<BlockContent[][]>([]);
|
||||
const [title, setTitle] = useState('')
|
||||
|
||||
const [state, setState] = useSetState({
|
||||
loading: false,
|
||||
open: false
|
||||
})
|
||||
const handleSave = () => {
|
||||
setState({loading: true})
|
||||
props.onSave?.().finally(() => {
|
||||
setState({loading: false,open: false})
|
||||
})
|
||||
props.onClose?.()
|
||||
// if (props.onSave) {
|
||||
// setState({loading: true})
|
||||
// props.onSave?.().then(() => {
|
||||
// setState({loading: false, open: false})
|
||||
// })
|
||||
// } else {
|
||||
// console.log(groups)
|
||||
// }
|
||||
}
|
||||
useEffect(() => {
|
||||
setState({open: typeof(props.title) != "undefined"})
|
||||
setGroups(props.groups || [])
|
||||
setTitle(props.title||'')
|
||||
}, [props.title,props.groups])
|
||||
if(props.id){
|
||||
if(props.id > 0){
|
||||
getArticleDetail(props.id).then(res => {
|
||||
setGroups(res.content_group)
|
||||
setTitle(res.title)
|
||||
})
|
||||
}else{
|
||||
setGroups([])
|
||||
setTitle('')
|
||||
}
|
||||
}
|
||||
}, [props.id])
|
||||
|
||||
return (<Modal
|
||||
title={'编辑文章'}
|
||||
open={state.open}
|
||||
open={props.id >= 0}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
width={800}
|
||||
onCancel={()=>setState({open: false})}
|
||||
onCancel={props.onClose}
|
||||
okButtonProps={{loading: state.loading}}
|
||||
coOk={handleSave}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<div className="article-title mt-5">
|
||||
<div className="title">
|
||||
|
@ -5,9 +5,9 @@ import ArticleBlock from "@/components/article/block.tsx";
|
||||
import styles from './article.module.scss'
|
||||
|
||||
type Props = {
|
||||
groups: ArticleContentGroup[];
|
||||
groups: BlockContent[][];
|
||||
editable?: boolean;
|
||||
onChange?: (groups: ArticleContentGroup[]) => void;
|
||||
onChange?: (groups: BlockContent[][]) => void;
|
||||
}
|
||||
export default function ArticleGroup({groups, editable, onChange}: Props) {
|
||||
/**
|
||||
@ -15,7 +15,7 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
|
||||
* @param insertIndex 插入的位置,-1表示插入到末尾
|
||||
*/
|
||||
const handleAddGroup = ( insertIndex: number = -1) => {
|
||||
const newGroup: ArticleContentGroup = {blocks: []}
|
||||
const newGroup: BlockContent[] = []
|
||||
const _groups = [...groups]
|
||||
if (insertIndex == -1 || insertIndex >= groups.length) { // -1或者越界表示新增
|
||||
_groups.push(newGroup)
|
||||
@ -27,11 +27,12 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
|
||||
return <div className={styles.group}>
|
||||
{groups.map((g, index) => (
|
||||
<ArticleBlock
|
||||
editable={editable} key={index} blocks={g.blocks}
|
||||
editable={editable} key={index} blocks={g}
|
||||
onChange={(blocks) => {
|
||||
groups[index].blocks = blocks
|
||||
groups[index] = blocks
|
||||
onChange?.([...groups])
|
||||
}}
|
||||
index={index}
|
||||
onAdd={() => {
|
||||
handleAddGroup?.(index + 1)
|
||||
}}
|
||||
@ -45,6 +46,6 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
|
||||
/>
|
||||
))}
|
||||
{groups.length == 0 && editable &&
|
||||
<ArticleBlock editable onChange={blocks => onChange?.([{blocks}])} blocks={[]}/>}
|
||||
<ArticleBlock editable onChange={blocks => onChange?.([blocks])} blocks={[]}/>}
|
||||
</div>
|
||||
}
|
@ -1,24 +1,28 @@
|
||||
import React from "react";
|
||||
import styles from './article.module.scss'
|
||||
import React, {useMemo, useRef, useState} from "react";
|
||||
import {Button, Input, Upload} from "antd";
|
||||
import {TextAreaRef} from "antd/es/input/TextArea";
|
||||
|
||||
import styles from './article.module.scss'
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
data: BlockContent;
|
||||
editable?: boolean;
|
||||
groupIndex?: number;
|
||||
blockIndex?: number;
|
||||
onChange?: (data: BlockContent) => void;
|
||||
}
|
||||
|
||||
export function BlockImage({data,editable}: Props) {
|
||||
export function BlockImage({data, editable}: Props) {
|
||||
return <div className={styles.image}>
|
||||
{editable ? <div>
|
||||
<Upload accept="image/*">
|
||||
<div className={styles.uploadImage} >
|
||||
{ data.content ? <>
|
||||
<div className={styles.uploadImage}>
|
||||
{data.content ? <>
|
||||
<img src={data.content}/>
|
||||
<div className={styles.uploadTips}>
|
||||
<span>编辑</span>
|
||||
<span>更换图片</span>
|
||||
</div>
|
||||
</> : <div className={styles.imagePlaceholder}>
|
||||
<Button>选择图片</Button>
|
||||
@ -29,14 +33,65 @@ export function BlockImage({data,editable}: Props) {
|
||||
</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}>
|
||||
{editable ? <div>
|
||||
{/*<textarea className={styles.textarea} defaultValue={data.content}/>*/}
|
||||
<Input.TextArea onChange={e => {
|
||||
onChange?.({type:'text',content:e.target.value})
|
||||
// console.log(e)
|
||||
}} placeholder={'请输入文本'} value={data.content} autoSize={{minRows:3}} variant={"borderless"} />
|
||||
</div>: <p className="p-2">{data.content}</p>}
|
||||
{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
|
||||
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>
|
||||
}
|
@ -5,6 +5,7 @@ import {useState} from "react";
|
||||
|
||||
import InputContainer from "@/components/form/input-container.tsx";
|
||||
import {clsx} from "clsx";
|
||||
import {sendSmsCode} from "@/service/api/user.ts";
|
||||
|
||||
type Props = {
|
||||
onChange?: (code: string) => void;
|
||||
@ -23,10 +24,11 @@ export function useSmsCode() {
|
||||
const sendCode = (phone?:string,interval = 60) => {
|
||||
if (countdown > 0 || sending || !phone) return;
|
||||
setSending(true)
|
||||
setTimeout(() => {
|
||||
sendSmsCode(phone).then(()=>{
|
||||
setTargetDate(Date.now() + interval * 1000)
|
||||
}).finally(()=>{
|
||||
setSending(false)
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
return {
|
||||
sendCode,
|
||||
|
@ -1,10 +1,19 @@
|
||||
import Avatar from "@/assets/images/avatar.png";
|
||||
// import Avatar from "@/assets/images/avatar.png";
|
||||
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 (
|
||||
<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>
|
||||
)
|
||||
}
|
@ -9,6 +9,8 @@ import {Popconfirm} from "antd";
|
||||
|
||||
type Props = {
|
||||
video: VideoInfo,
|
||||
editable?: boolean;
|
||||
sortable?: boolean;
|
||||
index?: number;
|
||||
checked?: boolean;
|
||||
active?: boolean;
|
||||
@ -16,17 +18,22 @@ type Props = {
|
||||
onPlay?: () => void;
|
||||
onEdit?: () => 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 {
|
||||
attributes, listeners,
|
||||
setNodeRef, transform
|
||||
} = useSortable({resizeObserverConfig: {}, id})
|
||||
|
||||
|
||||
const [state, setState] = useSetState<{checked?:boolean}>({})
|
||||
const [state, setState] = useSetState<{ checked?: boolean }>({})
|
||||
useEffect(() => {
|
||||
setState({checked})
|
||||
}, [checked])
|
||||
@ -38,34 +45,41 @@ export const VideoListItem = ({index,id, video, onPlay, onRemove, checked, onChe
|
||||
<div
|
||||
className="index-value w-[40px] h-[40px] flex items-center justify-center bg-gray-100 rounded-3xl">{id}</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-item-cover'}>
|
||||
<img className="w-[100px] rounded-md" src={video.cover} alt={video.title}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="operation flex items-center ml-2 gap-3 text-lg text-gray-400">
|
||||
<button className="hover:text-blue-500" {...attributes} {...listeners}>
|
||||
<MenuOutlined/>
|
||||
</button>
|
||||
{onPlay && <button className="hover:text-blue-500" onClick={onPlay} style={{fontSize: '1.3em'}}><IconPlay/>
|
||||
</button>}
|
||||
{onEdit && <button className="hover:text-blue-500" onClick={onEdit} style={{fontSize: '1.1em'}}><IconEdit/>
|
||||
</button>}
|
||||
<button className="hover:text-blue-300" onClick={() => {
|
||||
if (onCheckedChange) {
|
||||
onCheckedChange(!state.checked)
|
||||
} else {
|
||||
setState({checked: !state.checked})
|
||||
}
|
||||
}}><CheckCircleFilled className={clsx({'text-blue-500': state.checked})}/></button>
|
||||
{onRemove && <Popconfirm
|
||||
title="提示"
|
||||
description={<div style={{minWidth: 150}}><span>请确认删除此视频?</span></div>}
|
||||
onConfirm={onRemove}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
><button className="hover:text-blue-500"><MinusCircleFilled/></button></Popconfirm>}
|
||||
</div>
|
||||
{editable &&
|
||||
<div className="operation flex items-center ml-2 gap-3 text-lg text-gray-400">
|
||||
{!active ? <button className="hover:text-blue-500 cursor-move" {...attributes} {...listeners}>
|
||||
<MenuOutlined/>
|
||||
</button> : <button disabled className="cursor-not-allowed"><MenuOutlined/></button>}
|
||||
{onPlay &&
|
||||
<button className="hover:text-blue-500" onClick={onPlay} style={{fontSize: '1.3em'}}><IconPlay/>
|
||||
</button>}
|
||||
{onEdit &&
|
||||
<button className="hover:text-blue-500" onClick={onEdit} style={{fontSize: '1.1em'}}><IconEdit/>
|
||||
</button>}
|
||||
<button className="hover:text-blue-300" onClick={() => {
|
||||
if (onCheckedChange) {
|
||||
onCheckedChange(!state.checked)
|
||||
} else {
|
||||
setState({checked: !state.checked})
|
||||
}
|
||||
}}><CheckCircleFilled className={clsx({'text-blue-500': state.checked})}/></button>
|
||||
{onRemove && <Popconfirm
|
||||
title="提示"
|
||||
description={<div style={{minWidth: 150}}><span>请确认删除此视频?</span></div>}
|
||||
onConfirm={onRemove}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<button className="hover:text-blue-500"><MinusCircleFilled/></button>
|
||||
</Popconfirm>}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
@ -32,7 +32,7 @@ const initialState: AuthProps = {
|
||||
};
|
||||
|
||||
// 状态 reducer
|
||||
const authReducer = (prevState: AuthProps, {payload}:AuthAction ) => {
|
||||
const authReducer = (prevState: AuthProps, {payload}: AuthAction) => {
|
||||
return {
|
||||
...prevState,
|
||||
...payload,
|
||||
@ -45,50 +45,43 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
|
||||
// MOCK INIT DATA
|
||||
const init = async () => {
|
||||
const token = getAuthToken();
|
||||
if (!token) {
|
||||
dispatch({
|
||||
payload: {
|
||||
isInitialized: true,
|
||||
}
|
||||
})
|
||||
return 'initialized'
|
||||
}
|
||||
getUserInfo().then(user => {
|
||||
dispatch({
|
||||
action: 'init',
|
||||
payload: {
|
||||
isInitialized: true,
|
||||
isLoggedIn: !!user,
|
||||
user: {
|
||||
...user,
|
||||
role: getCurrentRole()
|
||||
if (token) {
|
||||
const result = localStorage.getItem(AppConfig.AUTHED_PERSON_DATA_KEY)
|
||||
if (result) {
|
||||
const user = JSON.parse(result) as UserProfile
|
||||
dispatch({
|
||||
payload: {
|
||||
isInitialized: true,
|
||||
isLoggedIn: true,
|
||||
user,
|
||||
token
|
||||
}
|
||||
}
|
||||
})
|
||||
}).finally(() => {
|
||||
dispatch({
|
||||
payload: {
|
||||
isInitialized: true,
|
||||
}
|
||||
})
|
||||
})
|
||||
return 'initialized'
|
||||
}
|
||||
}
|
||||
dispatch({
|
||||
payload: {
|
||||
isInitialized: true,
|
||||
user: null,
|
||||
token: null
|
||||
}
|
||||
})
|
||||
return 'initialized'
|
||||
}
|
||||
// 登录
|
||||
const login = async (code: string, state: string) => {
|
||||
const user = await auth(code, state)
|
||||
const ret = await auth(code, state)
|
||||
// 保存token
|
||||
setAuthToken(user.token, user.expiration_time ? (new Date(user.expiration_time)).getTime() : -1);
|
||||
setAuthToken(ret.token, ret.user_info, -1);
|
||||
|
||||
//
|
||||
dispatch({
|
||||
action: 'login',
|
||||
payload: {
|
||||
isLoggedIn: true,
|
||||
user: {
|
||||
...user,
|
||||
role: getCurrentRole()
|
||||
}
|
||||
token: ret.token,
|
||||
user: ret.user_info
|
||||
}
|
||||
})
|
||||
}
|
||||
|
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;
|
||||
};
|
||||
|
||||
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) {
|
||||
localStorage.removeItem(AppConfig.AUTH_TOKEN_KEY);
|
||||
clearAuth();
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(AppConfig.AUTH_TOKEN_KEY, JSON.stringify({
|
||||
token, expiry_time
|
||||
}));
|
||||
localStorage.setItem(AppConfig.AUTHED_PERSON_DATA_KEY, JSON.stringify(profileData));
|
||||
}
|
||||
|
||||
export const getAuthToken = () => {
|
||||
@ -29,7 +35,7 @@ export const getAuthToken = () => {
|
||||
try {
|
||||
const {token, expiry_time} = JSON.parse(data) as { token: string, expiry_time: number };
|
||||
if (expiry_time != -1 && expiry_time < Date.now()) {
|
||||
localStorage.removeItem(AppConfig.AUTH_TOKEN_KEY);
|
||||
clearAuth();
|
||||
return;
|
||||
}
|
||||
return token;
|
||||
|
@ -108,6 +108,7 @@ export default function CreateIndex() {
|
||||
onEdit={() => {
|
||||
setEditNews({title:v.title, groups: [...ArticleGroupList]})
|
||||
}}
|
||||
editable
|
||||
/>))}
|
||||
</SortableContext>
|
||||
</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 {PlayCircleOutlined} from "@ant-design/icons";
|
||||
import {ListTimes} from "@/pages/news/components/news-source.ts";
|
||||
|
||||
type SearchParams = {
|
||||
keywords?: string;
|
||||
@ -15,8 +17,9 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) {
|
||||
timeRange: string;
|
||||
keywords: string;
|
||||
searching: boolean;
|
||||
time: string;
|
||||
}>({
|
||||
keywords: "", searching: false, timeRange: ""
|
||||
keywords: "", searching: false, timeRange: "", time: '-1'
|
||||
})
|
||||
const onFinish = (values: any) => {
|
||||
setState({searching: true})
|
||||
@ -29,26 +32,39 @@ export default function SearchForm({onSearch, onBtnStartClick}: Props) {
|
||||
}
|
||||
|
||||
return (<div className={'search-panel'}>
|
||||
<div className="flex justify-end items-center">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="search-form">
|
||||
<Form className={""} layout="inline" onFinish={onFinish}>
|
||||
<Form.Item name="keywords">
|
||||
<Input className="w-[250px]" placeholder={'请输入搜索信息'}/>
|
||||
<Input className="w-[200px]" placeholder={'请输入搜索信息'}/>
|
||||
</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 label={'更新时间'} name="date" className="w-[250px]">
|
||||
<Select
|
||||
defaultValue={state.time} options={ListTimes}
|
||||
optionRender={(option) => (
|
||||
<div className="flex items-center">
|
||||
<span role="icon" className={`radio-icon`}></span>
|
||||
<span role="listitem" aria-label={String(option.label)}>{option.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
<Space size={10}>
|
||||
<Button loading={state.searching} type={'primary'}
|
||||
onClick={onBtnStartClick}>一键推流</Button>
|
||||
<Button
|
||||
loading={state.searching} type={'primary'}
|
||||
onClick={onBtnStartClick} icon={<PlayCircleOutlined/>}
|
||||
>一键推流</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>)
|
||||
|
@ -10,20 +10,28 @@ type VideoItemProps = {
|
||||
onLive?: boolean;
|
||||
onClick?: () => void;
|
||||
onRemove?: () => void;
|
||||
onCheckedChange?: (checked:boolean) => void;
|
||||
}
|
||||
export default function VideoItem(props: VideoItemProps) {
|
||||
const [state, setState] = useState({
|
||||
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`}>
|
||||
<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 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 className="text-sm">
|
||||
<div className="text-sm py-2 px-3">
|
||||
<div className="title my-1 cursor-pointer" onClick={props.onClick}>把丰碑立在人民心中</div>
|
||||
<div className="info flex justify-between gap-2 text-sm">
|
||||
<div className="video-time-info text-gray-500">
|
||||
@ -31,7 +39,7 @@ export default function VideoItem(props: VideoItemProps) {
|
||||
<span className="ml-1">16小时前</span>
|
||||
</div>
|
||||
{props.onLive && <div className="live-info">
|
||||
<Tag color="processing">已在直播间</Tag>
|
||||
<Tag color="processing" className="mr-0">已在直播间</Tag>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,6 +9,7 @@ import VideoDetail from "@/pages/library/components/video-detail.tsx";
|
||||
export default function LibraryIndex() {
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
const [videoData,] = useState(getEmptyPageData<VideoInfo>())
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
const handleRemove = (video: VideoInfo) => {
|
||||
modal.confirm({
|
||||
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>()
|
||||
|
||||
return (<>
|
||||
<div className={'container py-20'}>
|
||||
{contextHolder}
|
||||
<div className="search-form-container mb-5">
|
||||
<SearchForm onSearch={async () => {
|
||||
}}/>
|
||||
<SearchForm
|
||||
onSearch={async () => {
|
||||
}}
|
||||
onBtnStartClick={handleLive}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="bg-white rounded p-5">
|
||||
<div className={'video-list-container grid gap-5 grid-cols-4'}>
|
||||
{videoData.list.map((it, idx) => (
|
||||
<VideoItem
|
||||
@ -35,6 +49,11 @@ export default function LibraryIndex() {
|
||||
videoInfo={it}
|
||||
onRemove={() => handleRemove(it)}
|
||||
onClick={() => setDetailVideo(it)}
|
||||
onCheckedChange={(checked) => {
|
||||
setCheckedIdArray(idArray => {
|
||||
return checked ? idArray.concat(it.id) : idArray.filter(id => id != it.id);
|
||||
})
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -43,6 +62,6 @@ export default function LibraryIndex() {
|
||||
</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 {SortableContext, arrayMove} from '@dnd-kit/sortable';
|
||||
import {DndContext} from "@dnd-kit/core";
|
||||
@ -10,6 +10,7 @@ export default function LiveIndex() {
|
||||
const [videoData, setVideoData] = useState<VideoInfo[]>(MockVideoDataList)
|
||||
const [modal, contextHolder] = Modal.useModal()
|
||||
const [checkedIdArray, setCheckedIdArray] = useState<number[]>([])
|
||||
const [editable,setEditable] = useState<boolean>(false)
|
||||
const processDeleteVideo = async (_idArray: number[]) => {
|
||||
message.info('删除成功!!!' + _idArray.join(''));
|
||||
}
|
||||
@ -20,6 +21,15 @@ export default function LiveIndex() {
|
||||
onOk: () => processDeleteVideo(checkedIdArray)
|
||||
})
|
||||
}
|
||||
const handleConfirm = () => {
|
||||
modal.confirm({
|
||||
title: '提示',
|
||||
content: '是否采纳全部编辑操作?',
|
||||
onOk: () => {
|
||||
message.info('编辑成功!!!');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
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=" bg-white py-8 px-6 rounded py-1">
|
||||
<div className="live-control flex justify-between mb-8">
|
||||
<div className="flex gap-2">
|
||||
<Button type="primary">开始直播</Button>
|
||||
<Button>暂停直播</Button>
|
||||
</div>
|
||||
<div>
|
||||
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
||||
</div>
|
||||
{editable ?<>
|
||||
<div className="flex gap-2">
|
||||
<Button type="primary" onClick={handleConfirm}>确定</Button>
|
||||
<Button onClick={()=>setEditable(false)}>退出</Button>
|
||||
</div>
|
||||
<div>
|
||||
<span className="cursor-pointer" onClick={handleDeleteBatch}>批量删除</span>
|
||||
</div>
|
||||
</>: <div>
|
||||
<Button type="primary" onClick={()=>setEditable(true)}>编辑</Button>
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
<DndContext onDragEnd={(e) => {
|
||||
const {active, over} = e;
|
||||
@ -80,6 +95,7 @@ export default function LiveIndex() {
|
||||
})
|
||||
}}
|
||||
onRemove={() => processDeleteVideo([v.id])}
|
||||
editable={editable}
|
||||
/>))}
|
||||
</SortableContext>
|
||||
</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 {Card} from "@/components/card";
|
||||
import React from "react";
|
||||
import React, {useState} from "react";
|
||||
import {NewsSources} from "@/pages/news/components/news-source.ts";
|
||||
import {useRequest, useSetState} from "ahooks";
|
||||
import {formatTime} from "@/util/strings.ts";
|
||||
import ArticleEditModal from "@/components/article/edit-modal.tsx";
|
||||
import {ArticleGroupList} from "@/_local/mock-data.ts";
|
||||
import useArticleTags from "@/hooks/useArticleTags.ts";
|
||||
import {getArticleList} from "@/service/api/article.ts";
|
||||
|
||||
|
||||
const dataList: NewsInfo[] = [
|
||||
@ -36,17 +38,21 @@ const rowSelection: TableProps<NewsInfo>['rowSelection'] = {
|
||||
};
|
||||
|
||||
export default function NewEdit() {
|
||||
const [editNews, setEditNews] = useSetState<{
|
||||
title?: string;
|
||||
groups?: ArticleContentGroup[];
|
||||
}>({})
|
||||
const [editId, setEditId] = useState(-1)
|
||||
const articleTags = useArticleTags()
|
||||
const [params, setParams] = useSetState({
|
||||
source: NewsSources.map(s => s.value),
|
||||
search: '',
|
||||
page: 1
|
||||
page: 1,
|
||||
limit: 10
|
||||
})
|
||||
const {data} = useRequest(async () => {
|
||||
return [...dataList]
|
||||
return getArticleList({
|
||||
pagination:{
|
||||
page: params.page,
|
||||
limit: params.limit
|
||||
}
|
||||
})
|
||||
}, {
|
||||
refreshDeps: [params]
|
||||
})
|
||||
@ -75,34 +81,38 @@ export default function NewEdit() {
|
||||
}
|
||||
|
||||
|
||||
const columns: TableColumnsType<NewsInfo> = [
|
||||
const columns: TableColumnsType<ListArticleItem> = [
|
||||
{
|
||||
title: '标题',
|
||||
minWidth:300,
|
||||
dataIndex: 'title',
|
||||
// render: (text: string) => <a>{text}</a>,
|
||||
},
|
||||
{
|
||||
title: '内容',
|
||||
dataIndex: 'content',
|
||||
dataIndex: 'summary',
|
||||
render: (value) => (<Typography.Paragraph style={{marginBottom:0}} ellipsis={{
|
||||
rows: 2,expandable: true,symbol:'More'
|
||||
}}>{value}</Typography.Paragraph>)
|
||||
},
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'source',
|
||||
minWidth:150,
|
||||
dataIndex: 'media_name',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
width:150,
|
||||
dataIndex: 'time',
|
||||
render: (_, record) => {
|
||||
return formatTime(record.time, 'YYYY-MM-DD HH:mm')
|
||||
return formatTime(record.publish_time, 'YYYY-MM-DD HH:mm')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width:80,
|
||||
align: 'center',
|
||||
render: (_, record) => (<Button type="link" onClick={() => {
|
||||
setEditNews({
|
||||
title: record.title, groups: [...ArticleGroupList]
|
||||
})
|
||||
setEditId(record.id)
|
||||
}}>编辑</Button>),
|
||||
},
|
||||
];
|
||||
@ -115,54 +125,64 @@ export default function NewEdit() {
|
||||
onPressEnter={(e) => {
|
||||
setParams({search: e.target.value})
|
||||
}}
|
||||
type="text" className="rounded px-3 w-[220px]"
|
||||
type="text" className="rounded px-3 w-[250px]"
|
||||
suffix={<SearchOutlined/>}
|
||||
placeholder="请输入你先搜索的关键词"
|
||||
/>
|
||||
<span className="ml-5 text-sm">来源</span>
|
||||
<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}
|
||||
<Cascader
|
||||
options={articleTags}
|
||||
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>
|
||||
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={() => {
|
||||
setEditNews({title: '', groups: []})
|
||||
}}>手动新增</Button>
|
||||
<Button type="primary" onClick={() => setEditId(0)}>手动新增</Button>
|
||||
</div>
|
||||
<div className="news-list-container mt-5">
|
||||
<Table<NewsInfo>
|
||||
<Table<ListArticleItem>
|
||||
rowSelection={{type: 'checkbox', ...rowSelection}}
|
||||
columns={columns}
|
||||
dataSource={data as any}
|
||||
dataSource={data?.list||[]}
|
||||
rowKey={'id'}
|
||||
bordered
|
||||
pagination={{
|
||||
position: ['bottomLeft'],
|
||||
simple: true,
|
||||
defaultCurrent: 1,
|
||||
total: 5000004,
|
||||
pageSize: 20,
|
||||
defaultCurrent: params.page,
|
||||
total: data?.pagination.total || 0,
|
||||
pageSize: params.limit,
|
||||
showSizeChanger: false,
|
||||
rootClassName: 'simple-pagination',
|
||||
onChange: (page) => setParams({page})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ArticleEditModal title={editNews.title} groups={editNews.groups}/>
|
||||
<ArticleEditModal id={editId} onClose={()=>setEditId(-1)} />
|
||||
</Card>
|
||||
</div>)
|
||||
}
|
@ -33,7 +33,7 @@ export default function FormLogin() {
|
||||
navigate(params.get('from') || '/')
|
||||
}).catch(e => {
|
||||
setError(e.message)
|
||||
}).finally(()=>setLoading(false));
|
||||
}).finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
return (<div className="form">
|
||||
@ -54,7 +54,7 @@ export default function FormLogin() {
|
||||
<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">
|
||||
<UserOutlined/>
|
||||
<Input size={'large'} variant={'borderless'} placeholder="请输入账号" />
|
||||
<Input size={'large'} variant={'borderless'} placeholder="请输入账号"/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item name="password">
|
||||
@ -83,8 +83,9 @@ export default function FormLogin() {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button disabled={disabled || loading} loading={loading} type="primary" size={'large'} htmlType="submit" block shape={'round'}>
|
||||
{login?'登录中':'立即登录'}
|
||||
<Button disabled={disabled || loading} loading={loading} type="primary" size={'large'} htmlType="submit"
|
||||
block shape={'round'}>
|
||||
{login ? '登录中' : '立即登录'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
@ -1,7 +1,18 @@
|
||||
import styles from './style.module.scss'
|
||||
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(){
|
||||
const {user} = useAuth();
|
||||
const navigate = useNavigate() ;
|
||||
const [param] = useSearchParams()
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate(param.get('from') || '/')
|
||||
}
|
||||
}, [user])
|
||||
return (<div className={styles.main}>
|
||||
<div className={styles.boxLogin}>
|
||||
<FormLogin />
|
||||
|
@ -9,21 +9,22 @@ import {UserAvatar} from "@/components/icons/user-avatar.tsx";
|
||||
import {DashboardNavigation} from "@/routes/layout/dashboard-navigation.tsx";
|
||||
|
||||
import useAuth from "@/hooks/useAuth.ts";
|
||||
import {hidePhone} from "@/util/strings.ts";
|
||||
|
||||
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
const NavigationUserContainer = () => {
|
||||
const {logout} = useAuth()
|
||||
const {logout,user} = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
key: 'profile',
|
||||
label: '个人中心',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
key: 'logout',
|
||||
label: <div onClick={()=>{
|
||||
logout().then(()=>navigate('/user'))
|
||||
}}>退出</div>,
|
||||
@ -31,9 +32,9 @@ const NavigationUserContainer = () => {
|
||||
];
|
||||
return (<div className={"flex items-center justify-between gap-2"}>
|
||||
<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"/>
|
||||
<span className={"username ml-2 text-sm"}>180xxxx7788</span>
|
||||
<span className={"username ml-2 text-sm"}>{hidePhone(user?.nickname)}</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</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";
|
||||
|
||||
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')
|
||||
}
|
||||
import {post} from "@/service/request.ts";
|
||||
|
||||
/**
|
||||
* 使用 sso 返回的code、state换取登录凭证或用户信息
|
||||
* 登录并返回 token
|
||||
* @param code
|
||||
* @param state
|
||||
* @param phone
|
||||
*/
|
||||
export async function auth(_username: string, _password: string) {
|
||||
await sleep(1500);
|
||||
return mockUser;
|
||||
//return post<UserProfile>('/auth', {code, state})
|
||||
export async function auth(phone: string, code: string) {
|
||||
return post<{
|
||||
token: string;
|
||||
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 Axios = axios.create({
|
||||
baseURL: AppConfig.API_PREFIX || '/api',
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
headers: {'Content-Type': JSON_FORMAT}
|
||||
timeout: REQUEST_TIMEOUT,
|
||||
headers: {'Content-Type': JSON_FORMAT}
|
||||
})
|
||||
|
||||
|
||||
// 请求前拦截
|
||||
Axios.interceptors.request.use(config => {
|
||||
const token = getAuthToken();
|
||||
if (token) {
|
||||
config.headers['Token'] = `${token}`;
|
||||
}
|
||||
if (config.data && config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data';
|
||||
}
|
||||
return config
|
||||
const token = getAuthToken();
|
||||
if (token) {
|
||||
config.headers['Token'] = `${token}`;
|
||||
}
|
||||
if (config.data && config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data';
|
||||
}
|
||||
return config
|
||||
}, 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) => {
|
||||
Axios.request<APIResponse<T>>({
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
}).then(res => {
|
||||
if (res.status != 200) {
|
||||
reject(new BizError("Service Internal Exception,Please Try Later!", res.status))
|
||||
return;
|
||||
}
|
||||
if (getOriginResult) {
|
||||
resolve(res.data as unknown as T)
|
||||
return;
|
||||
}
|
||||
// const
|
||||
const {code, message, data,request_id} = res.data
|
||||
if (code == 0) {
|
||||
resolve(data as unknown as T)
|
||||
} else {
|
||||
reject(new BizError(message, code,request_id, data as unknown as AllType))
|
||||
}
|
||||
}).catch(e => {
|
||||
reject(new BizError(e.message, 500))
|
||||
})
|
||||
})
|
||||
|
||||
export function request<T>(options: RequestOption) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const {url, method, data, baseURL, getOriginResult} = options;
|
||||
|
||||
Axios.request<APIResponse<T>>({
|
||||
url,
|
||||
method: method || 'get',
|
||||
data,
|
||||
baseURL: baseURL || AppConfig.API_PREFIX,
|
||||
}).then(res => {
|
||||
if (res.status != 200) {
|
||||
reject(new BizError("Service Internal Exception,Please Try Later!", res.status))
|
||||
return;
|
||||
}
|
||||
if (getOriginResult) {
|
||||
resolve(res.data as unknown as T)
|
||||
return;
|
||||
}
|
||||
// const
|
||||
const {code, message, data, request_id} = res.data
|
||||
if (code == 0) {
|
||||
resolve(data as unknown as T)
|
||||
} 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) {
|
||||
return request<T>(url, 'post', data, returnOrigin)
|
||||
export function post<T>(params: RequestOption) {
|
||||
return request<T>({
|
||||
...params,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function get<T>(url: string, data: AllType = null, returnOrigin = false) {
|
||||
if (data) {
|
||||
url += (url.indexOf('?') === -1 ? '?' : '&') + stringify(data)
|
||||
}
|
||||
return request<T>(url, 'get', data, returnOrigin)
|
||||
export function get<T>(params: RequestOption) {
|
||||
if (params.data) {
|
||||
params.url += (params.url.indexOf('?') === -1 ? '?' : '&') + stringify(params.data)
|
||||
}
|
||||
return request<T>({
|
||||
...params,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function put<T>(url: string, data: AllType = {}) {
|
||||
return request<T>(url, 'put', data)
|
||||
export function put<T>(params: RequestOption) {
|
||||
return request<T>({
|
||||
...params,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function getFileBlob(url: string) {
|
||||
return new Promise<Blob>((resolve, reject) => {
|
||||
fetch(url).then(res => res.blob()).then(res => {
|
||||
resolve(res)
|
||||
}).catch(reject);
|
||||
});
|
||||
return new Promise<Blob>((resolve, reject) => {
|
||||
fetch(url).then(res => res.blob()).then(res => {
|
||||
resolve(res)
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
@ -12,4 +12,4 @@ export class BizError extends Error {
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
}
|
77
src/types/api.d.ts
vendored
77
src/types/api.d.ts
vendored
@ -1,18 +1,38 @@
|
||||
// 请求方式
|
||||
declare type RequestMethod = 'get' | 'post' | 'put' | 'delete'
|
||||
declare interface ApiRequestPageParams {
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
}
|
||||
|
||||
// 接口返回数据类型
|
||||
declare interface APIResponse<T> {
|
||||
/**
|
||||
* 错误码,0:成功,其他失败
|
||||
*/
|
||||
code: number;
|
||||
data?: T;
|
||||
/**
|
||||
* 非0情况下,提示信息
|
||||
*/
|
||||
message: string;
|
||||
request_id: string;
|
||||
declare interface ApiArticleSearchParams {
|
||||
// 1级标签id
|
||||
tag_level_1_id?: number;
|
||||
// 2级标签id 没有则为0
|
||||
tag_level_2_id?: number;
|
||||
// 标题
|
||||
title?: string;
|
||||
}
|
||||
|
||||
declare interface DataList<T> {
|
||||
pagination: {
|
||||
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 {
|
||||
@ -26,5 +46,32 @@ declare interface VideoInfo {
|
||||
description: string;
|
||||
tags: string[];
|
||||
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 UserProfile = {
|
||||
id: string | number;
|
||||
token: string;
|
||||
email: string;
|
||||
username: string;
|
||||
role: UserRole;
|
||||
expiration_time?:number | string
|
||||
|
||||
declare interface UserProfile {
|
||||
id: number;
|
||||
phone: string;
|
||||
nickname: string;
|
||||
realname: string;
|
||||
status: number;
|
||||
ctime: number;
|
||||
utime: number;
|
||||
dtime: number;
|
||||
}
|
||||
|
||||
|
||||
declare interface AuthProps {
|
||||
isLoggedIn: boolean;
|
||||
isInitialized?: boolean;
|
||||
user?: UserProfile | null;
|
||||
token?: string | null;
|
||||
isLoggedIn: boolean;
|
||||
isInitialized?: boolean;
|
||||
user?: UserProfile | null;
|
||||
token?: string | null;
|
||||
}
|
||||
|
||||
declare type AuthContextType = {
|
||||
isLoggedIn: boolean;
|
||||
isInitialized?: boolean;
|
||||
user?: UserProfile | null | undefined;
|
||||
logout: () => Promise<void>;
|
||||
login: (username:string,password:string) => Promise<void>;
|
||||
updateUser: (user:Partial<UserProfile>) => Promise<void>;
|
||||
isLoggedIn: boolean;
|
||||
isInitialized?: boolean;
|
||||
user?: UserProfile | null | undefined;
|
||||
logout: () => Promise<void>;
|
||||
login: (phone: string, code: string) => 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> {
|
||||
list: T[];
|
||||
pagination: {
|
||||
@ -10,22 +12,25 @@ declare interface RecordList<T> {
|
||||
};
|
||||
}
|
||||
declare interface OptionItem {
|
||||
label: string;value: string;
|
||||
label: string;
|
||||
value: string|number;
|
||||
children?: OptionItem[];
|
||||
}
|
||||
|
||||
declare interface BlockContent {
|
||||
type: 'text' | 'image';
|
||||
content: string;
|
||||
}
|
||||
|
||||
declare interface ArticleContentGroup {
|
||||
groupId?: number;
|
||||
blocks: BlockContent[];
|
||||
}
|
||||
|
||||
declare interface ArticleInfo {
|
||||
declare interface ArticleDetail {
|
||||
id: number;
|
||||
title: string;
|
||||
content: ArticleContentGroup[]
|
||||
content_group: BlockContent[][]
|
||||
}
|
||||
|
||||
declare interface NewsInfo {
|
||||
@ -35,4 +40,22 @@ declare interface NewsInfo {
|
||||
content: string;
|
||||
source: string;
|
||||
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: {
|
||||
// 登录凭证 token key
|
||||
AUTH_TOKEN_KEY: string;
|
||||
// 登录用户信息 key
|
||||
AUTHED_PERSON_DATA_KEY: string;
|
||||
API_PREFIX: string;
|
||||
};
|
||||
declare const AppMode: 'test' | 'production' | 'development';
|
||||
|
@ -10,8 +10,9 @@ export default defineConfig(({mode}) => {
|
||||
define: {
|
||||
AppConfig: JSON.stringify({
|
||||
SITE_URL: process.env.APP_SITE_URL || null,
|
||||
API_PREFIX: process.env.APP_API_PREFIX || '/api',
|
||||
AUTH_TOKEN_KEY: process.env.AUTH_TOKEN_KEY || 'ai-chat-token',
|
||||
API_PREFIX: process.env.APP_API_PREFIX || '/mgmt/v1/metahuman',
|
||||
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)
|
||||
},
|
||||
@ -30,11 +31,11 @@ export default defineConfig(({mode}) => {
|
||||
server: {
|
||||
port: 10021,
|
||||
proxy: {
|
||||
'/query': {
|
||||
target: 'http://101.132.145.21:606', //
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
//rewrite: (path) => path.replace(/^\/api/, '')
|
||||
'/mgmt': {
|
||||
target: 'http://192.168.0.231:9999', //\
|
||||
// changeOrigin: true,
|
||||
// ws: true,
|
||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user