feat: 开始集成及与API联调

This commit is contained in:
LittleBoy 2024-12-12 09:40:44 +08:00
parent ef5da31ea5
commit ca074f59b5
31 changed files with 686 additions and 304 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -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 codestate换取登录凭证或用户信息 * 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}
})
} }

View File

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

View File

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

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

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

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

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

View File

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