Merge branch 'test' of github.com:answerdev/answer into test

This commit is contained in:
aichy126
2023-06-01 11:26:04 +08:00
32 changed files with 305 additions and 157 deletions
-8
View File
@@ -1183,14 +1183,6 @@ ui:
accepted: Accepted
answered: answered
asked: asked
rank_type:
upvote: upvote
upvoted: upvoted
downvote: downvote
downvoted: downvoted
edit: edit
accept: accept
accepted: accepted
downvoted: downvoted
mod_short: Mod
mod_long: Moderators
+2 -2
View File
@@ -17,12 +17,12 @@ import (
type AvatarMiddleware struct {
serviceConfig *service_config.ServiceConfig
uploaderService *uploader.UploaderService
uploaderService uploader.UploaderService
}
// NewAvatarMiddleware new auth user middleware
func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig,
uploaderService *uploader.UploaderService,
uploaderService uploader.UploaderService,
) *AvatarMiddleware {
return &AvatarMiddleware{
serviceConfig: serviceConfig,
+2 -2
View File
@@ -21,11 +21,11 @@ const (
// UploadController upload controller
type UploadController struct {
uploaderService *uploader.UploaderService
uploaderService uploader.UploaderService
}
// NewUploadController new controller
func NewUploadController(uploaderService *uploader.UploaderService) *UploadController {
func NewUploadController(uploaderService uploader.UploaderService) *UploadController {
return &UploadController{
uploaderService: uploaderService,
}
+2 -2
View File
@@ -24,7 +24,7 @@ type UserController struct {
userService *service.UserService
authService *auth.AuthService
actionService *action.CaptchaService
uploaderService *uploader.UploaderService
uploaderService uploader.UploaderService
emailService *export.EmailService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
}
@@ -35,7 +35,7 @@ func NewUserController(
userService *service.UserService,
actionService *action.CaptchaService,
emailService *export.EmailService,
uploaderService *uploader.UploaderService,
uploaderService uploader.UploaderService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
) *UserController {
return &UserController{
+17 -10
View File
@@ -51,29 +51,36 @@ var (
}
)
// UploaderService user service
type UploaderService struct {
type UploaderService interface {
UploadAvatarFile(ctx *gin.Context) (url string, err error)
AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (avatarFile []byte, err error)
UploadPostFile(ctx *gin.Context) (url string, err error)
UploadBrandingFile(ctx *gin.Context) (url string, err error)
}
// uploaderService uploader service
type uploaderService struct {
serviceConfig *service_config.ServiceConfig
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewUploaderService new upload service
func NewUploaderService(serviceConfig *service_config.ServiceConfig,
siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService {
siteInfoService *siteinfo_common.SiteInfoCommonService) UploaderService {
for _, subPath := range subPathList {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath))
if err != nil {
panic(err)
}
}
return &UploaderService{
return &uploaderService{
serviceConfig: serviceConfig,
siteInfoService: siteInfoService,
}
}
// UploadAvatarFile upload avatar file
func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
func (us *uploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
if err != nil {
return "", err
@@ -98,7 +105,7 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err e
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (
func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (
avatarfile []byte, err error) {
if size > 1024 {
size = 1024
@@ -151,7 +158,7 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
return buf.Bytes(), nil
}
func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
func (us *uploaderService) UploadPostFile(ctx *gin.Context) (
url string, err error) {
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
if err != nil {
@@ -177,7 +184,7 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
func (us *uploaderService) UploadBrandingFile(ctx *gin.Context) (
url string, err error) {
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
if err != nil {
@@ -204,7 +211,7 @@ func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
func (us *uploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
url string, err error) {
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
@@ -230,7 +237,7 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead
return url, nil
}
func (us *UploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) (
func (us *uploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) (
url string, err error) {
_ = plugin.CallStorage(func(fn plugin.Storage) error {
resp := fn.UploadFile(ctx, source)
+6 -17
View File
@@ -1,28 +1,17 @@
.page-main {
max-width: 70%;
}
.page-right-side {
flex: none;
width: 30%;
}
.line {
position: absolute;
top: 0;
right: 12px;
width: 1px;
height: 100%;
background-color: var(--bs-gray-300);
min-height: calc(100vh - 62px - 74px);
}
// lg
@media screen and (max-width: 1199.9px) {
.page-main {
max-width: 100%;
}
.page-right-side {
width: 100%;
}
}
// md
@media screen and (max-width: 991.9px) {
.line {
display: none;
}
}
+4 -1
View File
@@ -27,12 +27,15 @@ const Index: FC<IProps> = ({ avatar, size, className, searchStr = '' }) => {
url = `${avatar.custom}?${searchStr}`;
}
const roundedCls =
className && className.indexOf('rounded') !== -1 ? '' : 'rounded';
return (
<img
src={url || DefaultAvatar}
width={size}
height={size}
className={classNames('rounded', className)}
className={classNames(roundedCls, className)}
alt=""
/>
);
+10 -6
View File
@@ -94,13 +94,17 @@ export function htmlRender(el: HTMLElement | null) {
el.querySelectorAll('.language-mermaid').forEach((pre) => {
const flag = Date.now();
mermaid.render(`theGraph${flag}`, pre.textContent, function (svgCode) {
const p = document.createElement('p');
p.className = 'text-center';
p.innerHTML = svgCode;
mermaid.render(
`theGraph${flag}`,
pre.textContent || '',
function (svgCode) {
const p = document.createElement('p');
p.className = 'text-center';
p.innerHTML = svgCode;
pre.parentNode?.replaceChild(p, pre);
});
pre.parentNode?.replaceChild(p, pre);
},
);
});
});
import('katex/contrib/auto-render/auto-render').then(
+22 -1
View File
@@ -1,7 +1,16 @@
#sideNav {
.nav-wrap {
position: sticky;
width: auto;
top: 62px;
box-sizing: border-box;
max-height: calc(100vh - 74px - 62px - 24px);
overflow-y: auto;
margin-bottom: 8px;
}
.nav {
max-width: 172px;
max-width: calc(100% - 24px);
}
.nav-link {
color: rgba(0, 0, 0, 0.65);
@@ -14,6 +23,15 @@
color: black;
background-color: var(--bs-gray-200);
}
.side-nav-right-line {
position: absolute;
top: 0;
right: 12px;
width: 1px;
height: 100%;
background-color: var(--bs-gray-300);
min-height: calc(100vh - 62px - 74px);
}
}
@media screen and (max-width: 991.9px) {
@@ -21,5 +39,8 @@
.nav {
max-width: 100%;
}
.side-nav-right-line {
display: none;
}
}
}
+43 -41
View File
@@ -20,53 +20,55 @@ const Index: FC = () => {
lg={3}
md={12}
className={classnames(
'pt-4 position-relative',
visible ? 'fade-in' : 'd-none d-lg-block',
'position-relative',
visible ? '' : 'd-none d-lg-block',
)}
id="sideNav">
<Nav variant="pills" className="flex-column">
<NavLink
to="/questions"
className={({ isActive }) =>
isActive || pathname === '/' ? 'nav-link active' : 'nav-link'
}>
<Icon name="question-circle-fill" className="me-2" />
<span>{t('header.nav.question')}</span>
</NavLink>
<div className="nav-wrap pt-4">
<Nav variant="pills" className="flex-column">
<NavLink
to="/questions"
className={({ isActive }) =>
isActive || pathname === '/' ? 'nav-link active' : 'nav-link'
}>
<Icon name="question-circle-fill" className="me-2" />
<span>{t('header.nav.question')}</span>
</NavLink>
<NavLink to="/tags" className="nav-link">
<Icon name="tags-fill" className="me-2" />
<span>{t('header.nav.tag')}</span>
</NavLink>
<NavLink to="/tags" className="nav-link">
<Icon name="tags-fill" className="me-2" />
<span>{t('header.nav.tag')}</span>
</NavLink>
<NavLink to="/users" className="nav-link">
<Icon name="people-fill" className="me-2" />
<span>{t('header.nav.user')}</span>
</NavLink>
<NavLink to="/users" className="nav-link">
<Icon name="people-fill" className="me-2" />
<span>{t('header.nav.user')}</span>
</NavLink>
{can_revision || userInfo?.role_id === 2 ? (
<>
<div className="py-2 px-3 mt-3 small fw-bold">
{t('header.nav.moderation')}
</div>
{can_revision && (
<NavLink to="/review" className="nav-link">
<span>{t('header.nav.review')}</span>
<span className="float-end">
{revision > 99 ? '99+' : revision > 0 ? revision : ''}
</span>
</NavLink>
)}
{can_revision || userInfo?.role_id === 2 ? (
<>
<div className="py-2 px-3 mt-3 small fw-bold">
{t('header.nav.moderation')}
</div>
{can_revision && (
<NavLink to="/review" className="nav-link">
<span>{t('header.nav.review')}</span>
<span className="float-end">
{revision > 99 ? '99+' : revision > 0 ? revision : ''}
</span>
</NavLink>
)}
{userInfo?.role_id === 2 ? (
<NavLink to="/admin" className="nav-link">
{t('header.nav.admin')}
</NavLink>
) : null}
</>
) : null}
</Nav>
<div className="line" />
{userInfo?.role_id === 2 ? (
<NavLink to="/admin" className="nav-link">
{t('header.nav.admin')}
</NavLink>
) : null}
</>
) : null}
</Nav>
</div>
<div className="side-nav-right-line" />
</Col>
);
};
+1 -1
View File
@@ -322,7 +322,7 @@ const Ask = () => {
<div className="pt-4 mb-5">
<h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3>
<Row>
<Col className="flex-auto">
<Col className="page-main flex-auto">
<Form noValidate onSubmit={handleSubmit}>
{isEdit && (
<Form.Group controlId="revision" className="mb-3">
@@ -0,0 +1,10 @@
.people-dropdown {
.dropdown-menu {
min-width: 15rem;
}
.dropdown-item.active {
color: #212529;
background-color: #e9ecef;
}
}
@@ -1,4 +1,4 @@
import { FC, memo, useEffect, useState } from 'react';
import { FC, useEffect, useState } from 'react';
import { Dropdown, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
@@ -6,21 +6,31 @@ import { loggedUserInfoStore } from '@/stores';
import { userSearchByName } from '@/services';
import { Avatar } from '@/components';
import * as Type from '@/common/interface';
import './PeopleDropdown.scss';
interface Props {
selectedPeople: Type.UserInfoBase[] | undefined;
onSelect: (people: Type.UserInfoBase) => void;
visible?: boolean;
}
const Index: FC<Props> = ({ selectedPeople = [], onSelect }) => {
const Index: FC<Props> = ({
selectedPeople = [],
visible = false,
onSelect,
}) => {
const { user: currentUser } = loggedUserInfoStore();
const { t } = useTranslation('translation', {
keyPrefix: 'invite_to_answer',
});
const [toggleState, setToggleState] = useState(false);
const [peopleList, setPeopleList] = useState<Type.UserInfoBase[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [searchValue, setSearchValue] = useState('');
const filterAndSetPeople = (source) => {
if (!toggleState) {
return;
}
const filteredPeople: Type.UserInfoBase[] = [];
source.forEach((p) => {
if (currentUser && currentUser.username === p.username) {
@@ -34,56 +44,109 @@ const Index: FC<Props> = ({ selectedPeople = [], onSelect }) => {
setPeopleList(filteredPeople);
};
const searchPeople = (evt) => {
const name = evt.target.value;
if (!name) {
const searchPeople = (s) => {
if (!s) {
setPeopleList([]);
return;
}
userSearchByName(name).then((resp) => {
userSearchByName(s).then((resp) => {
filterAndSetPeople(resp);
});
};
const handleSearch = (evt) => {
const s = evt.target.value;
setSearchValue(s);
searchPeople(s);
};
const updateCurrentIndex = (pl: number) => {
let curIndex = currentIndex;
if (currentIndex >= pl) {
curIndex = Math.max(pl - 1, 0);
}
setCurrentIndex(curIndex);
};
const handleSelect = (idx) => {
if (idx < 0 || idx >= peopleList.length) {
return;
}
const people = peopleList[idx];
if (people) {
updateCurrentIndex(peopleList.length - 1);
onSelect(people);
}
};
const handleKeyDown = (evt) => {
evt.stopPropagation();
if (!peopleList?.length) {
return;
}
const { keyCode } = evt;
if (keyCode === 38 && currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
}
if (keyCode === 40 && currentIndex < peopleList.length - 1) {
setCurrentIndex(currentIndex + 1);
}
if (keyCode === 13 && currentIndex > -1) {
evt.preventDefault();
handleSelect(currentIndex);
}
};
useEffect(() => {
filterAndSetPeople(peopleList);
}, [selectedPeople]);
return (
useEffect(() => {
searchPeople(searchValue);
}, [toggleState]);
useEffect(() => {
if (!visible && toggleState) {
setToggleState(false);
}
}, [visible]);
return visible ? (
<Dropdown
className="d-inline-flex"
show={toggleState}
className="d-inline-flex people-dropdown"
align="end"
onSelect={handleSelect}
onKeyDown={handleKeyDown}
onToggle={setToggleState}>
<Dropdown.Toggle
className="m-1 no-toggle"
size="sm"
variant="outline-secondary">
{t('add')} +
<span className="me-1">+</span>
{t('add')}
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Menu show={toggleState}>
<Dropdown.Header className="px-2 py-0">
<Form.Control
autoFocus
placeholder={t('search')}
onChange={searchPeople}
/>
{toggleState ? (
<Form.Control
autoFocus
placeholder={t('search')}
value={searchValue}
onChange={handleSearch}
/>
) : null}
</Dropdown.Header>
{peopleList.map((p, idx) => {
return (
<Dropdown.Item
key={p.username}
eventKey={idx}
active={idx === currentIndex}
className={idx === 0 ? 'mt-2' : ''}>
<div className="d-flex align-items-center text-nowrap">
<Avatar avatar={p.avatar} size="24" />
<Avatar avatar={p.avatar} size="24" className="rounded-1" />
<span className="mx-2">{p.display_name}</span>
<small className="text-secondary">@{p.username}</small>
</div>
@@ -92,7 +155,7 @@ const Index: FC<Props> = ({ selectedPeople = [], onSelect }) => {
})}
</Dropdown.Menu>
</Dropdown>
);
) : null;
};
export default memo(Index);
export default Index;
@@ -73,9 +73,15 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
}, [questionId]);
const showAddButton = editing && (!users || users.length < MAX_ASK_NUMBER);
const showInviteDesc = !editing && users?.length === 0;
const showInviteFeat = !editing && users?.length === 0;
const showInviteButton = showInviteFeat && !readOnly;
const showEditButton = !readOnly && !editing && users?.length;
const showSaveButton = !readOnly && editing;
const showEmpty = readOnly && users?.length === 0;
if (showEmpty) {
return null;
}
return (
<Card className="mt-4">
@@ -113,9 +119,9 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
<span className="text-nowrap ms-2">{user.display_name}</span>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
<span
className="ps-1 pe-1 me-n1"
className="px-1 me-n1"
onClick={() => removeInviteUser(user)}>
x
×
</span>
</Button>
);
@@ -125,22 +131,21 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
key={user.username}
to={`/users/${user.username}`}
className="mx-2 my-1 d-inline-flex flex-nowrap">
<Avatar avatar={user.avatar} size="24" />
<span className="text-nowrap ms-2">{user.display_name}</span>
<Avatar avatar={user.avatar} size="24" className="rounded-1" />
<small className="text-nowrap ms-2">{user.display_name}</small>
</Link>
);
})}
{showAddButton ? (
<PeopleDropdown
selectedPeople={users}
onSelect={updateInviteUsers}
/>
) : null}
<PeopleDropdown
visible={showAddButton}
selectedPeople={users}
onSelect={updateInviteUsers}
/>
</div>
{showInviteDesc ? (
{showInviteFeat ? (
<>
<div className="text-muted">{t('desc')}</div>
{readOnly ? null : (
{showInviteButton ? (
<Button
size="sm"
variant="outline-primary"
@@ -148,7 +153,7 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
onClick={() => setEditing(true)}>
{t('invite')}
</Button>
)}
) : null}
</>
) : null}
</Card.Body>
+1 -1
View File
@@ -212,7 +212,7 @@ const Index = () => {
return (
<Row className="questionDetailPage pt-4 mb-5">
<Col className="flex-auto">
<Col className="page-main flex-auto">
{question?.operation?.level && <Alert data={question.operation} />}
{isLoading ? (
<ContentLoader />
+1 -1
View File
@@ -187,7 +187,7 @@ const Index = () => {
<div className="pt-4 mb-5 edit-answer-wrap">
<h3 className="mb-4">{t('title')}</h3>
<Row>
<Col className="flex-auto">
<Col className="page-main flex-auto">
<a
href={pathFactory.questionLanding(qid, data?.question.url_title)}
target="_blank"
+1 -1
View File
@@ -52,7 +52,7 @@ const Questions: FC = () => {
usePageTags({ title: pageTitle, subtitle: slogan });
return (
<Row className="pt-4 mb-5">
<Col className="flex-auto">
<Col className="page-main flex-auto">
<QuestionList
source="questions"
data={listData}
+1 -1
View File
@@ -125,7 +125,7 @@ const Index: FC = () => {
return (
<Row className="pt-4 mb-5">
<h3 className="mb-4">{t('review')}</h3>
<Col className="flex-auto">
<Col className="page-main flex-auto">
{!noTasks && ro && (
<>
<Alert variant="secondary">
+1 -1
View File
@@ -39,7 +39,7 @@ const Index = () => {
});
return (
<Row className="pt-4 mb-5">
<Col className="flex-auto">
<Col className="page-main flex-auto">
<Head data={extra} />
<SearchHead sort={order} count={count} />
<ListGroup className="rounded-0 mb-5">
+1 -1
View File
@@ -142,7 +142,7 @@ const Index = () => {
<div className="pt-4 mb-5">
<h3 className="mb-4">{t('title')}</h3>
<Row>
<Col className="flex-auto">
<Col className="page-main flex-auto">
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="display_name" className="mb-3">
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
+18 -7
View File
@@ -17,18 +17,24 @@ import {
useQuerySynonymsTags,
useQuestionList,
} from '@/services';
import QuestionList from '@/components/QuestionList';
import QuestionList, { QUESTION_ORDER_KEYS } from '@/components/QuestionList';
import HotQuestions from '@/components/HotQuestions';
import { escapeRemove, guard } from '@/utils';
import { escapeRemove, guard, Storage } from '@/utils';
import { pathFactory } from '@/router/pathFactory';
import { QUESTIONS_ORDER_STORAGE_KEY } from '@/common/constants';
const Questions: FC = () => {
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'tags' });
const navigate = useNavigate();
const routeParams = useParams();
const curTagName = routeParams.tagName || '';
const [urlSearchParams] = useSearchParams();
const curOrder = urlSearchParams.get('order') || 'active';
const storageOrder = Storage.get(QUESTIONS_ORDER_STORAGE_KEY);
const curOrder =
urlSearchParams.get('order') || storageOrder || QUESTION_ORDER_KEYS[0];
if (curOrder !== storageOrder) {
Storage.set(QUESTIONS_ORDER_STORAGE_KEY, curOrder);
}
const curPage = Number(urlSearchParams.get('page')) || 1;
const reqParams: Type.QueryQuestionsReq = {
page_size: 20,
@@ -101,7 +107,7 @@ const Questions: FC = () => {
});
return (
<Row className="pt-4 mb-5">
<Col className="flex-auto">
<Col className="page-main flex-auto">
{isLoading || listLoading ? (
<div className="tag-box mb-5 placeholder-glow">
<div className="mb-3 h3 placeholder" style={{ width: '120px' }} />
@@ -148,7 +154,12 @@ const Questions: FC = () => {
</div>
</div>
)}
<QuestionList source="tag" data={listData} isLoading={listLoading} />
<QuestionList
source="tag"
data={listData}
order={curOrder}
isLoading={listLoading}
/>
</Col>
<Col className="page-right-side mt-4 mt-xl-0">
<CustomSidebar />
@@ -159,4 +170,4 @@ const Questions: FC = () => {
);
};
export default Questions;
export default Index;
+1 -1
View File
@@ -191,7 +191,7 @@ const Index = () => {
<div className="pt-4 mb-5">
<h3 className="mb-4">{t('title')}</h3>
<Row>
<Col className="flex-auto">
<Col className="page-main flex-auto">
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="revision" className="mb-3">
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
+1 -1
View File
@@ -133,7 +133,7 @@ const TagIntroduction = () => {
return (
<Row className="pt-4 mb-5">
<Col className="flex-auto">
<Col className="page-main flex-auto">
<h3 className="mb-3">
<Link
to={pathFactory.tagLanding(tagInfo.slug_name)}
@@ -65,12 +65,13 @@ const Index: FC = () => {
clearTimeout(checkTimer);
};
}, []);
if (getUaType() !== USER_AGENT_NAMES.WeCom) {
return (
<Card className="text-center">
<Card.Body>
<Card.Title as="h3" className="mb-3">
{ucAgent?.agent_info.display_name} {t('login')}
{ucAgent?.agent_info?.display_name} {t('login')}
</Card.Title>
{qrcodeDataUrl ? (
<>
@@ -82,7 +83,7 @@ const Index: FC = () => {
/>
<div className="text-secondary mt-3">
{t('qrcode_login_tip', {
agentName: ucAgent?.agent_info.display_name,
agentName: ucAgent?.agent_info?.display_name,
})}
</div>
</>
+1 -1
View File
@@ -13,7 +13,7 @@ const Index = () => {
const { t } = useTranslation('translation');
const [searchParam] = useSearchParams();
const { agent: ucAgent } = userCenterStore();
let agentName = ucAgent?.agent_info.name || '';
let agentName = ucAgent?.agent_info?.name || '';
if (searchParam.get('agent_name')) {
agentName = searchParam.get('agent_name') || '';
}
@@ -35,7 +35,7 @@ const Index: FC = () => {
<Card>
<Card.Body>
<h3 className="text-center pt-3 mb-3">
{ucAgent?.agent_info.display_name} {t('login')}
{ucAgent?.agent_info?.display_name} {t('login')}
</h3>
<p className="text-danger text-center">
{t('login_failed_email_tip')}
+1 -1
View File
@@ -13,7 +13,7 @@ const Index = () => {
const { t } = useTranslation('translation');
const [searchParam] = useSearchParams();
const { agent: ucAgent } = userCenterStore();
let agentName = ucAgent?.agent_info.name || '';
let agentName = ucAgent?.agent_info?.name || '';
if (searchParam.get('agent_name')) {
agentName = searchParam.get('agent_name') || '';
}
+1 -1
View File
@@ -88,7 +88,7 @@ const Notifications = () => {
});
return (
<Row className="pt-4 mb-5">
<Col className="flex-auto">
<Col className="page-main flex-auto">
<h3 className="mb-4">{t('title')}</h3>
<div className="d-flex justify-content-between mb-3">
<ButtonGroup size="sm">
@@ -1,6 +1,5 @@
import { FC, memo } from 'react';
import { ListGroup, ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { FormatTime } from '@/components';
import { pathFactory } from '@/router/pathFactory';
@@ -11,7 +10,6 @@ interface Props {
}
const Index: FC<Props> = ({ visible, data }) => {
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
if (!visible || !data?.length) {
return null;
}
@@ -48,9 +46,7 @@ const Index: FC<Props> = ({ visible, data }) => {
{item.title}
</a>
<div className="d-flex align-items-center small text-secondary">
<span>
{t(item.rank_type, { keyPrefix: 'personal.rank_type' })}
</span>
<span>{item.rank_type}</span>
<span className="split-dot" />
<FormatTime time={item.created_at} className="me-4" />
</div>
+2 -2
View File
@@ -63,7 +63,7 @@ const Personal: FC = () => {
{userInfo?.status !== 'normal' && userInfo?.status_msg && (
<Alert data={userInfo?.status_msg} />
)}
<Col className="flex-auto">
<Col className="page-main flex-auto">
<UserInfo data={userInfo as UserInfoRes} />
</Col>
<Col
@@ -85,7 +85,7 @@ const Personal: FC = () => {
<Row>
<NavBar tabName={tabName} slug={username} isSelf={isSelf} />
<Col className="flex-auto">
<Col className="page-main flex-auto">
<Overview
visible={tabName === 'overview'}
introduction={userInfo?.bio_html || ''}
+42
View File
@@ -10,6 +10,47 @@ const equalToCurrentHref = (target: string, base?: string) => {
const targetUrl = new URL(target, base);
return targetUrl.toString() === window.location.href;
};
const matchToCurrentHref = (target: string) => {
target = (target || '').trim();
// Empty string or `/` can match any path
if (!target || target === '/') {
return true;
}
const { pathname, search, hash } = window.location;
const tPart = target.split('?');
/**
* With the current requirements, `hash` and `search` can simply be matched
* Later extended to field-by-field matching if necessary
*/
if (tPart[1]) {
const tChip = tPart[1].split('#');
const tSearch = tChip[0] || '';
const tHash = tChip[1] || '';
if (tHash && hash.indexOf(tHash) === -1) {
return false;
}
if (tSearch && search.indexOf(tSearch) === -1) {
return false;
}
}
/**
* As determination above, `tPart[0]` must be a valid string
*/
let pathMatch = true;
const tPath = tPart[0].split('/').filter((_) => !!_);
const lPath = pathname.split('/').filter((_) => !!_);
tPath.forEach((p, i) => {
const lp = lPath[i];
if (p !== lp) {
pathMatch = false;
}
});
return pathMatch;
};
const storageLoginRedirect = () => {
const { pathname } = window.location;
@@ -136,5 +177,6 @@ export const floppyNavigation = {
isRoutableLink,
handleRouteLinkClick,
equalToCurrentHref,
matchToCurrentHref,
storageLoginRedirect,
};
+6 -4
View File
@@ -94,13 +94,15 @@ export const IGNORE_PATH_LIST = [
'/user-center/',
];
export const isIgnoredPath = (ignoredPath: string | string[]) => {
export const isIgnoredPath = (ignoredPath?: string | string[]) => {
if (!ignoredPath) {
ignoredPath = IGNORE_PATH_LIST;
}
if (!Array.isArray(ignoredPath)) {
ignoredPath = [ignoredPath];
}
const { pathname } = window.location;
const matchingPath = ignoredPath.find((_) => {
return pathname.indexOf(_) !== -1;
const matchingPath = ignoredPath.find((p) => {
return floppyNavigation.matchToCurrentHref(p);
});
return !!matchingPath;
};