This is an automated email from the ASF dual-hosted git repository. shuai pushed a commit to branch test in repository https://gitbox.apache.org/repos/asf/answer.git
The following commit(s) were added to refs/heads/test by this push: new dc582eec style: question details page UI optimization dc582eec is described below commit dc582eec54aa6aa2b633ff159f00035497d4a08e Author: shuai <lishuail...@sifou.com> AuthorDate: Tue Aug 12 10:10:56 2025 +0800 style: question details page UI optimization --- i18n/en_US.yaml | 2 + i18n/zh_CN.yaml | 2 + .../Comment/components/ActionBar/index.tsx | 4 +- ui/src/components/Comment/index.tsx | 28 +++-- ui/src/components/Operate/index.tsx | 133 ++++++++++++++------- ui/src/components/Share/index.tsx | 16 ++- ui/src/components/UserCard/index.tsx | 32 ++++- ui/src/index.scss | 4 + .../Questions/Detail/components/Answer/index.tsx | 95 +++++---------- .../Questions/Detail/components/Question/index.tsx | 82 ++++--------- .../Detail/components/Reactions/index.tsx | 7 +- ui/src/pages/Questions/Detail/index.tsx | 1 - 12 files changed, 221 insertions(+), 185 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index da34fcb1..2d804918 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1407,9 +1407,11 @@ ui: search: Search people question_detail: action: Action + created: Created Asked: Asked asked: asked update: Modified + Edited: Edited edit: edited commented: commented Views: Viewed diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index b62fd470..35162045 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1381,9 +1381,11 @@ ui: search: 搜索人员 question_detail: action: 操作 + created: 创建于 Asked: 提问于 asked: 提问于 update: 修改于 + Edited: 编辑于 edit: 编辑于 commented: 评论 Views: 阅读次数 diff --git a/ui/src/components/Comment/components/ActionBar/index.tsx b/ui/src/components/Comment/components/ActionBar/index.tsx index c55bf40e..692b0d20 100644 --- a/ui/src/components/Comment/components/ActionBar/index.tsx +++ b/ui/src/components/Comment/components/ActionBar/index.tsx @@ -64,7 +64,9 @@ const ActionBar = ({ }`} onClick={onVote}> <Icon name="hand-thumbs-up-fill" /> - {voteCount > 0 && <span className="ms-2">{voteCount}</span>} + {voteCount > 0 && ( + <span className="ms-2 link-secondary">{voteCount}</span> + )} </Button> <Button variant="link" diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index c639fc64..2a052e28 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { useState, useEffect } from 'react'; +import { FC, useState, useEffect } from 'react'; import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; @@ -50,7 +50,14 @@ import { Form, ActionBar, Reply } from './components'; import './index.scss'; -const Comment = ({ objectId, mode, commentId }) => { +interface IProps { + objectId: string; + mode?: 'answer' | 'question'; + commentId?: string; + children?: React.ReactNode; +} + +const Comment: FC<IProps> = ({ objectId, mode, commentId, children }) => { const pageUsers = usePageUsers(); const [pageIndex, setPageIndex] = useState(0); const [visibleComment, setVisibleComment] = useState(false); @@ -374,11 +381,18 @@ const Comment = ({ objectId, mode, commentId }) => { return ( <> - <Reactions - objectId={objectId} - showAddCommentBtn={comments.length === 0} - handleClickComment={handleAddComment} - /> + <div + className={classNames( + 'd-flex flex-wrap justify-content-between align-items-center', + comments.length === 0 ? '' : 'mb-3', + )}> + <Reactions + objectId={objectId} + showAddCommentBtn={comments.length === 0} + handleClickComment={handleAddComment} + /> + {children} + </div> <div className={classNames( 'comments-wrap', diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index 47b4463f..c7b5c740 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -22,7 +22,7 @@ import { Button, Dropdown } from 'react-bootstrap'; import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Modal } from '@/components'; +import { Icon, Modal } from '@/components'; import { useReportModal, useToast } from '@/hooks'; import { useCaptchaPlugin } from '@/utils/pluginKit'; import { QuestionOperationReq } from '@/common/interface'; @@ -40,6 +40,8 @@ import { tryNormalLogged } from '@/utils/guard'; import { floppyNavigation } from '@/utils'; import { toastStore } from '@/stores'; +import '@/components/QueryGroup/index.scss'; + interface IProps { type: 'answer' | 'question'; qid: string; @@ -334,54 +336,95 @@ const Index: FC<IProps> = ({ ) || []; return ( - <div className="d-flex align-items-center"> - <Share type={type} qid={qid} aid={aid} title={title} /> - {firstAction?.map((item) => { - if (item.action === 'edit') { + <> + <div className="md-show align-items-center"> + <Share + type={type} + qid={qid} + aid={aid} + title={title} + className="link-secondary small" + /> + {firstAction?.map((item) => { + if (item.action === 'edit') { + return ( + <Link + key={item.action} + to={editUrl} + className="link-secondary p-0 small ms-3" + onClick={(evt) => handleEdit(evt, editUrl)} + style={{ lineHeight: '23px' }}> + {item.name} + </Link> + ); + } return ( - <Link + <Button key={item.action} - to={editUrl} - className="link-secondary p-0 small ms-3" - onClick={(evt) => handleEdit(evt, editUrl)} - style={{ lineHeight: '23px' }}> + variant="link" + size="sm" + className="link-secondary p-0 ms-3" + onClick={() => handleAction(item.action)}> {item.name} - </Link> + </Button> ); - } - return ( - <Button - key={item.action} - variant="link" - size="sm" - className="link-secondary p-0 ms-3" - onClick={() => handleAction(item.action)}> - {item.name} - </Button> - ); - })} - {secondAction.length > 0 && ( - <Dropdown className="ms-3 d-flex"> - <Dropdown.Toggle - variant="link" - size="sm" - className="link-secondary p-0 no-toggle"> - {t('action', { keyPrefix: 'question_detail' })} - </Dropdown.Toggle> - <Dropdown.Menu> - {secondAction.map((item) => { - return ( - <Dropdown.Item - key={item.action} - onClick={() => handleAction(item.action)}> - {item.name} - </Dropdown.Item> - ); - })} - </Dropdown.Menu> - </Dropdown> - )} - </div> + })} + {secondAction.length > 0 && ( + <Dropdown className="ms-3 d-flex"> + <Dropdown.Toggle + variant="link" + size="sm" + title={t('action', { keyPrefix: 'question_detail' })} + className="link-secondary p-0 no-toggle"> + <Icon name="three-dots" /> + </Dropdown.Toggle> + <Dropdown.Menu> + {secondAction.map((item) => { + return ( + <Dropdown.Item + key={item.action} + onClick={() => handleAction(item.action)}> + {item.name} + </Dropdown.Item> + ); + })} + </Dropdown.Menu> + </Dropdown> + )} + </div> + <div className="md-hide"> + {memberActions.length > 0 && ( + <Dropdown className="d-flex"> + <Dropdown.Toggle + variant="link" + size="sm" + title={t('action', { keyPrefix: 'question_detail' })} + className="link-secondary no-toggle"> + <Icon name="three-dots" /> + </Dropdown.Toggle> + <Dropdown.Menu> + <Share + type={type} + qid={qid} + aid={aid} + title={title} + className="inherit" + mode="mobile" + /> + {[...firstAction, ...secondAction].map((item) => { + return ( + <Dropdown.Item + key={item.action} + onClick={() => handleAction(item.action)}> + {item.name} + </Dropdown.Item> + ); + })} + </Dropdown.Menu> + </Dropdown> + )} + </div> + </> ); }; diff --git a/ui/src/components/Share/index.tsx b/ui/src/components/Share/index.tsx index 89ae05ce..d63147b1 100644 --- a/ui/src/components/Share/index.tsx +++ b/ui/src/components/Share/index.tsx @@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next'; import { FacebookShareButton, TwitterShareButton } from 'next-share'; import copy from 'copy-to-clipboard'; +import classNames from 'classnames'; import { BASE_ORIGIN } from '@/router/alias'; import { loggedUserInfoStore } from '@/stores'; @@ -32,10 +33,12 @@ interface IProps { qid: any; aid?: any; title: string; + className?: string; + mode?: 'normal' | 'mobile'; // slugTitle: string; } -const Index: FC<IProps> = ({ type, qid, aid, title }) => { +const Index: FC<IProps> = ({ type, qid, aid, title, className, mode }) => { const user = loggedUserInfoStore((state) => state.user); const [show, setShow] = useState(false); const [showTip, setShowTip] = useState(false); @@ -78,12 +81,21 @@ const Index: FC<IProps> = ({ type, qid, aid, title }) => { setSystemShareState(true); } }, []); + + if (mode === 'mobile') { + if (canSystemShare) { + return ( + <Dropdown.Item onClick={systemShare}>{t('share.name')}</Dropdown.Item> + ); + } + return null; + } return ( <Dropdown show={show} onToggle={closeShare}> <Dropdown.Toggle id="dropdown-share" as="a" - className="no-toggle small link-secondary pointer d-flex" + className={classNames('no-toggle pointer d-flex', className)} onClick={() => setShow(true)} style={{ lineHeight: '23px' }}> {t('share.name')} diff --git a/ui/src/components/UserCard/index.tsx b/ui/src/components/UserCard/index.tsx index 44bd19e0..cc7e883c 100644 --- a/ui/src/components/UserCard/index.tsx +++ b/ui/src/components/UserCard/index.tsx @@ -28,10 +28,12 @@ import { formatCount } from '@/utils'; interface Props { data: any; time: number; - preFix: string; + preFix?: string; isLogged: boolean; timelinePath: string; className?: string; + updateTime?: number; + updateTimePrefix?: string; } const Index: FC<Props> = ({ @@ -41,6 +43,8 @@ const Index: FC<Props> = ({ isLogged, timelinePath, className = '', + updateTime = 0, + updateTimePrefix = '', }) => { return ( <div className={classnames('d-flex', className)}> @@ -81,7 +85,7 @@ const Index: FC<Props> = ({ /> </> )} - <div className="small text-secondary d-flex flex-row flex-md-column align-items-center align-items-md-start"> + <div className="small text-secondary d-flex flex-column"> <div className="me-1 me-md-0 d-flex align-items-center"> {data?.status !== 'deleted' ? ( <Link @@ -105,9 +109,31 @@ const Index: FC<Props> = ({ preFix={preFix} className="link-secondary" /> + {updateTime > 0 && ( + <> + <span className="mx-1 link-secondary">•</span> + <FormatTime + time={updateTime} + preFix={updateTimePrefix} + className="link-secondary" + /> + </> + )} </Link> ) : ( - <FormatTime time={time} preFix={preFix} /> + <> + <FormatTime time={time} preFix={preFix} /> + {updateTime > 0 && ( + <> + <span className="mx-1 link-secondary">•</span> + <FormatTime + time={updateTime} + preFix={updateTimePrefix} + className="link-secondary" + /> + </> + )} + </> ))} </div> </div> diff --git a/ui/src/index.scss b/ui/src/index.scss index fb1e06a7..e3153fd2 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -422,3 +422,7 @@ img[src=''] { display: none; line-height: 1; } + +.inherit { + color: inherit !important; +} diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index 825f698d..957a60df 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -20,7 +20,7 @@ import { memo, FC, useEffect, useRef } from 'react'; import { Button, Alert, Badge } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Link, useSearchParams } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { Actions, @@ -28,7 +28,6 @@ import { UserCard, Icon, Comment, - FormatTime, htmlRender, ImgViewer, } from '@/components'; @@ -110,22 +109,34 @@ const Index: FC<Props> = ({ {t('post_pending', { keyPrefix: 'messages' })} </Alert> )} - - {data?.accepted === 2 && ( - <div className="mb-3 lh-1"> - <Badge bg="success" pill> - <Icon name="check-circle-fill me-1" /> - Best answer - </Badge> + <div className="d-flex justify-content-between mb-3"> + <div style={{ minWidth: '196px' }}> + <UserCard + data={data?.user_info} + time={Number(data.create_time)} + updateTime={Number(data.update_time)} + updateTimePrefix={t('edit')} + isLogged={isLogged} + timelinePath={`/posts/${data.question_id}/${data.id}/timeline`} + /> </div> - )} + + {data?.accepted === 2 && ( + <div className="lh-1"> + <Badge bg="success" pill> + <Icon name="check-circle-fill me-1" /> + Best answer + </Badge> + </div> + )} + </div> <ImgViewer> <article className="fmt text-break text-wrap" dangerouslySetInnerHTML={{ __html: data?.html }} /> </ImgViewer> - <div className="d-flex align-items-center mt-4"> + <div className="d-flex align-items-center my-4"> <Actions source="answer" data={{ @@ -155,60 +166,20 @@ const Index: FC<Props> = ({ )} </div> - <div className="d-block d-md-flex flex-wrap mt-4 mb-3"> - <div className="mb-3 mb-md-0 me-4 flex-grow-1"> - <Operate - qid={data.question_id} - aid={data.id} - memberActions={data?.member_actions} - type="answer" - isAccepted={data.accepted === 2} - title={questionTitle} - callback={callback} - /> - </div> - <div className="mb-3 mb-md-0 me-4" style={{ minWidth: '196px' }}> - {data.update_user_info && - data.update_user_info?.username !== data.user_info?.username ? ( - <UserCard - data={data?.update_user_info} - time={Number(data.update_time)} - preFix={t('edit')} - isLogged={isLogged} - timelinePath={`/posts/${data.question_id}/${data.id}/timeline`} - /> - ) : isLogged ? ( - <Link to={`/posts/${data.question_id}/${data.id}/timeline`}> - <FormatTime - time={Number(data.update_time)} - preFix={t('edit')} - className="link-secondary small" - /> - </Link> - ) : ( - <FormatTime - time={Number(data.update_time)} - preFix={t('edit')} - className="text-secondary small" - /> - )} - </div> - <div style={{ minWidth: '196px' }}> - <UserCard - data={data?.user_info} - time={Number(data.create_time)} - preFix={t('answered')} - isLogged={isLogged} - timelinePath={`/posts/${data.question_id}/${data.id}/timeline`} - /> - </div> - </div> - <Comment objectId={data.id} mode="answer" - commentId={searchParams.get('commentId')} - /> + commentId={String(searchParams.get('commentId'))}> + <Operate + qid={data.question_id} + aid={data.id} + memberActions={data?.member_actions} + type="answer" + isAccepted={data.accepted === 2} + title={questionTitle} + callback={callback} + /> + </Comment> </div> ); }; diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index 93134578..19b73b06 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -26,7 +26,7 @@ import { Tag, Actions, Operate, - UserCard, + BaseUserCard, Comment, FormatTime, htmlRender, @@ -40,11 +40,10 @@ import { pathFactory } from '@/router/pathFactory'; interface Props { data: any; hasAnswer: boolean; - isLogged: boolean; initPage: (type: string) => void; } -const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => { +const Index: FC<Props> = ({ data, initPage, hasAnswer }) => { const { t } = useTranslation('translation', { keyPrefix: 'question_detail', }); @@ -90,7 +89,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => { return ( <div> - <h1 className="h3 mb-3 text-wrap text-break"> + <h1 className="h3 mb-2 text-wrap text-break pb-1"> <Link className="link-dark" reloadDocument @@ -102,18 +101,21 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => { </Link> </h1> - <div className="d-flex flex-wrap align-items-center small mb-3 text-secondary"> + <div className="d-flex flex-wrap align-items-center small mb-4 text-secondary border-bottom pb-3"> + <BaseUserCard data={data.user_info} className="me-3" /> + <FormatTime time={data.create_time} - preFix={t('Asked')} + preFix={t('created')} className="me-3" /> <FormatTime - time={data.update_time} - preFix={t('update')} + time={data.edit_time} + preFix={t('Edited')} className="me-3" /> + {data?.view_count > 0 && ( <div className="me-3"> {t('Views')} {formatCount(data.view_count)} @@ -131,19 +133,21 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => { </Button> </OverlayTrigger> </div> - <div className="m-n1"> - {data?.tags?.map((item: any) => { - return <Tag className="m-1" key={item.slug_name} data={item} />; - })} - </div> + <ImgViewer> <article ref={ref} - className="fmt text-break text-wrap mt-4" + className="fmt text-break text-wrap" dangerouslySetInnerHTML={{ __html: data?.html }} /> </ImgViewer> + <div className="m-n1"> + {data?.tags?.map((item: any) => { + return <Tag className="m-1" key={item.slug_name} data={item} />; + })} + </div> + <Actions className="mt-4" source="question" @@ -158,8 +162,11 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => { }} /> - <div className="d-block d-md-flex flex-wrap mt-4 mb-3"> - <div className="mb-3 mb-md-0 me-4 flex-grow-1"> + <div className="mt-4"> + <Comment + objectId={data?.id} + mode="question" + commentId={String(searchParams.get('commentId'))}> <Operate qid={data?.id} type="question" @@ -169,49 +176,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => { isAccepted={Boolean(data?.accepted_answer_id)} callback={initPage} /> - </div> - <div style={{ minWidth: '196px' }} className="mb-3 me-4 mb-md-0"> - {data.update_user_info && - data.update_user_info?.username !== data.user_info?.username ? ( - <UserCard - data={data?.update_user_info} - time={data.edit_time} - preFix={t('edit')} - isLogged={isLogged} - timelinePath={`/posts/${data.id}/timeline`} - /> - ) : isLogged ? ( - <Link to={`/posts/${data.id}/timeline`}> - <FormatTime - time={data.edit_time} - preFix={t('edit')} - className="link-secondary small" - /> - </Link> - ) : ( - <FormatTime - time={data.edit_time} - preFix={t('edit')} - className="text-secondary small" - /> - )} - </div> - <div style={{ minWidth: '196px' }}> - <UserCard - data={data?.user_info} - time={data.create_time} - preFix={t('asked')} - isLogged={isLogged} - timelinePath={`/posts/${data.id}/timeline`} - /> - </div> + </Comment> </div> - - <Comment - objectId={data?.id} - mode="question" - commentId={searchParams.get('commentId')} - /> </div> ); }; diff --git a/ui/src/pages/Questions/Detail/components/Reactions/index.tsx b/ui/src/pages/Questions/Detail/components/Reactions/index.tsx index 9c747cf8..9386b94c 100644 --- a/ui/src/pages/Questions/Detail/components/Reactions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Reactions/index.tsx @@ -21,8 +21,6 @@ import { FC, memo, useEffect, useState } from 'react'; import { Button, OverlayTrigger, Popover, Tooltip } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import classNames from 'classnames'; - import { Icon } from '@/components'; import { queryReactions, updateReaction } from '@/services'; import { tryNormalLogged } from '@/utils/guard'; @@ -111,10 +109,7 @@ const Index: FC<Props> = ({ ); return ( - <div - className={classNames('d-flex flex-wrap', { - 'mb-3': !showAddCommentBtn, - })}> + <div className="d-flex flex-wrap"> {showAddCommentBtn && ( <Button className="rounded-pill me-2 link-secondary btn-reaction" diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 8c8921b8..2a1e1d77 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -253,7 +253,6 @@ const Index = () => { data={question} initPage={initPage} hasAnswer={answers.count > 0} - isLogged={isLogged} /> )} {!isLoading && answers.count > 0 && (