mirror of https://gitee.com/answerdev/answer.git
feat: add timeline router
This commit is contained in:
parent
9359f27729
commit
f3ec755e92
|
@ -380,6 +380,7 @@ ui:
|
||||||
tag_info:
|
tag_info:
|
||||||
created_at: Created
|
created_at: Created
|
||||||
edited_at: Edited
|
edited_at: Edited
|
||||||
|
history: History
|
||||||
synonyms:
|
synonyms:
|
||||||
title: Synonyms
|
title: Synonyms
|
||||||
text: The following tags will be remapped to
|
text: The following tags will be remapped to
|
||||||
|
|
|
@ -51,6 +51,7 @@ module.exports = {
|
||||||
'react-hooks/exhaustive-deps': 'off',
|
'react-hooks/exhaustive-deps': 'off',
|
||||||
'react/jsx-props-no-spreading': 'off',
|
'react/jsx-props-no-spreading': 'off',
|
||||||
'@typescript-eslint/default-param-last': 'off',
|
'@typescript-eslint/default-param-last': 'off',
|
||||||
|
'no-nested-ternary': 'off',
|
||||||
'import/order': [
|
'import/order': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
|
|
@ -563,4 +563,6 @@ export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
|
||||||
'deleted',
|
'deleted',
|
||||||
'downvote',
|
'downvote',
|
||||||
'upvote',
|
'upvote',
|
||||||
|
'reopened',
|
||||||
|
'closed',
|
||||||
];
|
];
|
||||||
|
|
|
@ -376,10 +376,8 @@ export interface AdminDashboard {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimelineReq {
|
export interface TimelineReq {
|
||||||
object_type: string;
|
|
||||||
show_vote: boolean;
|
show_vote: boolean;
|
||||||
object_id?: string;
|
object_id: string;
|
||||||
tag_slug_name?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimelineItem {
|
export interface TimelineItem {
|
||||||
|
|
|
@ -4,28 +4,27 @@ import { Tag } from '@/components';
|
||||||
import { diffText } from '@/utils';
|
import { diffText } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentData: Record<string, any>;
|
objectType: string;
|
||||||
prevData?: Record<string, any>;
|
newData: Record<string, any>;
|
||||||
|
oldData?: Record<string, any>;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Index: FC<Props> = ({ currentData, prevData, className = '' }) => {
|
const Index: FC<Props> = ({ objectType, newData, oldData, className = '' }) => {
|
||||||
if (!currentData?.content) return null;
|
if (!newData?.original_text) return null;
|
||||||
|
|
||||||
let tag;
|
let tag = newData.tags;
|
||||||
if (prevData?.tags) {
|
if (objectType === 'question' && oldData?.tags) {
|
||||||
const addTags = currentData.tags.filter(
|
const addTags = newData.tags.filter(
|
||||||
(c) => !prevData?.tags?.find((p) => p.slug_name === c.slug_name),
|
(c) => !oldData?.tags?.find((p) => p.slug_name === c.slug_name),
|
||||||
);
|
);
|
||||||
|
|
||||||
let deleteTags = prevData?.tags
|
let deleteTags = oldData?.tags
|
||||||
.filter(
|
.filter((c) => !newData?.tags.find((p) => p.slug_name === c.slug_name))
|
||||||
(c) => !currentData?.tags.find((p) => p.slug_name === c.slug_name),
|
|
||||||
)
|
|
||||||
.map((v) => ({ ...v, state: 'delete' }));
|
.map((v) => ({ ...v, state: 'delete' }));
|
||||||
|
|
||||||
deleteTags = deleteTags?.map((v) => {
|
deleteTags = deleteTags?.map((v) => {
|
||||||
const index = prevData?.tags?.findIndex(
|
const index = oldData?.tags?.findIndex(
|
||||||
(c) => c.slug_name === v.slug_name,
|
(c) => c.slug_name === v.slug_name,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
@ -34,7 +33,7 @@ const Index: FC<Props> = ({ currentData, prevData, className = '' }) => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
tag = currentData.tags.map((item) => {
|
tag = newData.tags.map((item) => {
|
||||||
const find = addTags.find((c) => c.slug_name === item.slug_name);
|
const find = addTags.find((c) => c.slug_name === item.slug_name);
|
||||||
if (find) {
|
if (find) {
|
||||||
return {
|
return {
|
||||||
|
@ -52,27 +51,43 @@ const Index: FC<Props> = ({ currentData, prevData, className = '' }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<h5
|
{objectType !== 'answer' && (
|
||||||
dangerouslySetInnerHTML={{
|
<h5
|
||||||
__html: diffText(currentData.title, prevData?.title),
|
dangerouslySetInnerHTML={{
|
||||||
}}
|
__html: diffText(newData.title, oldData?.title),
|
||||||
className="mb-3"
|
}}
|
||||||
/>
|
className="mb-3"
|
||||||
<div className="mb-4">
|
/>
|
||||||
{tag.map((item) => {
|
)}
|
||||||
return (
|
{objectType === 'question' && (
|
||||||
<Tag
|
<div className="mb-4">
|
||||||
key={item.slug_name}
|
{tag.map((item) => {
|
||||||
className="me-1"
|
return (
|
||||||
data={item}
|
<Tag
|
||||||
textClassName={`d-inline-block review-text-${item.state}`}
|
key={item.slug_name}
|
||||||
/>
|
className="me-1"
|
||||||
);
|
data={item}
|
||||||
})}
|
textClassName={`d-inline-block review-text-${item.state}`}
|
||||||
</div>
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{objectType === 'tag' && (
|
||||||
|
<div className="mb-4">
|
||||||
|
{`/tags/${
|
||||||
|
newData?.main_tag_slug_name
|
||||||
|
? diffText(
|
||||||
|
newData.main_tag_slug_name,
|
||||||
|
oldData?.main_tag_slug_name,
|
||||||
|
)
|
||||||
|
: diffText(newData.slug_name, oldData?.slug_name)
|
||||||
|
}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: diffText(currentData.content, prevData?.content),
|
__html: diffText(newData.original_text, oldData?.original_text),
|
||||||
}}
|
}}
|
||||||
className="pre-line"
|
className="pre-line"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,10 +10,19 @@ interface Props {
|
||||||
data: any;
|
data: any;
|
||||||
time: number;
|
time: number;
|
||||||
preFix: string;
|
preFix: string;
|
||||||
|
isLogged: boolean;
|
||||||
|
timelinePath: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Index: FC<Props> = ({ data, time, preFix, className = '' }) => {
|
const Index: FC<Props> = ({
|
||||||
|
data,
|
||||||
|
time,
|
||||||
|
preFix,
|
||||||
|
isLogged,
|
||||||
|
timelinePath,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={classnames('d-flex', className)}>
|
<div className={classnames('d-flex', className)}>
|
||||||
{data?.status !== 'deleted' ? (
|
{data?.status !== 'deleted' ? (
|
||||||
|
@ -62,7 +71,18 @@ const Index: FC<Props> = ({ data, time, preFix, className = '' }) => {
|
||||||
{formatCount(data?.rank)}
|
{formatCount(data?.rank)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{time && <FormatTime time={time} preFix={preFix} />}
|
{time &&
|
||||||
|
(isLogged ? (
|
||||||
|
<Link to={timelinePath}>
|
||||||
|
<FormatTime
|
||||||
|
time={time}
|
||||||
|
preFix={preFix}
|
||||||
|
className="link-secondary"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<FormatTime time={time} preFix={preFix} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { memo, FC, useEffect, useRef } from 'react';
|
import { memo, FC, useEffect, useRef } from 'react';
|
||||||
import { Row, Col, Button } from 'react-bootstrap';
|
import { Row, Col, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Actions,
|
Actions,
|
||||||
|
@ -22,12 +23,14 @@ interface Props {
|
||||||
/** is author */
|
/** is author */
|
||||||
isAuthor: boolean;
|
isAuthor: boolean;
|
||||||
questionTitle: string;
|
questionTitle: string;
|
||||||
|
isLogged: boolean;
|
||||||
callback: (type: string) => void;
|
callback: (type: string) => void;
|
||||||
}
|
}
|
||||||
const Index: FC<Props> = ({
|
const Index: FC<Props> = ({
|
||||||
aid,
|
aid,
|
||||||
data,
|
data,
|
||||||
isAuthor,
|
isAuthor,
|
||||||
|
isLogged,
|
||||||
questionTitle = '',
|
questionTitle = '',
|
||||||
callback,
|
callback,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -119,7 +122,17 @@ const Index: FC<Props> = ({
|
||||||
data={data?.update_user_info}
|
data={data?.update_user_info}
|
||||||
time={Number(data.update_time)}
|
time={Number(data.update_time)}
|
||||||
preFix={t('edit')}
|
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 fs-14"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<FormatTime
|
<FormatTime
|
||||||
time={Number(data.update_time)}
|
time={Number(data.update_time)}
|
||||||
|
@ -133,6 +146,8 @@ const Index: FC<Props> = ({
|
||||||
data={data?.user_info}
|
data={data?.user_info}
|
||||||
time={Number(data.create_time)}
|
time={Number(data.create_time)}
|
||||||
preFix={t('answered')}
|
preFix={t('answered')}
|
||||||
|
isLogged={isLogged}
|
||||||
|
timelinePath={`/posts/${data.question_id}/${data.id}/timeline`}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -18,10 +18,11 @@ import { following } from '@/services';
|
||||||
interface Props {
|
interface Props {
|
||||||
data: any;
|
data: any;
|
||||||
hasAnswer: boolean;
|
hasAnswer: boolean;
|
||||||
|
isLogged: boolean;
|
||||||
initPage: (type: string) => void;
|
initPage: (type: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
keyPrefix: 'question_detail',
|
keyPrefix: 'question_detail',
|
||||||
});
|
});
|
||||||
|
@ -133,7 +134,17 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
||||||
data={data?.user_info}
|
data={data?.user_info}
|
||||||
time={data.edit_time}
|
time={data.edit_time}
|
||||||
preFix={t('edit')}
|
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 fs-14"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<FormatTime
|
<FormatTime
|
||||||
time={data.edit_time}
|
time={data.edit_time}
|
||||||
|
@ -147,6 +158,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
||||||
data={data?.user_info}
|
data={data?.user_info}
|
||||||
time={data.create_time}
|
time={data.create_time}
|
||||||
preFix={t('asked')}
|
preFix={t('asked')}
|
||||||
|
isLogged={isLogged}
|
||||||
|
timelinePath={`/posts/${data.id}/timeline`}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -39,6 +39,7 @@ const Index = () => {
|
||||||
const { setUsers } = usePageUsers();
|
const { setUsers } = usePageUsers();
|
||||||
const userInfo = loggedUserInfoStore((state) => state.user);
|
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||||
const isAuthor = userInfo?.username === question?.user_info?.username;
|
const isAuthor = userInfo?.username === question?.user_info?.username;
|
||||||
|
const isLogged = Boolean(userInfo?.access_token);
|
||||||
const requestAnswers = async () => {
|
const requestAnswers = async () => {
|
||||||
const res = await getAnswers({
|
const res = await getAnswers({
|
||||||
order: order === 'updated' ? order : 'default',
|
order: order === 'updated' ? order : 'default',
|
||||||
|
@ -126,6 +127,7 @@ const Index = () => {
|
||||||
data={question}
|
data={question}
|
||||||
initPage={initPage}
|
initPage={initPage}
|
||||||
hasAnswer={answers.count > 0}
|
hasAnswer={answers.count > 0}
|
||||||
|
isLogged={isLogged}
|
||||||
/>
|
/>
|
||||||
{answers.count > 0 && (
|
{answers.count > 0 && (
|
||||||
<>
|
<>
|
||||||
|
@ -139,6 +141,7 @@ const Index = () => {
|
||||||
questionTitle={question?.title || ''}
|
questionTitle={question?.title || ''}
|
||||||
isAuthor={isAuthor}
|
isAuthor={isAuthor}
|
||||||
callback={initPage}
|
callback={initPage}
|
||||||
|
isLogged={isLogged}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -12,8 +12,11 @@ import {
|
||||||
saveSynonymsTags,
|
saveSynonymsTags,
|
||||||
deleteTag,
|
deleteTag,
|
||||||
} from '@/services';
|
} from '@/services';
|
||||||
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
|
|
||||||
const TagIntroduction = () => {
|
const TagIntroduction = () => {
|
||||||
|
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||||
|
const isLogged = Boolean(userInfo?.access_token);
|
||||||
const [isEdit, setEditState] = useState(false);
|
const [isEdit, setEditState] = useState(false);
|
||||||
const { tagName } = useParams();
|
const { tagName } = useParams();
|
||||||
const { data: tagInfo } = useTagInfo({ name: tagName });
|
const { data: tagInfo } = useTagInfo({ name: tagName });
|
||||||
|
@ -125,6 +128,13 @@ const TagIntroduction = () => {
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{isLogged && (
|
||||||
|
<Link
|
||||||
|
to={`/tags/${tagInfo?.tag_id}/timeline`}
|
||||||
|
className="link-secondary btn-no-border p-0 fs-14 ms-3">
|
||||||
|
{t('history')}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
|
|
|
@ -6,113 +6,43 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { Icon, BaseUserCard, DiffContent, FormatTime } from '@/components';
|
import { Icon, BaseUserCard, DiffContent, FormatTime } from '@/components';
|
||||||
import { TIMELINE_NORMAL_ACTIVITY_TYPE } from '@/common/constants';
|
import { TIMELINE_NORMAL_ACTIVITY_TYPE } from '@/common/constants';
|
||||||
import * as Type from '@/common/interface';
|
import * as Type from '@/common/interface';
|
||||||
|
import { getTimelineDetail } from '@/services';
|
||||||
const data1 = {
|
|
||||||
title: '不是管理员,提一个问题看看能不能编辑resserved tag?',
|
|
||||||
tags: [
|
|
||||||
{
|
|
||||||
display_name: 'bug',
|
|
||||||
slug_name: 'bug',
|
|
||||||
recommend: true,
|
|
||||||
reserved: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display_name: '黄马甲',
|
|
||||||
slug_name: '黄马甲',
|
|
||||||
recommend: false,
|
|
||||||
reserved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display_name: 'go',
|
|
||||||
slug_name: 'go',
|
|
||||||
recommend: false,
|
|
||||||
reserved: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
content: `# 前言
|
|
||||||
手写 Promise 是面试的时候大家都逃避的送命题,在学些了解后发现通过实现源码更能将新一代的异步方案理解的通透,知其然知其所以然的运用。
|
|
||||||
|
|
||||||
如果直接将源码贴到此处势必不能有更大的收获,下面就按实现版本来看做简要分析。
|
|
||||||
|
|
||||||
## 回顾 Promise
|
|
||||||
Promise 是 CommonJS 提出来的这一种规范,有多个版本,在 ES6 当中已经纳入规范,原生支持 Promise 对象,非 ES6 环境可以用类似 Bluebird、Q 这类库来支持。
|
|
||||||
|
|
||||||
Promise 可以将回调变成链式调用写法,流程更加清晰,代码更加优雅,还可以批量处理异步任务。
|
|
||||||
|
|
||||||
简单归纳下 Promise:三个状态、两个过程、一个方法,快速记忆方法:3-2-1
|
|
||||||
|
|
||||||
三个状态:pending、fulfilled、rejected
|
|
||||||
|
|
||||||
两个过程:
|
|
||||||
|
|
||||||
* pending → fulfilled(resolve)
|
|
||||||
* pending → rejected(reject)
|
|
||||||
一个方法:then
|
|
||||||
|
|
||||||
当然还有其他概念,如 catch、 Promise.all/race/allSettled。`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data2 = {
|
|
||||||
title: '提一个问题看看能不能编辑 resserved tag?',
|
|
||||||
tags: [
|
|
||||||
{
|
|
||||||
display_name: 'discussion',
|
|
||||||
slug_name: 'discussion',
|
|
||||||
recommend: true,
|
|
||||||
reserved: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display_name: '黄马甲',
|
|
||||||
slug_name: '黄马甲',
|
|
||||||
recommend: false,
|
|
||||||
reserved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
display_name: 'go',
|
|
||||||
slug_name: 'go',
|
|
||||||
recommend: false,
|
|
||||||
reserved: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
content: `# 前言
|
|
||||||
手写 Promise 是面试的时候大家都逃避的送命题,在学些了解后发现通过实现源码更能将新一代的异步方案理解的通透,知用。
|
|
||||||
|
|
||||||
## 增加的titlte
|
|
||||||
|
|
||||||
如果直接将源码贴到此处势必不能有更大的收获。
|
|
||||||
|
|
||||||
## 回顾 Promise
|
|
||||||
Promise 是 CommonJS 规范,并且有多个版本,在 ES6 当中已经纳入规范,原生支持 Promise 对象,非 ES6 环境可以用类似 Bluebird、Q 这类库来支持。
|
|
||||||
|
|
||||||
Promise 可以将回调变成链式调用写法,流程更加清晰,代码更加优雅,还可以批量处理异步任务。
|
|
||||||
|
|
||||||
简单归纳下 Promise:三个状态、两个过程、一个方法,快速记忆方法:3-2-1
|
|
||||||
|
|
||||||
两个过程:
|
|
||||||
|
|
||||||
* pending → fulfilled(resolve)
|
|
||||||
* pending → rejected(reject)
|
|
||||||
一个方法:then
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: Type.TimelineItem;
|
data: Type.TimelineItem;
|
||||||
objectInfo: Type.TimelineObject;
|
objectInfo: Type.TimelineObject;
|
||||||
source: 'question' | 'answer' | 'tag';
|
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
revisionList: Type.TimelineItem[];
|
||||||
}
|
}
|
||||||
const Index: FC<Props> = ({
|
const Index: FC<Props> = ({ data, isAdmin, objectInfo, revisionList }) => {
|
||||||
data,
|
|
||||||
isAdmin,
|
|
||||||
source = 'question',
|
|
||||||
objectInfo,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const handleItemClick = () => {
|
const [detailData, setDetailData] = useState({
|
||||||
|
new_revision: {},
|
||||||
|
old_revision: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleItemClick = async (id) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
const revisionItem = revisionList?.find((v) => v.revision_id === id);
|
||||||
|
let oldId;
|
||||||
|
if (revisionList?.length > 0 && revisionItem) {
|
||||||
|
const idIndex = revisionList.indexOf(revisionItem) || 0;
|
||||||
|
if (idIndex === revisionList.length - 1) {
|
||||||
|
oldId = 0;
|
||||||
|
} else {
|
||||||
|
oldId = revisionList[idIndex + 1].revision_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await getTimelineDetail({
|
||||||
|
new_revision_id: id,
|
||||||
|
old_revision_id: oldId,
|
||||||
|
});
|
||||||
|
setDetailData(res);
|
||||||
|
}
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -126,9 +56,10 @@ const Index: FC<Props> = ({
|
||||||
data.activity_type === 'edited' ||
|
data.activity_type === 'edited' ||
|
||||||
data.activity_type === 'asked' ||
|
data.activity_type === 'asked' ||
|
||||||
data.activity_type === 'created' ||
|
data.activity_type === 'created' ||
|
||||||
(source === 'answer' && data.activity_type === 'answered')) && (
|
(objectInfo.object_type === 'answer' &&
|
||||||
|
data.activity_type === 'answered')) && (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleItemClick}
|
onClick={() => handleItemClick(data.revision_id)}
|
||||||
variant="link"
|
variant="link"
|
||||||
className="text-body p-0 btn-no-border">
|
className="text-body p-0 btn-no-border">
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -139,24 +70,25 @@ const Index: FC<Props> = ({
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{data.activity_type === 'accept' && (
|
{data.activity_type === 'accept' && (
|
||||||
<Link to={`/question/${objectInfo.question_id}`}>
|
<Link to={`/questions/${objectInfo.question_id}`}>
|
||||||
{t(data.activity_type)}
|
{t(data.activity_type)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{source === 'question' && data.activity_type === 'answered' && (
|
{data.object_type === 'question' &&
|
||||||
<Link
|
data.activity_type === 'answered' && (
|
||||||
to={`/question/${objectInfo.question_id}/${objectInfo.answer_id}`}>
|
<Link
|
||||||
{t(data.activity_type)}
|
to={`/questions/${objectInfo.question_id}/${data.object_id}`}>
|
||||||
</Link>
|
{t(data.activity_type)}
|
||||||
)}
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
{data.activity_type === 'commented' && (
|
{data.activity_type === 'commented' && (
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={
|
||||||
data.object_type === 'answer'
|
objectInfo.object_type === 'answer'
|
||||||
? `/question/${objectInfo.question_id}/${objectInfo.answer_id}?commentId=${data.object_id}`
|
? `/questions/${objectInfo.question_id}/${objectInfo.answer_id}?commentId=${data.object_id}`
|
||||||
: `/question/${objectInfo.question_id}?commentId=${data.object_id}`
|
: `/questions/${objectInfo.question_id}?commentId=${data.object_id}`
|
||||||
}>
|
}>
|
||||||
{t(data.activity_type)}
|
{t(data.activity_type)}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -167,7 +99,7 @@ const Index: FC<Props> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data.cancelled && (
|
{data.cancelled && (
|
||||||
<div className="text-danger"> {t('cancelled')}</div>
|
<div className="text-danger">{t('cancelled')}</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -192,7 +124,11 @@ const Index: FC<Props> = ({
|
||||||
<td colSpan={5} className="p-0 py-5">
|
<td colSpan={5} className="p-0 py-5">
|
||||||
<Row className="justify-content-center">
|
<Row className="justify-content-center">
|
||||||
<Col xxl={8}>
|
<Col xxl={8}>
|
||||||
<DiffContent currentData={data1} prevData={data2} />
|
<DiffContent
|
||||||
|
objectType={objectInfo.object_type}
|
||||||
|
newData={detailData?.new_revision}
|
||||||
|
oldData={detailData?.old_revision}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,113 +1,70 @@
|
||||||
import { FC } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { Container, Row, Col, Form, Table } from 'react-bootstrap';
|
import { Container, Row, Col, Form, Table } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { useTimelineData } from '@/services';
|
import { useTimelineData } from '@/services';
|
||||||
|
import { PageTitle } from '@/components';
|
||||||
|
|
||||||
import HistoryItem from './components/Item';
|
import HistoryItem from './components/Item';
|
||||||
|
|
||||||
// const list = [
|
|
||||||
// {
|
|
||||||
// activity_id: 1,
|
|
||||||
// revision_id: 1,
|
|
||||||
// created_at: 1669084579,
|
|
||||||
// activity_type: 'deleted',
|
|
||||||
// username: 'John Doe',
|
|
||||||
// user_display_name: 'John Doe',
|
|
||||||
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
|
|
||||||
// object_id: '1',
|
|
||||||
// object_type: 'question',
|
|
||||||
// cancelled: false,
|
|
||||||
// cancelled_at: null,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// activity_id: 2,
|
|
||||||
// revision_id: 2,
|
|
||||||
// created_at: 1669084579,
|
|
||||||
// activity_type: 'undeleted',
|
|
||||||
// username: 'John Doe2',
|
|
||||||
// user_display_name: 'John Doe2',
|
|
||||||
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
|
|
||||||
// object_id: '2',
|
|
||||||
// object_type: 'question',
|
|
||||||
// cancelled: false,
|
|
||||||
// cancelled_at: null,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// activity_id: 3,
|
|
||||||
// revision_id: 3,
|
|
||||||
// created_at: 1669084579,
|
|
||||||
// activity_type: 'downvote',
|
|
||||||
// username: 'johndoe3',
|
|
||||||
// user_display_name: 'John Doe3',
|
|
||||||
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
|
|
||||||
// object_id: '3',
|
|
||||||
// object_type: 'question',
|
|
||||||
// cancelled: true,
|
|
||||||
// cancelled_at: 1637021579,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// activity_id: 4,
|
|
||||||
// revision_id: 4,
|
|
||||||
// created_at: 1669084579,
|
|
||||||
// activity_type: 'rollback',
|
|
||||||
// username: 'johndoe4',
|
|
||||||
// user_display_name: 'John Doe4',
|
|
||||||
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
|
|
||||||
// object_id: '4',
|
|
||||||
// object_type: 'question',
|
|
||||||
// cancelled: false,
|
|
||||||
// cancelled_at: null,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// activity_id: 5,
|
|
||||||
// revision_id: 5,
|
|
||||||
// created_at: 1669084579,
|
|
||||||
// activity_type: 'edited',
|
|
||||||
// username: 'johndoe4',
|
|
||||||
// user_display_name: 'John Doe4',
|
|
||||||
// object_id: '5',
|
|
||||||
// object_type: 'question',
|
|
||||||
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
|
|
||||||
// cancelled: false,
|
|
||||||
// cancelled_at: null,
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// const object_info = {
|
|
||||||
// title: '问题标题,当回答时也是问题标题,当为 tag 时是 slug_name',
|
|
||||||
// object_type: 'question', // question/answer/tag
|
|
||||||
// question_id: 'xxxxxxxxxxxxxxxxxxx',
|
|
||||||
// answer_id: 'xxxxxxxxxxxxxxxx',
|
|
||||||
// };
|
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
||||||
|
const { qid = '', aid = '', tid = '' } = useParams();
|
||||||
const { is_admin } = loggedUserInfoStore((state) => state.user);
|
const { is_admin } = loggedUserInfoStore((state) => state.user);
|
||||||
|
const [showVotes, setShowVotes] = useState(false);
|
||||||
|
|
||||||
const { data: timelineData } = useTimelineData({
|
const { data: timelineData } = useTimelineData({
|
||||||
object_id: '10010000000000001',
|
object_id: tid || aid || qid,
|
||||||
object_type: 'question',
|
show_vote: showVotes,
|
||||||
show_vote: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('timelineData=', timelineData);
|
const handleSwitch = (bol: boolean) => {
|
||||||
|
setShowVotes(bol);
|
||||||
|
};
|
||||||
|
|
||||||
|
let linkUrl = '';
|
||||||
|
if (timelineData?.object_info.object_type === 'question') {
|
||||||
|
linkUrl = `/questions/${timelineData?.object_info.question_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timelineData?.object_info.object_type === 'answer') {
|
||||||
|
linkUrl = `/questions/${timelineData?.object_info.question_id}/${timelineData?.object_info.answer_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timelineData?.object_info.object_type === 'tag') {
|
||||||
|
linkUrl = `/tags/${timelineData?.object_info.title}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const revisionList =
|
||||||
|
timelineData?.timeline?.filter((item) => item.revision_id > 0) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="py-3">
|
<Container className="py-3">
|
||||||
|
<PageTitle
|
||||||
|
title={
|
||||||
|
timelineData?.object_info.object_type === 'tag'
|
||||||
|
? `Timeline for tag ${timelineData?.object_info.title}`
|
||||||
|
: `Timeline for ${timelineData?.object_info.title}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Row className="py-3 justify-content-center">
|
<Row className="py-3 justify-content-center">
|
||||||
<Col xxl={10}>
|
<Col xxl={10}>
|
||||||
<h5 className="mb-4">
|
<h5 className="mb-4">
|
||||||
{t('title')} <Link to="/">{timelineData?.object_info?.title}</Link>
|
{t('title')}{' '}
|
||||||
|
<Link to={linkUrl}>{timelineData?.object_info?.title}</Link>
|
||||||
</h5>
|
</h5>
|
||||||
<Form.Check
|
{timelineData?.object_info.object_type !== 'tag' && (
|
||||||
className="mb-4"
|
<Form.Check
|
||||||
type="switch"
|
className="mb-4"
|
||||||
id="custom-switch"
|
type="switch"
|
||||||
label={t('show_votes')}
|
id="custom-switch"
|
||||||
/>
|
label={t('show_votes')}
|
||||||
|
checked={showVotes}
|
||||||
|
onChange={(e) => handleSwitch(e.target.checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Table hover>
|
<Table hover>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -123,9 +80,9 @@ const Index: FC = () => {
|
||||||
<HistoryItem
|
<HistoryItem
|
||||||
data={item}
|
data={item}
|
||||||
objectInfo={timelineData?.object_info}
|
objectInfo={timelineData?.object_info}
|
||||||
key={item.revision_id}
|
key={item.activity_id}
|
||||||
isAdmin={is_admin}
|
isAdmin={is_admin}
|
||||||
source="question"
|
revisionList={revisionList}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -202,7 +202,21 @@ const routes: RouteNode[] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/history',
|
path: '/posts/:qid/timeline',
|
||||||
|
page: 'pages/Timeline',
|
||||||
|
guard: async () => {
|
||||||
|
return guard.logged();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/posts/:qid/:aid/timeline',
|
||||||
|
page: 'pages/Timeline',
|
||||||
|
guard: async () => {
|
||||||
|
return guard.logged();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/tags/:tid/timeline',
|
||||||
page: 'pages/Timeline',
|
page: 'pages/Timeline',
|
||||||
guard: async () => {
|
guard: async () => {
|
||||||
return guard.logged();
|
return guard.logged();
|
||||||
|
|
|
@ -17,3 +17,14 @@ export const useTimelineData = (params: Type.TimelineReq) => {
|
||||||
mutate,
|
mutate,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTimelineDetail = (params: {
|
||||||
|
new_revision_id: string;
|
||||||
|
old_revision_id: string;
|
||||||
|
}) => {
|
||||||
|
return request.get(
|
||||||
|
`/answer/api/v1/activity/timeline/detail?${qs.stringify(params, {
|
||||||
|
skipNulls: true,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -176,7 +176,7 @@ function diffText(newText: string, oldText: string): string {
|
||||||
?.replace(/<iframe/gi, '<iframe')
|
?.replace(/<iframe/gi, '<iframe')
|
||||||
?.replace(/<input/gi, '<input');
|
?.replace(/<input/gi, '<input');
|
||||||
}
|
}
|
||||||
const diff = Diff.diffChars(newText, oldText);
|
const diff = Diff.diffChars(oldText, newText);
|
||||||
const result = diff.map((part) => {
|
const result = diff.map((part) => {
|
||||||
if (part.added) {
|
if (part.added) {
|
||||||
return `<span class="review-text-add">${part.value}</span>`;
|
return `<span class="review-text-add">${part.value}</span>`;
|
||||||
|
|
Loading…
Reference in New Issue