style: question details page UI optimization

This commit is contained in:
shuai
2025-08-12 10:10:56 +08:00
parent bfa48a8ca9
commit dc582eec54
12 changed files with 222 additions and 186 deletions
+2
View File
@@ -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
+2
View File
@@ -1381,9 +1381,11 @@ ui:
search: 搜索人员
question_detail:
action: 操作
created: 创建于
Asked: 提问于
asked: 提问于
update: 修改于
Edited: 编辑于
edit: 编辑于
commented: 评论
Views: 阅读次数
@@ -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"
+21 -7
View File
@@ -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',
+88 -45
View File
@@ -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>
</>
);
};
+14 -2
View File
@@ -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')}
+29 -3
View File
@@ -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>
+4
View File
@@ -422,3 +422,7 @@ img[src=''] {
display: none;
line-height: 1;
}
.inherit {
color: inherit !important;
}
@@ -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>
);
};
@@ -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,18 +133,20 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
</Button>
</OverlayTrigger>
</div>
<ImgViewer>
<article
ref={ref}
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>
<ImgViewer>
<article
ref={ref}
className="fmt text-break text-wrap mt-4"
dangerouslySetInnerHTML={{ __html: data?.html }}
/>
</ImgViewer>
<Actions
className="mt-4"
@@ -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>
);
};
@@ -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"
-1
View File
@@ -253,7 +253,6 @@ const Index = () => {
data={question}
initPage={initPage}
hasAnswer={answers.count > 0}
isLogged={isLogged}
/>
)}
{!isLoading && answers.count > 0 && (