From 3b6aea6ae61ef9cad04fb4f619bf0b4b383ae64e Mon Sep 17 00:00:00 2001 From: Suyi Date: Wed, 5 Jan 2022 19:06:54 +0800 Subject: [PATCH 1/4] feat: update basic layout --- src/component/Brand/index.tsx | 27 ++++++----- src/component/RightContent/index.tsx | 67 ++++++++++++++++++++++++++++ src/layout.tsx | 63 +++++++++++++------------- src/layout/index.tsx | 24 ++++++---- typings.d.ts | 63 +++++++++++++++++++------- 5 files changed, 179 insertions(+), 65 deletions(-) create mode 100644 src/component/RightContent/index.tsx diff --git a/src/component/Brand/index.tsx b/src/component/Brand/index.tsx index 0f4409e..f0e2723 100644 --- a/src/component/Brand/index.tsx +++ b/src/component/Brand/index.tsx @@ -1,18 +1,25 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { RouteContext } from '@ant-design/pro-layout'; + import * as styles from './index.less'; -const Brand: React.FC = ({ logo, title, description }) => ( -
- logo - {/*

{title}

*/} -

{description}

-
-); +const Brand: React.FC = ({ logo, title, description }) => { + const { collapsed, isMobile } = useContext(RouteContext); + + return ( +
+ logo + {collapsed || isMobile ? null : ( +

{description}

+ )} +
+ ); +}; export default Brand; interface Props { - title: string; - description: string; logo?: string; + title?: string; + description: string; } diff --git a/src/component/RightContent/index.tsx b/src/component/RightContent/index.tsx new file mode 100644 index 0000000..a0f6fb4 --- /dev/null +++ b/src/component/RightContent/index.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { history, useModel, Link } from 'umi'; +import { Avatar, Space, Button, Badge, Menu, Dropdown } from 'antd'; + +const RightContent: React.FC = (props) => { + const { user, logout } = useModel('user'); + const { count } = useModel('message'); + + if (!user) { + return ( +
+ +
+ ); + } + + const { loginname, avatar_url } = user; + + const menu = ( + + + 个人资料 + + + + 未读消息 + + + + + { + e.preventDefault(); + logout(); + }} + > + 退出登录 + + + + ); + + return ( +
+ + + + + {loginname} + + + +
+ ); +}; + +export default RightContent; + +interface Props {} diff --git a/src/layout.tsx b/src/layout.tsx index abf0812..09eccb5 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -10,36 +10,37 @@ import { import config from '../config/basic'; import Brand from './component/Brand'; - -const RightContent: React.FC<{ - user?: UserModel; -}> = (props) => { - const user = props?.user; - - if (!user) { - return ( -
- -
- ); - } - - const { loginname, avatar_url } = user; - return ( -
- - - -
- ); -}; +import RightContent from './component/RightContent'; + +// const RightContent: React.FC<{ +// user?: UserModel; +// }> = (props) => { +// const user = props?.user; + +// if (!user) { +// return ( +//
+// +//
+// ); +// } + +// const { loginname, avatar_url } = user; +// return ( +//
+// +// +// +//
+// ); +// }; const layoutConfig = ({ initialState, @@ -90,7 +91,7 @@ const layoutConfig = ({ item.path && {item.name}, rightContentRender: () => { - return ; + return ; }, footerRender: () => ( diff --git a/src/layout/index.tsx b/src/layout/index.tsx index 50eb1d5..f182db0 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -30,6 +30,11 @@ const getCurrentRoute = (route: IRoute, path: string): IRoute | undefined => { return target; }; +const BREADCRUMB_NAME_MAP = { + user: '用户', + topic: '话题', +}; + const Layout: React.FC> = (props) => { const { route, location } = props; const currentRoute = getCurrentRoute(route, location.pathname); @@ -38,14 +43,13 @@ const Layout: React.FC> = (props) => { title: currentRoute?.title || currentRoute?.name, }; - const topicDetailRegx = /\/topic\/([a-f0-9]){24}/g; - const userDetailRegx = /\/user\/(.*)/g; + const detailRegx = /\/(topic|user)\/(.*)/g; + + if (location.pathname.match(detailRegx)) { + const paths = location.pathname.split('/'); - if ( - location.pathname.match(topicDetailRegx) || - location.pathname.match(userDetailRegx) - ) { - const currentBreadcrumbName = location.pathname.split('/').pop(); + const id = paths.pop(); + const category = paths.pop(); headerConfig = { title: null, @@ -58,9 +62,13 @@ const Layout: React.FC> = (props) => { path: '/', breadcrumbName: '主页', }, + { + path: '/', + breadcrumbName: BREADCRUMB_NAME_MAP[category as 'user' | 'topic'], + }, { path: location.pathname, - breadcrumbName: currentBreadcrumbName, + breadcrumbName: id, }, ], }, diff --git a/typings.d.ts b/typings.d.ts index 04838a2..4dd7ef9 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -13,17 +13,6 @@ interface Window { initialState: InitialState; } -interface InitialState { - user?: UserModel; - token?: string; -} - -interface UserModel { - id: string; - loginname: string; - avatar_url: string; -} - interface QiankunApp { name: string; type: string; @@ -34,17 +23,59 @@ interface QiankunApp { locale?: string; } -interface ReplyModel { +interface InitialState { + user?: UserModel; + token?: string; +} + +interface UserModel extends AuthorModel { + id: string; + // loginname: string; + // avatar_url: string; +} + +interface TopicModel { id: string; + author_id: string; + + tab: string; content: string; + title: string; + last_reply_at: Date; + good: boolean; + top: boolean; + reply_count: number; + visit_count: number; + create_at: Date; - author: { - loginname: string; - avatar_url: string; - }; + author: AuthorModel; + replies: ReplyModel[]; +} +interface ReplyModel { + id: string; + author: AuthorModel; + + content: string; ups: string[]; create_at: Date; reply_id?: string; is_uped: boolean; } + +interface AuthorModel { + id?: string; + loginname: string; + avatar_url: string; +} + +interface MessageModel { + id: string; + type: string; + has_read: boolean; + create_at: Date; + + author: AuthorModel; + topic: TopicModel; + reply: ReplyModel; +} From 40ec36dead1e39ec67a98a67936d5b9e0aacf3cb Mon Sep 17 00:00:00 2001 From: Suyi Date: Wed, 5 Jan 2022 19:07:31 +0800 Subject: [PATCH 2/4] fix: reset form after submit --- src/page/topic/component/CommentForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/page/topic/component/CommentForm.tsx b/src/page/topic/component/CommentForm.tsx index e32a4e4..13853be 100644 --- a/src/page/topic/component/CommentForm.tsx +++ b/src/page/topic/component/CommentForm.tsx @@ -28,6 +28,7 @@ const CommentForm: React.FC = (props) => { } await onSubmit(value); + setValue(''); }} > {onSubmitText} From 21efa64f0960f23b34b8a063b91ebc5b521b9619 Mon Sep 17 00:00:00 2001 From: Suyi Date: Wed, 5 Jan 2022 19:08:42 +0800 Subject: [PATCH 3/4] feat: add message page --- config/routes.ts | 10 +++- src/access.ts | 1 + src/component/MessageList/index.less | 10 ++++ src/component/MessageList/index.tsx | 80 ++++++++++++++++++++++++++++ src/constants/index.ts | 15 ++++-- src/model/message.ts | 49 +++++++++++++++++ src/page/message/index.tsx | 34 ++++++++++++ src/service/message.ts | 35 ++++++++++++ 8 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 src/component/MessageList/index.less create mode 100644 src/component/MessageList/index.tsx create mode 100644 src/model/message.ts create mode 100644 src/page/message/index.tsx create mode 100644 src/service/message.ts diff --git a/config/routes.ts b/config/routes.ts index 0517713..08db875 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -19,11 +19,19 @@ const routes: IRoute[] = [ name: '主页', component: '@/page/topic', }, + { + path: '/my/messages', + exact: true, + icon: 'message', + name: '未读消息', + access: 'canReadMessage', + component: '@/page/message', + }, { path: '/about', exact: true, icon: 'info', - name: '关于', + name: '关于我们', component: '@/page/about', }, { diff --git a/src/access.ts b/src/access.ts index bede563..9b2e8c8 100644 --- a/src/access.ts +++ b/src/access.ts @@ -4,5 +4,6 @@ export default function (initialState: InitialState) { return { canPostTopic: !!token, canPostComment: !!token, + canReadMessage: !!token, }; } diff --git a/src/component/MessageList/index.less b/src/component/MessageList/index.less new file mode 100644 index 0000000..2532459 --- /dev/null +++ b/src/component/MessageList/index.less @@ -0,0 +1,10 @@ +.list { + :global { + .ant-card { + padding: 0; + > .ant-card-body { + padding: 0; + } + } + } +} diff --git a/src/component/MessageList/index.tsx b/src/component/MessageList/index.tsx new file mode 100644 index 0000000..d7faf22 --- /dev/null +++ b/src/component/MessageList/index.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import { useHistory } from 'umi'; +import { Space, Avatar, Tag } from 'antd'; +import { ListToolBarProps } from '@ant-design/pro-table'; +import ProList, { ProListMetas } from '@ant-design/pro-list'; + +import { MESSAGE_TYPE_MAP, MessageType } from '@/constants'; + +import * as styles from './index.less'; + +const MessageList: React.FC = ({ dataSource, loading, toolbar }) => { + const history = useHistory(); + + const metas: ProListMetas = { + avatar: { + dataIndex: 'author.avatar_url', + render: (_, entity: MessageModel) => { + const { type: _type, author } = entity; + const type = MESSAGE_TYPE_MAP[_type as MessageType]; + + return ( + +
+ + + {author.loginname} + +
+ + {type.name} +
+ ); + }, + }, + title: { + dataIndex: 'title', + valueType: 'text', + render: (_, entity: MessageModel) => { + return entity.topic.title; + }, + }, + actions: { + render: (_, entity: MessageModel) => { + return dayjs(entity.create_at).fromNow(); + }, + }, + }; + + return ( + { + return { + onClick: () => { + history.push(`/topic/${record.topic.id}`); + }, + }; + }} + /> + ); +}; + +export default MessageList; + +interface Props { + dataSource?: MessageModel[]; + loading?: boolean; + toolbar?: ListToolBarProps; +} diff --git a/src/constants/index.ts b/src/constants/index.ts index fd80038..34e8782 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -21,13 +21,22 @@ export const TABS_MAP = { name: '客户端测试', color: 'green', }, - dev: { - name: '客户端测试', +}; + +export type TabType = keyof typeof TABS_MAP; + +export const MESSAGE_TYPE_MAP = { + at: { + name: '提到了你', + color: '#108ee9', + }, + reply: { + name: '回复了你', color: 'green', }, }; -export type TabType = keyof typeof TABS_MAP; +export type MessageType = keyof typeof MESSAGE_TYPE_MAP; export enum FORM_TYPE { LOGIN = 'login', diff --git a/src/model/message.ts b/src/model/message.ts new file mode 100644 index 0000000..e09a8dc --- /dev/null +++ b/src/model/message.ts @@ -0,0 +1,49 @@ +import { useState, useCallback, useEffect } from 'react'; +import { loadInitialState } from '@/util'; +import * as API from '@/service/message'; + +export default () => { + const initialState = loadInitialState(); + const { token } = initialState; + + const [count, setCount] = useState(0); + const [message, setMessage] = useState>(); + const [unreadMessage, setUnreadMessage] = useState>(); + + useEffect(() => { + if (!token) { + return; + } + + load(); + fetch(); + }, [token]); + + const load = useCallback(async () => { + if (!token) { + return; + } + + const { data } = await API.getMessageCount({ + accesstoken: token, + }); + + setCount(data); + }, [token]); + + const fetch = useCallback(async () => { + if (!token) { + return; + } + + const { data } = await API.getMessages({ + accesstoken: token, + mdrender: false, + }); + + setMessage(data.has_read_messages); + setUnreadMessage(data.hasnot_read_messages); + }, [token]); + + return { count, message, unreadMessage, load, fetch }; +}; diff --git a/src/page/message/index.tsx b/src/page/message/index.tsx new file mode 100644 index 0000000..d304b4d --- /dev/null +++ b/src/page/message/index.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { useModel } from 'umi'; +import { Divider } from 'antd'; +import ProCard from '@ant-design/pro-card'; +import MessageList from '@/component/MessageList'; + +const MessagePage: React.FC = (props) => { + const { message, unreadMessage } = useModel('message'); + + console.debug('===message', message); + console.debug('===unreadMessage', unreadMessage); + + const renderUnreadMessage = () => { + if (unreadMessage?.length === 0) { + return 暂无新消息; + } + + return ; + }; + + return ( +
+ {renderUnreadMessage()} + + + + +
+ ); +}; + +export default MessagePage; + +interface Props {} diff --git a/src/service/message.ts b/src/service/message.ts new file mode 100644 index 0000000..d6b373e --- /dev/null +++ b/src/service/message.ts @@ -0,0 +1,35 @@ +import { request } from 'umi'; +import { BASE_URL } from '@/constants'; + +export const getMessageCount = async (params: { + accesstoken: string; +}): Promise<{ + data: number; +}> => { + const options: any = { + method: 'GET', + params, + }; + const res: any = await request(`${BASE_URL}/api/v1/message/count`, options); + return res; +}; + +export const getMessages = async (params: { + accesstoken: string; + mdrender?: boolean; +}): Promise<{ + data: MessageCollection; +}> => { + const options: any = { + method: 'GET', + params, + }; + const res: any = await request(`${BASE_URL}/api/v1/messages`, options); + + return res; +}; + +interface MessageCollection { + has_read_messages: MessageModel[]; + hasnot_read_messages: MessageModel[]; +} From 052ac03ec9743df99b5edd56963112b058d134c3 Mon Sep 17 00:00:00 2001 From: Suyi Date: Wed, 5 Jan 2022 19:08:57 +0800 Subject: [PATCH 4/4] feat: refactor topic list --- .../{TopicItemList => TopicList}/index.less | 0 .../{TopicItemList => TopicList}/index.tsx | 18 +++++++------- .../topic/component/CommentList/index.tsx | 16 +++++++++---- src/page/topic/detail.tsx | 23 +++++++++++++++--- src/page/topic/index.tsx | 16 +++++++++---- src/page/user/index.tsx | 18 +++++++------- src/service/topic.ts | 24 ++++++++++++++++--- 7 files changed, 84 insertions(+), 31 deletions(-) rename src/component/{TopicItemList => TopicList}/index.less (100%) rename src/component/{TopicItemList => TopicList}/index.tsx (90%) diff --git a/src/component/TopicItemList/index.less b/src/component/TopicList/index.less similarity index 100% rename from src/component/TopicItemList/index.less rename to src/component/TopicList/index.less diff --git a/src/component/TopicItemList/index.tsx b/src/component/TopicList/index.tsx similarity index 90% rename from src/component/TopicItemList/index.tsx rename to src/component/TopicList/index.tsx index 5bc4694..ce6ee7e 100644 --- a/src/component/TopicItemList/index.tsx +++ b/src/component/TopicList/index.tsx @@ -1,19 +1,21 @@ -import { TABS_MAP, TabType } from '@/constants'; -import ProList, { ProListMetas } from '@ant-design/pro-list'; -import { Space, Avatar, Tag } from 'antd'; import React from 'react'; import dayjs from 'dayjs'; import { useHistory } from 'umi'; +import { Space, Avatar, Tag } from 'antd'; import { ListToolBarProps } from '@ant-design/pro-table'; +import ProList, { ProListMetas } from '@ant-design/pro-list'; + +import { TABS_MAP, TabType } from '@/constants'; + import * as styles from './index.less'; -const TopicItemList: React.FC = ({ dataSource, loading, toolbar }) => { +const TopicList: React.FC = ({ dataSource, loading, toolbar }) => { const history = useHistory(); const metas: ProListMetas = { avatar: { dataIndex: 'author.avatar_url', - render: (_, entity) => { + render: (_, entity: TopicModel) => { const { tab: _tab, author, reply_count, visit_count, top } = entity; const category = TABS_MAP[_tab as TabType]; @@ -55,7 +57,7 @@ const TopicItemList: React.FC = ({ dataSource, loading, toolbar }) => { valueType: 'text', }, actions: { - render: (_, entity) => { + render: (_, entity: TopicModel) => { const { last_reply_at } = entity; return dayjs(last_reply_at).fromNow(); }, @@ -82,10 +84,10 @@ const TopicItemList: React.FC = ({ dataSource, loading, toolbar }) => { ); }; -export default TopicItemList; +export default TopicList; interface Props { - dataSource?: any[]; + dataSource?: TopicModel[]; loading?: boolean; toolbar?: ListToolBarProps; } diff --git a/src/page/topic/component/CommentList/index.tsx b/src/page/topic/component/CommentList/index.tsx index aa03199..9400cd6 100644 --- a/src/page/topic/component/CommentList/index.tsx +++ b/src/page/topic/component/CommentList/index.tsx @@ -31,7 +31,7 @@ const unflatten = (array: Node[], parent?: Node, tree?: Node[]) => { }; const CommentList: React.FC = (props) => { - const { list, onReply, replyRender } = props; + const { list, onLike, onReply, replyRender } = props; const tree = unflatten(list); const CommentDetail: React.FC<{ @@ -46,12 +46,16 @@ const CommentList: React.FC = (props) => { , + { + onLike && onLike(data); + }} + />, , , { - onReply(data); + onReply && onReply(data); }} />, ]} @@ -90,7 +94,11 @@ export default CommentList; interface Props { list: ReplyModel[]; - onReply: (record: Node) => void; + onLike?: (record: Node) => void; + onEdit?: (record: Node) => void; + onReply?: (record: Node) => void; + onDelete?: (record: Node) => void; + replyRender: (id: string) => React.ReactNode; } diff --git a/src/page/topic/detail.tsx b/src/page/topic/detail.tsx index af6d229..bb3aad0 100644 --- a/src/page/topic/detail.tsx +++ b/src/page/topic/detail.tsx @@ -48,7 +48,7 @@ const TopicDetail: React.FC> = (props) => { } const onComment = async (data: { content: string; reply_id?: string }) => { - console.log(topicId, token, data); + console.debug('===onComment', topicId, token, data); if (!token) { return; @@ -58,12 +58,24 @@ const TopicDetail: React.FC> = (props) => { return; } - await API.postTopicReply(topicId, { + await API.postReply(topicId, { ...data, accesstoken: token, }); }; + const onLike = async (record: ReplyModel) => { + const { id: replyId } = record; + + if (!replyId || !token) { + return; + } + + await API.postReplyUps(replyId, { + accesstoken: token, + }); + }; + const onReply = (record: ReplyModel) => { if (reply) { setReply(null); @@ -94,7 +106,12 @@ const TopicDetail: React.FC> = (props) => { const { replies } = data; return ( - + ); }; diff --git a/src/page/topic/index.tsx b/src/page/topic/index.tsx index 8897bad..36f7792 100644 --- a/src/page/topic/index.tsx +++ b/src/page/topic/index.tsx @@ -10,15 +10,21 @@ import type { TabType } from '@/constants'; import * as API from '@/service/topic'; import * as styles from './index.less'; -import TopicItemList from '@/component/TopicItemList'; +import TopicList from '@/component/TopicList'; interface Props {} -const TopicList: React.FC = (props) => { +const TopicListPage: React.FC = (props) => { const access = useAccess(); const history = useHistory(); - const state = useReactive({ + const state = useReactive<{ + tab: string; + page: number; + limit: number; + hasNext: boolean; + data: TopicModel[]; + }>({ tab: 'share', page: 1, limit: 25, @@ -135,7 +141,7 @@ const TopicList: React.FC = (props) => { return (
- item?.author?.loginname)} toolbar={{ @@ -159,4 +165,4 @@ const TopicList: React.FC = (props) => { ); }; -export default TopicList; +export default TopicListPage; diff --git a/src/page/user/index.tsx b/src/page/user/index.tsx index 45c7ae4..9143d42 100644 --- a/src/page/user/index.tsx +++ b/src/page/user/index.tsx @@ -1,12 +1,14 @@ -import TopicItemList from '@/component/TopicItemList'; -import { getUserInfo } from '@/service/user'; -import { GithubOutlined } from '@ant-design/icons'; -import ProCard from '@ant-design/pro-card'; -import { useRequest } from 'ahooks'; -import { Avatar, Divider, Space, Typography } from 'antd'; import dayjs from 'dayjs'; import React from 'react'; import { useParams } from 'umi'; +import { useRequest } from 'ahooks'; +import { GithubOutlined } from '@ant-design/icons'; +import { Avatar, Divider, Space, Typography } from 'antd'; +import ProCard from '@ant-design/pro-card'; + +import TopicList from '@/component/TopicList'; +import { getUserInfo } from '@/service/user'; + import * as styles from './index.less'; const { Text, Paragraph } = Typography; @@ -78,13 +80,13 @@ const UserDetail: React.FC = (props) => { - + - + ); diff --git a/src/service/topic.ts b/src/service/topic.ts index e9308f7..38ec0a4 100644 --- a/src/service/topic.ts +++ b/src/service/topic.ts @@ -6,7 +6,9 @@ export const queryTopicList = async (params: { page?: number; limit?: number; mdrender?: boolean; -}) => { +}): Promise<{ + data: TopicModel[]; +}> => { const { tab = 'ask', page = 1, limit = 10, mdrender = false } = params; const options: any = { method: 'GET', @@ -41,7 +43,9 @@ export const queryTopicDetail = async (params: { id: string; mdrender?: boolean; accesstoken?: boolean; -}) => { +}): Promise<{ + data: TopicModel; +}> => { const { id, mdrender, accesstoken } = params; const options: any = { method: 'GET', @@ -54,7 +58,7 @@ export const queryTopicDetail = async (params: { return request(`${BASE_URL}/api/v1/topic/${id}`, options); }; -export const postTopicReply = async ( +export const postReply = async ( topicId: string, data: { content: string; @@ -69,3 +73,17 @@ export const postTopicReply = async ( return request(`${BASE_URL}/api/v1/topic/${topicId}/replies`, options); }; + +export const postReplyUps = async ( + replyId: string, + data: { + accesstoken: string; + }, +) => { + const options: any = { + method: 'POST', + data, + }; + + return request(`${BASE_URL}/api/v1/reply/${replyId}/ups`, options); +};