feat: Increase the skeleton screen loading effect

This commit is contained in:
shuai 2023-01-05 14:22:25 +08:00
parent df191fa736
commit 0526bb95b6
14 changed files with 436 additions and 151 deletions

View File

@ -13,6 +13,7 @@ import {
Empty,
BaseUserCard,
QueryGroup,
QuestionListLoader,
} from '@/components';
import { useQuestionList } from '@/services';
@ -64,69 +65,73 @@ const QuestionList: FC<Props> = ({ source }) => {
/>
</div>
<ListGroup className="rounded-0">
{listData?.list?.map((li) => {
return (
<ListGroup.Item
key={li.id}
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
<h5 className="text-wrap text-break">
<NavLink
to={pathFactory.questionLanding(li.id, li.url_title)}
className="link-dark">
{li.title}
{li.status === 2 ? ` [${t('closed')}]` : ''}
</NavLink>
</h5>
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 mb-2 text-secondary">
<div className="d-flex">
<BaseUserCard
data={li.operator}
showAvatar={false}
className="me-1"
/>
<FormatTime
time={li.operated_at}
className="text-secondary ms-1"
preFix={t(li.operation_type)}
/>
</div>
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
<span>
<Icon name="hand-thumbs-up-fill" />
<em className="fst-normal ms-1">{li.vote_count}</em>
</span>
<span
className={`ms-3 ${
li.accepted_answer_id >= 1 ? 'text-success' : ''
}`}>
<Icon
name={
li.accepted_answer_id >= 1
? 'check-circle-fill'
: 'chat-square-text-fill'
}
{isLoading ? (
<QuestionListLoader />
) : (
listData?.list?.map((li) => {
return (
<ListGroup.Item
key={li.id}
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
<h5 className="text-wrap text-break">
<NavLink
to={pathFactory.questionLanding(li.id, li.url_title)}
className="link-dark">
{li.title}
{li.status === 2 ? ` [${t('closed')}]` : ''}
</NavLink>
</h5>
<div className="d-flex flex-column flex-md-row align-items-md-center fs-14 mb-2 text-secondary">
<div className="d-flex">
<BaseUserCard
data={li.operator}
showAvatar={false}
className="me-1"
/>
<em className="fst-normal ms-1">{li.answer_count}</em>
</span>
<span className="summary-stat ms-3">
<Icon name="eye-fill" />
<em className="fst-normal ms-1">{li.view_count}</em>
</span>
<FormatTime
time={li.operated_at}
className="text-secondary ms-1"
preFix={t(li.operation_type)}
/>
</div>
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
<span>
<Icon name="hand-thumbs-up-fill" />
<em className="fst-normal ms-1">{li.vote_count}</em>
</span>
<span
className={`ms-3 ${
li.accepted_answer_id >= 1 ? 'text-success' : ''
}`}>
<Icon
name={
li.accepted_answer_id >= 1
? 'check-circle-fill'
: 'chat-square-text-fill'
}
/>
<em className="fst-normal ms-1">{li.answer_count}</em>
</span>
<span className="summary-stat ms-3">
<Icon name="eye-fill" />
<em className="fst-normal ms-1">{li.view_count}</em>
</span>
</div>
</div>
</div>
<div className="question-tags m-n1">
{Array.isArray(li.tags)
? li.tags.map((tag) => {
return (
<Tag key={tag.slug_name} className="m-1" data={tag} />
);
})
: null}
</div>
</ListGroup.Item>
);
})}
<div className="question-tags m-n1">
{Array.isArray(li.tags)
? li.tags.map((tag) => {
return (
<Tag key={tag.slug_name} className="m-1" data={tag} />
);
})
: null}
</div>
</ListGroup.Item>
);
})
)}
</ListGroup>
{count <= 0 && !isLoading && <Empty />}
<div className="mt-4 mb-2 d-flex justify-content-center">

View File

@ -0,0 +1,36 @@
import { FC, memo } from 'react';
import { ListGroupItem } from 'react-bootstrap';
interface Props {
count?: number;
}
const Index: FC<Props> = ({ count = 10 }) => {
const list = new Array(count).fill(0).map((v, i) => v + i);
return (
<>
{list.map((v) => (
<ListGroupItem
className="bg-transparent py-3 px-0 border-start-0 border-end-0 placeholder-glow"
key={v}>
<div
className="placeholder w-100 h5 align-top"
style={{ height: '24px' }}
/>
<div
className="placeholder w-75 d-block align-top mb-2"
style={{ height: '21px' }}
/>
<div
className="placeholder w-50 align-top"
style={{ height: '24px' }}
/>
</ListGroupItem>
))}
</>
);
};
export default memo(Index);

View File

@ -0,0 +1,49 @@
import { FC, memo } from 'react';
import { Col, Card } from 'react-bootstrap';
interface Props {
count?: number;
}
const Index: FC<Props> = ({ count = 20 }) => {
const list = new Array(count).fill(0).map((v, i) => v + i);
return (
<>
{list.map((v) => (
<Col
key={v}
xs={12}
lg={3}
md={4}
sm={6}
className="mb-4 placeholder-glow">
<Card className="h-100">
<Card.Body className="d-flex flex-column align-items-start">
<div
className="placeholder align-top w-25 mb-3"
style={{ height: '24px' }}
/>
<p
className="placeholder fs-14 text-truncate-3 w-100"
style={{ height: '42px' }}
/>
<div className="d-flex align-items-center">
<div
className="placeholder me-2"
style={{ width: '80px', height: '31px' }}
/>
<span
className="placeholder text-secondary fs-14 text-nowrap"
style={{ width: '100px', height: '21px' }}
/>
</div>
</Card.Body>
</Card>
</Col>
))}
</>
);
};
export default memo(Index);

View File

@ -30,6 +30,8 @@ import DiffContent from './DiffContent';
import Customize from './Customize';
import CustomizeTheme from './CustomizeTheme';
import PageTags from './PageTags';
import QuestionListLoader from './QuestionListLoader';
import TagsLoader from './TagsLoader';
export {
Avatar,
@ -66,5 +68,7 @@ export {
Customize,
CustomizeTheme,
PageTags,
QuestionListLoader,
TagsLoader,
};
export type { EditorRef, JSONSchema, UISchema };

View File

@ -0,0 +1,80 @@
import { FC, memo } from 'react';
const Index: FC = () => {
return (
<div className="placeholder-glow">
<div className="placeholder w-100 h1 mb-3" style={{ height: '34px' }} />
<div className="placeholder w-75 mb-3" style={{ height: '21px' }} />
<div
className="placeholder w-50 d-block align-top mb-4"
style={{ height: '24px' }}
/>
<div
className="placeholder w-100 align-top"
style={{ height: '450px', marginBottom: '2rem' }}
/>
<div className="d-flex">
<div
className="placeholder align-top me-3 rounded"
style={{ height: '38px', width: '120px' }}
/>
<div
className="placeholder align-top rounded"
style={{ height: '38px', width: '68px' }}
/>
</div>
<div className="d-block d-md-flex flex-wrap mt-4 mb-3">
<div
className="placeholder mb-3 mb-md-0 me-4"
style={{ height: '21px', width: '40%' }}
/>
<div
style={{ minWidth: '196px', height: '24px' }}
className="placeholder mb-3 me-4 mb-md-0 d-block d-md-none"
/>
<div
style={{ minWidth: '196px', height: '24px' }}
className="placeholder d-block d-md-none"
/>
<div
style={{ minWidth: '196px', height: '40px' }}
className="placeholder mb-3 me-4 mb-md-0 d-none d-md-block"
/>
<div
style={{ minWidth: '196px', height: '40px' }}
className="placeholder d-none d-md-block"
/>
</div>
{[0, 1, 2].map((item, i) => (
<div
className={`border-bottom py-2 ${i === 0 ? 'border-top' : ''}`}
key={item}>
<div className="placeholder w-100 mb-1" style={{ height: '17px' }} />
<div className="placeholder w-50" style={{ height: '17px' }} />
</div>
))}
<div className="d-flex mt-2 mb-4">
<div
className="placeholder align-top me-4"
style={{ height: '21px', width: '140px' }}
/>
<div
className="placeholder align-top"
style={{ height: '21px', width: '140px' }}
/>
</div>
</div>
);
};
export default memo(Index);

View File

@ -17,11 +17,15 @@ const Index: FC<Props> = ({ id }) => {
keyPrefix: 'related_question',
});
const { data } = useSimilarQuestion({
const { data, isLoading } = useSimilarQuestion({
question_id: id,
page_size: 5,
});
if (isLoading) {
return null;
}
return (
<Card>
<Card.Header>{t('title')}</Card.Header>

View File

@ -4,5 +4,14 @@ import AnswerHead from './AnswerHead';
import RelatedQuestions from './RelatedQuestions';
import WriteAnswer from './WriteAnswer';
import Alert from './Alert';
import ContentLoader from './ContentLoader';
export { Question, Answer, AnswerHead, RelatedQuestions, WriteAnswer, Alert };
export {
Question,
Answer,
AnswerHead,
RelatedQuestions,
WriteAnswer,
Alert,
ContentLoader,
};

View File

@ -27,6 +27,7 @@ import {
RelatedQuestions,
WriteAnswer,
Alert,
ContentLoader,
} from './components';
import './index.scss';
@ -45,6 +46,7 @@ const Index = () => {
const page = Number(urlSearch.get('page') || 0);
const order = urlSearch.get('order') || '';
const [question, setQuestion] = useState<QuestionDetailRes | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [answers, setAnswers] = useState<ListResult<AnswerItem>>({
count: -1,
list: [],
@ -95,15 +97,20 @@ const Index = () => {
};
const getDetail = async () => {
const res = await questionDetail(qid);
if (res) {
// undo
setUsers([
res.user_info,
res?.update_user_info,
res?.last_answered_user_info,
]);
setQuestion(res);
setIsLoading(true);
try {
const res = await questionDetail(qid);
if (res) {
setUsers([
res.user_info,
res?.update_user_info,
res?.last_answered_user_info,
]);
setQuestion(res);
}
setIsLoading(false);
} catch (e) {
setIsLoading(false);
}
};
@ -162,13 +169,17 @@ const Index = () => {
{question?.operation?.operation_type && (
<Alert data={question.operation} />
)}
<Question
data={question}
initPage={initPage}
hasAnswer={answers.count > 0}
isLogged={isLogged}
/>
{answers.count > 0 && (
{isLoading ? (
<ContentLoader />
) : (
<Question
data={question}
initPage={initPage}
hasAnswer={answers.count > 0}
isLogged={isLogged}
/>
)}
{!isLoading && answers.count > 0 && (
<>
<AnswerHead count={answers.count} order={order} />
{answers?.list?.map((item) => {
@ -188,7 +199,7 @@ const Index = () => {
</>
)}
{Math.ceil(answers.count / 15) > 1 && (
{!isLoading && Math.ceil(answers.count / 15) > 1 && (
<div className="d-flex justify-content-center answer-item pt-4">
<Pagination
currentPage={Number(page || 1)}
@ -198,7 +209,7 @@ const Index = () => {
</div>
)}
{!question?.operation?.operation_type && (
{!isLoading && !question?.operation?.operation_type && (
<WriteAnswer
visible={answers.count === 0}
data={{

View File

@ -0,0 +1,46 @@
import { FC, memo } from 'react';
import { ListGroupItem } from 'react-bootstrap';
interface Props {
count?: number;
}
const Index: FC<Props> = ({ count = 10 }) => {
const list = new Array(count).fill(0).map((v, i) => v + i);
return (
<>
{list.map((v) => (
<ListGroupItem
className="py-3 px-0 border-start-0 border-end-0 bg-transparent placeholder-glow"
key={v}>
<div className="mb-2">
<div
className="placeholder me-2"
style={{ height: '25px', width: '30px' }}
/>
<div
className="h5 mb-0 w-75 placeholder"
style={{ height: '25px' }}
/>
</div>
<div
className="placeholder w-50 h5 align-top mb-2"
style={{ height: '21px' }}
/>
<div
className="placeholder w-100 d-block align-top mb-2"
style={{ height: '42px' }}
/>
<div
className="placeholder w-25 align-top"
style={{ height: '24px' }}
/>
</ListGroupItem>
))}
</>
);
};
export default memo(Index);

View File

@ -3,5 +3,6 @@ import SearchItem from './SearchItem';
import Tips from './Tips';
import Empty from './Empty';
import SearchHead from './SearchHead';
import ListLoader from './ListLoader';
export { Head, SearchItem, Tips, Empty, SearchHead };
export { Head, SearchItem, Tips, Empty, SearchHead, ListLoader };

View File

@ -7,7 +7,14 @@ import { usePageTags } from '@/hooks';
import { Pagination } from '@/components';
import { useSearch } from '@/services';
import { Head, SearchHead, SearchItem, Tips, Empty } from './components';
import {
Head,
SearchHead,
SearchItem,
Tips,
Empty,
ListLoader,
} from './components';
const Index = () => {
const { t } = useTranslation('translation');
@ -38,9 +45,13 @@ const Index = () => {
<Head data={extra} />
<SearchHead sort={order} count={count} />
<ListGroup className="rounded-0 mb-5">
{list?.map((item) => {
return <SearchItem key={item.object.id} data={item} />;
})}
{isLoading ? (
<ListLoader />
) : (
list?.map((item) => {
return <SearchItem key={item.object.id} data={item} />;
})
)}
</ListGroup>
{!isLoading && !list?.length && <Empty />}

View File

@ -19,7 +19,7 @@ const Questions: FC = () => {
const curTagName = routeParams.tagName || '';
const [tagInfo, setTagInfo] = useState<any>({});
const [tagFollow, setTagFollow] = useState<Type.FollowParams>();
const { data: tagResp } = useTagInfo({ name: curTagName });
const { data: tagResp, isLoading } = useTagInfo({ name: curTagName });
const { data: followResp } = useFollow(tagFollow);
const { data: synonymsRes } = useQuerySynonymsTags(tagInfo?.tag_id);
const toggleFollow = () => {
@ -73,37 +73,52 @@ const Questions: FC = () => {
<Container className="pt-4 mt-2 mb-5">
<Row className="justify-content-center">
<Col xxl={7} lg={8} sm={12}>
<div className="tag-box mb-5">
<h3 className="mb-3">
<Link
to={pathFactory.tagLanding(tagInfo.slug_name)}
replace
className="link-dark">
{tagInfo.display_name}
</Link>
</h3>
{isLoading ? (
<div className="tag-box mb-5 placeholder-glow">
<div className="mb-3 h3 placeholder" style={{ width: '120px' }} />
<p
className="placeholder w-100 d-block align-top"
style={{ height: '24px' }}
/>
<p className="text-break">
{escapeRemove(tagInfo.excerpt) || t('no_desc')}
<Link to={pathFactory.tagInfo(curTagName)} className="ms-1">
[{t('more')}]
</Link>
</p>
<div className="box-ft">
{tagInfo.is_follower ? (
<Button variant="primary" onClick={() => toggleFollow()}>
{t('button_following')}
</Button>
) : (
<Button
variant="outline-primary"
onClick={() => toggleFollow()}>
{t('button_follow')}
</Button>
)}
<div
className="placeholder d-block align-top"
style={{ height: '38px', width: '100px' }}
/>
</div>
</div>
) : (
<div className="tag-box mb-5">
<h3 className="mb-3">
<Link
to={pathFactory.tagLanding(tagInfo.slug_name)}
replace
className="link-dark">
{tagInfo.display_name}
</Link>
</h3>
<p className="text-break">
{escapeRemove(tagInfo.excerpt) || t('no_desc')}
<Link to={pathFactory.tagInfo(curTagName)} className="ms-1">
[{t('more')}]
</Link>
</p>
<div className="box-ft">
{tagInfo.is_follower ? (
<Button variant="primary" onClick={() => toggleFollow()}>
{t('button_following')}
</Button>
) : (
<Button
variant="outline-primary"
onClick={() => toggleFollow()}>
{t('button_follow')}
</Button>
)}
</div>
</div>
)}
<QuestionList source="tag" />
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">

View File

@ -4,7 +4,7 @@ import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { Tag, Pagination, QueryGroup } from '@/components';
import { Tag, Pagination, QueryGroup, TagsLoader } from '@/components';
import { formatCount } from '@/utils';
import { useQueryTags, following } from '@/services';
@ -19,7 +19,11 @@ const Tags = () => {
const sort = urlSearch.get('sort');
const pageSize = 20;
const { data: tags, mutate } = useQueryTags({
const {
data: tags,
mutate,
isLoading,
} = useQueryTags({
page,
page_size: pageSize,
...(searchTag ? { slug_name: searchTag } : {}),
@ -69,39 +73,43 @@ const Tags = () => {
<Col className="mt-4" xxl={10} sm={12}>
<Row>
{tags?.list?.map((tag) => (
<Col
key={tag.slug_name}
xs={12}
lg={3}
md={4}
sm={6}
className="mb-4">
<Card className="h-100">
<Card.Body className="d-flex flex-column align-items-start">
<Tag className="mb-3" data={tag} />
{isLoading ? (
<TagsLoader />
) : (
tags?.list?.map((tag) => (
<Col
key={tag.slug_name}
xs={12}
lg={3}
md={4}
sm={6}
className="mb-4">
<Card className="h-100">
<Card.Body className="d-flex flex-column align-items-start">
<Tag className="mb-3" data={tag} />
<p className="fs-14 flex-fill text-break text-wrap text-truncate-3">
{tag.original_text}
</p>
<div className="d-flex align-items-center">
<Button
className={`me-2 ${tag.is_follower ? 'active' : ''}`}
variant="outline-primary"
size="sm"
onClick={() => handleFollow(tag)}>
{tag.is_follower
? t('button_following')
: t('button_follow')}
</Button>
<span className="text-secondary fs-14 text-nowrap">
{formatCount(tag.question_count)} {t('tag_label')}
</span>
</div>
</Card.Body>
</Card>
</Col>
))}
<p className="fs-14 flex-fill text-break text-wrap text-truncate-3">
{tag.original_text}
</p>
<div className="d-flex align-items-center">
<Button
className={`me-2 ${tag.is_follower ? 'active' : ''}`}
variant="outline-primary"
size="sm"
onClick={() => handleFollow(tag)}>
{tag.is_follower
? t('button_following')
: t('button_follow')}
</Button>
<span className="text-secondary fs-14 text-nowrap">
{formatCount(tag.question_count)} {t('tag_label')}
</span>
</div>
</Card.Body>
</Card>
</Col>
))
)}
</Row>
<div className="d-flex justify-content-center">
<Pagination

View File

@ -19,12 +19,18 @@ export const useQueryQuestionByTitle = (title) => {
};
export const useQueryTags = (params) => {
return useSWR<Type.ListResult>(
const { data, error, mutate } = useSWR<Type.ListResult>(
`/answer/api/v1/tags/page?${qs.stringify(params, {
skipNulls: true,
})}`,
request.instance.get,
);
return {
data,
isLoading: !data && !error,
error,
mutate,
};
};
export const useQueryRevisions = (object_id: string | undefined) => {