diff --git a/src/components/article/group.tsx b/src/components/article/group.tsx
index bef3b63..04f8e2f 100644
--- a/src/components/article/group.tsx
+++ b/src/components/article/group.tsx
@@ -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
}
\ No newline at end of file
diff --git a/src/components/article/item.tsx b/src/components/article/item.tsx
index c28f701..2b62dd7 100644
--- a/src/components/article/item.tsx
+++ b/src/components/article/item.tsx
@@ -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
{editable ?
-
- { data.content ? <>
+
+ {data.content ? <>
- 编辑
+ 更换图片
> :
@@ -29,14 +33,65 @@ export function BlockImage({data,editable}: Props) {
}
-export function BlockText({data,editable,onChange}: Props) {
+export function BlockText({data, editable, onChange, groupIndex,blockIndex}: Props) {
+ const inputRef = useRef
(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
- {editable ?
- {/*
:
{data.content}
}
+ {editable ?
+ {/*
*/}
+
{
+ onChange?.({type: 'text', content: e.target.value})
+ }}
+ placeholder={'请输入文本'} onBlur={handleTextBlur} value={data.content} autoSize={{minRows: 3}}
+ variant={"borderless"}/>
+ {groupIndex == 0 && blockIndex == 0 &&
+ {
+ 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 {sentence}{index == 0 ? '(本句由数字人播报)' : ''}
+ })}
+
}
+ {/*{firstSentence}*/}
+ {/**/}
+ :
{data.content}
}
}
\ No newline at end of file
diff --git a/src/components/form/sms-code.tsx b/src/components/form/sms-code.tsx
index 61da70f..a97230f 100644
--- a/src/components/form/sms-code.tsx
+++ b/src/components/form/sms-code.tsx
@@ -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,
diff --git a/src/components/icons/user-avatar.tsx b/src/components/icons/user-avatar.tsx
index 8664d09..8737308 100644
--- a/src/components/icons/user-avatar.tsx
+++ b/src/components/icons/user-avatar.tsx
@@ -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 (
-
+ {/*
*/}
+
)
}
\ No newline at end of file
diff --git a/src/components/video/video-list-item.tsx b/src/components/video/video-list-item.tsx
index 0cc1dc4..02f4573 100644
--- a/src/components/video/video-list-item.tsx
+++ b/src/components/video/video-list-item.tsx
@@ -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
{id}
}
-
+
{video.id} - {video.title}
-
-
- {onPlay &&
}
- {onEdit &&
}
-
- {onRemove &&
请确认删除此视频?}
- onConfirm={onRemove}
- okText="删除"
- cancelText="取消"
- >
}
-
+ {editable &&
+
+ {!active ?
:
}
+ {onPlay &&
+
}
+ {onEdit &&
+
}
+
+ {onRemove &&
请确认删除此视频?}
+ onConfirm={onRemove}
+ okText="删除"
+ cancelText="取消"
+ >
+
+ }
+
+ }
}
\ No newline at end of file
diff --git a/src/contexts/auth/index.tsx b/src/contexts/auth/index.tsx
index 7349902..6c78c56 100644
--- a/src/contexts/auth/index.tsx
+++ b/src/contexts/auth/index.tsx
@@ -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
}
})
}
diff --git a/src/hooks/useArticleTags.ts b/src/hooks/useArticleTags.ts
new file mode 100644
index 0000000..7a19020
--- /dev/null
+++ b/src/hooks/useArticleTags.ts
@@ -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
+}
\ No newline at end of file
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts
index b3a7313..ba9cc0b 100644
--- a/src/hooks/useAuth.ts
+++ b/src/hooks/useAuth.ts
@@ -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;
diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx
index f70f32d..b4771ec 100644
--- a/src/pages/create/index.tsx
+++ b/src/pages/create/index.tsx
@@ -108,6 +108,7 @@ export default function CreateIndex() {
onEdit={() => {
setEditNews({title:v.title, groups: [...ArticleGroupList]})
}}
+ editable
/>))}
diff --git a/src/pages/library/components/search-form.tsx b/src/pages/library/components/search-form.tsx
index adb9e3d..2fbb21f 100644
--- a/src/pages/library/components/search-form.tsx
+++ b/src/pages/library/components/search-form.tsx
@@ -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 (
-
)
diff --git a/src/pages/library/components/video-item.tsx b/src/pages/library/components/video-item.tsx
index 57733da..a9bf568 100644
--- a/src/pages/library/components/video-item.tsx
+++ b/src/pages/library/components/video-item.tsx
@@ -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
+ const handleCheckedChange = (checked:boolean) => {
+ setState({checked})
+ if (props.onCheckedChange) {
+ props.onCheckedChange(checked)
+ }
+ }
+
+ return
- {!props.onLive && setState({checked: e.target.checked})} />}
+ {!props.onLive && handleCheckedChange(e.target.checked)} />}
-
+
-
+
把丰碑立在人民心中
@@ -31,7 +39,7 @@ export default function VideoItem(props: VideoItemProps) {
16小时前
{props.onLive &&
- 已在直播间
+ 已在直播间
}
diff --git a/src/pages/library/index.tsx b/src/pages/library/index.tsx
index ed2e6a1..dfc8ae6 100644
--- a/src/pages/library/index.tsx
+++ b/src/pages/library/index.tsx
@@ -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
())
+ const [checkedIdArray, setCheckedIdArray] = useState([])
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()
return (<>
{contextHolder}
- {
- }}/>
+ {
+ }}
+ onBtnStartClick={handleLive}
+ />
-
+
{videoData.list.map((it, idx) => (
handleRemove(it)}
onClick={() => setDetailVideo(it)}
+ onCheckedChange={(checked) => {
+ setCheckedIdArray(idArray => {
+ return checked ? idArray.concat(it.id) : idArray.filter(id => id != it.id);
+ })
+ }}
/>
))}
@@ -43,6 +62,6 @@ export default function LibraryIndex() {
- setDetailVideo(undefined)} />
+ setDetailVideo(undefined)}/>
>)
}
\ No newline at end of file
diff --git a/src/pages/live/index.tsx b/src/pages/live/index.tsx
index 9df3eaf..1dd7edb 100644
--- a/src/pages/live/index.tsx
+++ b/src/pages/live/index.tsx
@@ -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(MockVideoDataList)
const [modal, contextHolder] = Modal.useModal()
const [checkedIdArray, setCheckedIdArray] = useState([])
+ const [editable,setEditable] = useState(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 (
@@ -36,13 +46,18 @@ export default function LiveIndex() {
-
-
-
-
-
- 批量删除
-
+ {editable ?<>
+
+
+
+
+
+ 批量删除
+
+ >:
+
+
}
+
{
const {active, over} = e;
@@ -80,6 +95,7 @@ export default function LiveIndex() {
})
}}
onRemove={() => processDeleteVideo([v.id])}
+ editable={editable}
/>))}
diff --git a/src/pages/news/edit.tsx b/src/pages/news/edit.tsx
index 47064f8..5a367cc 100644
--- a/src/pages/news/edit.tsx
+++ b/src/pages/news/edit.tsx
@@ -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
['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 = [
+ const columns: TableColumnsType = [
{
title: '标题',
+ minWidth:300,
dataIndex: 'title',
- // render: (text: string) => {text},
},
{
title: '内容',
- dataIndex: 'content',
+ dataIndex: 'summary',
+ render: (value) => ({value})
},
{
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) => (),
},
];
@@ -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={}
placeholder="请输入你先搜索的关键词"
/>
来源
-
-
+
-
+
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})
}}
/>
-
+ setEditId(-1)} />
)
}
\ No newline at end of file
diff --git a/src/pages/user/components/form-login.tsx b/src/pages/user/components/form-login.tsx
index 9498462..a94f52f 100644
--- a/src/pages/user/components/form-login.tsx
+++ b/src/pages/user/components/form-login.tsx
@@ -33,7 +33,7 @@ export default function FormLogin() {
navigate(params.get('from') || '/')
}).catch(e => {
setError(e.message)
- }).finally(()=>setLoading(false));
+ }).finally(() => setLoading(false));
};
return (
@@ -54,7 +54,7 @@ export default function FormLogin() {
-
+
@@ -83,8 +83,9 @@ export default function FormLogin() {
-
diff --git a/src/pages/user/index.tsx b/src/pages/user/index.tsx
index 11ddc55..3722d62 100644
--- a/src/pages/user/index.tsx
+++ b/src/pages/user/index.tsx
@@ -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 (
diff --git a/src/routes/layout/dashboard-layout.tsx b/src/routes/layout/dashboard-layout.tsx
index 40a1f53..7f50d6a 100644
--- a/src/routes/layout/dashboard-layout.tsx
+++ b/src/routes/layout/dashboard-layout.tsx
@@ -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:
{
logout().then(()=>navigate('/user'))
}}>退出
,
@@ -31,9 +32,9 @@ const NavigationUserContainer = () => {
];
return (
-
+
- 180xxxx7788
+ {hidePhone(user?.nickname)}
)
diff --git a/src/service/api/article.ts b/src/service/api/article.ts
new file mode 100644
index 0000000..85cd21a
--- /dev/null
+++ b/src/service/api/article.ts
@@ -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