From a801ff6cda4aca2221ca26717b81a7add5042d76 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Sat, 29 Oct 2022 20:43:52 +0800 Subject: [PATCH 001/157] feat(navigation): guard route done --- ui/commitlint.config.js | 2 +- ui/src/App.tsx | 3 +- ui/src/common/constants.ts | 12 +- ui/src/common/interface.ts | 2 +- ui/src/components/Actions/index.tsx | 13 +- ui/src/components/Comment/index.tsx | 18 +- ui/src/components/Editor/ToolBars/image.tsx | 3 +- ui/src/components/FollowingTags/index.tsx | 7 +- ui/src/components/Header/index.tsx | 16 +- ui/src/components/HotQuestions/index.tsx | 3 +- ui/src/components/Modal/PicAuthCodeModal.tsx | 5 +- ui/src/components/Operate/index.tsx | 7 +- ui/src/components/QuestionList/index.tsx | 2 +- ui/src/components/Share/index.tsx | 4 +- ui/src/components/TagSelector/index.tsx | 2 +- ui/src/components/Unactivate/index.tsx | 11 +- ui/src/hooks/useChangeModal/index.tsx | 2 +- ui/src/hooks/useReportModal/index.tsx | 3 +- ui/src/i18n/init.ts | 4 +- ui/src/index.tsx | 22 ++- ui/src/pages/Admin/Answers/index.tsx | 2 +- ui/src/pages/Admin/Flags/index.tsx | 2 +- ui/src/pages/Admin/General/index.tsx | 2 +- ui/src/pages/Admin/Interface/index.tsx | 2 +- ui/src/pages/Admin/Questions/index.tsx | 2 +- ui/src/pages/Admin/Users/index.tsx | 2 +- ui/src/pages/Layout/index.tsx | 43 ++--- ui/src/pages/Questions/Ask/index.tsx | 2 +- .../Detail/components/Answer/index.tsx | 2 +- .../Detail/components/Question/index.tsx | 2 +- .../components/RelatedQuestions/index.tsx | 6 +- .../Detail/components/WriteAnswer/index.tsx | 2 +- ui/src/pages/Questions/Detail/index.tsx | 6 +- ui/src/pages/Questions/EditAnswer/index.tsx | 2 +- ui/src/pages/Search/components/Head/index.tsx | 6 +- ui/src/pages/Search/index.tsx | 2 +- ui/src/pages/Tags/Detail/index.tsx | 2 +- ui/src/pages/Tags/Edit/index.tsx | 6 +- ui/src/pages/Tags/Info/index.tsx | 2 +- ui/src/pages/Tags/index.tsx | 2 +- .../AccountForgot/components/sendEmail.tsx | 2 +- ui/src/pages/Users/AccountForgot/index.tsx | 5 +- ui/src/pages/Users/ActiveEmail/index.tsx | 6 +- ui/src/pages/Users/ConfirmNewEmail/index.tsx | 2 +- ui/src/pages/Users/Login/index.tsx | 31 +-- ui/src/pages/Users/Notifications/index.tsx | 2 +- ui/src/pages/Users/PasswordReset/index.tsx | 13 +- ui/src/pages/Users/Personal/index.tsx | 6 +- .../Register/components/SignUpForm/index.tsx | 2 +- ui/src/pages/Users/Register/index.tsx | 5 +- .../Account/components/ModifyEmail/index.tsx | 4 +- .../Account/components/ModifyPass/index.tsx | 2 +- .../pages/Users/Settings/Interface/index.tsx | 10 +- .../Users/Settings/Notification/index.tsx | 4 +- ui/src/pages/Users/Settings/Profile/index.tsx | 8 +- ui/src/pages/Users/Settings/index.tsx | 4 +- ui/src/pages/Users/Suspended/index.tsx | 4 +- ui/src/router/alias.ts | 8 + ui/src/router/guarder.ts | 42 ++++ ui/src/router/index.tsx | 48 +++-- ui/src/router/route-rules.ts | 9 - ui/src/router/{route-config.ts => routes.ts} | 19 +- ui/src/services/client/index.ts | 1 - ui/src/services/client/notification.ts | 5 +- ui/src/services/client/tag.ts | 5 +- ui/src/services/client/user.ts | 17 -- ui/src/services/common.ts | 2 +- ui/src/services/{api.ts => index.ts} | 0 ui/src/stores/index.ts | 4 +- ui/src/stores/userInfo.ts | 19 +- ui/src/utils/common.ts | 80 ++++++++ ui/src/utils/floppyNavigation.ts | 40 ++++ ui/src/utils/guards.ts | 182 ++++++++++++++++++ ui/src/utils/index.ts | 118 +----------- ui/src/utils/request.ts | 114 ++++++----- ui/src/utils/storage.ts | 5 +- ui/tsconfig.json | 1 - 77 files changed, 656 insertions(+), 411 deletions(-) create mode 100644 ui/src/router/alias.ts create mode 100644 ui/src/router/guarder.ts delete mode 100644 ui/src/router/route-rules.ts rename ui/src/router/{route-config.ts => routes.ts} (90%) delete mode 100644 ui/src/services/client/user.ts rename ui/src/services/{api.ts => index.ts} (100%) create mode 100644 ui/src/utils/common.ts create mode 100644 ui/src/utils/floppyNavigation.ts create mode 100644 ui/src/utils/guards.ts diff --git a/ui/commitlint.config.js b/ui/commitlint.config.js index 84dcb122..4944db0e 100644 --- a/ui/commitlint.config.js +++ b/ui/commitlint.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: ['@commitlint/config-conventional'], + extends: ['@commitlint/routes-conventional'], }; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 5d4f6925..878ca1ab 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,8 +1,9 @@ import { RouterProvider } from 'react-router-dom'; -import router from '@/router'; +import { routes, createBrowserRouter } from '@/router'; function App() { + const router = createBrowserRouter(routes); return ; } diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 9f7f6f5e..364c6bfe 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -1,9 +1,9 @@ -export const LOGIN_NEED_BACK = [ - '/users/login', - '/users/register', - '/users/account-recovery', - '/users/password-reset', -]; +export const DEFAULT_LANG = 'en_US'; +export const CURRENT_LANG_STORAGE_KEY = '_a_lang__'; +export const LOGGED_USER_STORAGE_KEY = '_a_lui_'; +export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; +export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; +export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_'; export const ADMIN_LIST_STATUS = { // normal; diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 73eac5d4..0b0a733e 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -108,7 +108,7 @@ export interface UserInfoBase { */ status?: string; /** roles */ - is_admin?: true; + is_admin?: boolean; } export interface UserInfoRes extends UserInfoBase { diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx index 87c35cef..25402ab2 100644 --- a/ui/src/components/Actions/index.tsx +++ b/ui/src/components/Actions/index.tsx @@ -5,11 +5,12 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import { Icon } from '@answer/components'; -import { bookmark, postVote } from '@answer/api'; -import { isLogin } from '@answer/utils'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { useToast } from '@answer/hooks'; +import { tryNormalLogged } from '@/utils/guards'; +import { bookmark, postVote } from '@/services'; + interface Props { className?: string; data: { @@ -32,7 +33,7 @@ const Index: FC = ({ className, data }) => { state: data?.collected, count: data?.collectCount, }); - const { username = '' } = userInfoStore((state) => state.user); + const { username = '' } = loggedUserInfoStore((state) => state.user); const toast = useToast(); const { t } = useTranslation(); useEffect(() => { @@ -48,7 +49,7 @@ const Index: FC = ({ className, data }) => { }, []); const handleVote = (type: 'up' | 'down') => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } @@ -84,7 +85,7 @@ const Index: FC = ({ className, data }) => { }; const handleBookmark = () => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } bookmark({ diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index e7fd3bff..a6dcdc98 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -8,18 +8,20 @@ import { unionBy } from 'lodash'; import { marked } from 'marked'; import * as Types from '@answer/common/interface'; +import { Modal } from '@answer/components'; +import { usePageUsers, useReportModal } from '@answer/hooks'; +import { matchedUsers, parseUserInfo } from '@answer/utils'; + +import { Form, ActionBar, Reply } from './components'; + +import { tryNormalLogged } from '@/utils/guards'; import { useQueryComments, addComment, deleteComment, updateComment, postVote, -} from '@answer/api'; -import { Modal } from '@answer/components'; -import { usePageUsers, useReportModal } from '@answer/hooks'; -import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils'; - -import { Form, ActionBar, Reply } from './components'; +} from '@/services'; import './index.scss'; @@ -163,7 +165,7 @@ const Comment = ({ objectId, mode }) => { }; const handleVote = (id, is_cancel) => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } @@ -189,7 +191,7 @@ const Comment = ({ objectId, mode }) => { }; const handleAction = ({ action }, item) => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } if (action === 'report') { diff --git a/ui/src/components/Editor/ToolBars/image.tsx b/ui/src/components/Editor/ToolBars/image.tsx index 5f4475a2..ead20eee 100644 --- a/ui/src/components/Editor/ToolBars/image.tsx +++ b/ui/src/components/Editor/ToolBars/image.tsx @@ -3,10 +3,11 @@ import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Modal as AnswerModal } from '@answer/components'; -import { uploadImage } from '@answer/api'; import ToolItem from '../toolItem'; import { IEditorContext } from '../types'; +import { uploadImage } from '@/services'; + const Image: FC = ({ editor }) => { const { t } = useTranslation('translation', { keyPrefix: 'editor' }); diff --git a/ui/src/components/FollowingTags/index.tsx b/ui/src/components/FollowingTags/index.tsx index 7f93a205..55e9bc76 100644 --- a/ui/src/components/FollowingTags/index.tsx +++ b/ui/src/components/FollowingTags/index.tsx @@ -4,8 +4,9 @@ import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; import { TagSelector, Tag } from '@answer/components'; -import { isLogin } from '@answer/utils'; -import { useFollowingTags, followTags } from '@answer/api'; + +import { tryNormalLogged } from '@/utils/guards'; +import { useFollowingTags, followTags } from '@/services'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'question' }); @@ -32,7 +33,7 @@ const Index: FC = () => { }); }; - if (!isLogin()) { + if (!tryNormalLogged()) { return null; } diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 54d6cee7..b4cd4e16 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -17,17 +17,22 @@ import { useLocation, } from 'react-router-dom'; -import { userInfoStore, siteInfoStore, interfaceStore } from '@answer/stores'; -import { logout, useQueryNotificationStatus } from '@answer/api'; -import Storage from '@answer/utils/storage'; +import { + loggedUserInfoStore, + siteInfoStore, + interfaceStore, +} from '@answer/stores'; import NavItems from './components/NavItems'; +import { logout, useQueryNotificationStatus } from '@/services'; +import { RouteAlias } from '@/router/alias'; + import './index.scss'; const Header: FC = () => { const navigate = useNavigate(); - const { user, clear } = userInfoStore(); + const { user, clear } = loggedUserInfoStore(); const { t } = useTranslation(); const [urlSearch] = useSearchParams(); const q = urlSearch.get('q'); @@ -42,9 +47,8 @@ const Header: FC = () => { const handleLogout = async () => { await logout(); - Storage.remove('token'); clear(); - navigate('/'); + navigate(RouteAlias.home); }; useEffect(() => { diff --git a/ui/src/components/HotQuestions/index.tsx b/ui/src/components/HotQuestions/index.tsx index d398d172..920bdce2 100644 --- a/ui/src/components/HotQuestions/index.tsx +++ b/ui/src/components/HotQuestions/index.tsx @@ -3,9 +3,10 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useHotQuestions } from '@answer/api'; import { Icon } from '@answer/components'; +import { useHotQuestions } from '@/services'; + const HotQuestions: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'question' }); const [questions, setQuestions] = useState([]); diff --git a/ui/src/components/Modal/PicAuthCodeModal.tsx b/ui/src/components/Modal/PicAuthCodeModal.tsx index f0fe2091..3a081a0c 100644 --- a/ui/src/components/Modal/PicAuthCodeModal.tsx +++ b/ui/src/components/Modal/PicAuthCodeModal.tsx @@ -9,6 +9,9 @@ import type { ImgCodeRes, } from '@answer/common/interface'; +import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; +import Storage from '@/utils/storage'; + interface IProps { /** control visible */ visible: boolean; @@ -55,7 +58,7 @@ const Index: React.FC = ({ placeholder={t('placeholder')} isInvalid={captcha.isInvalid} onChange={(e) => { - localStorage.setItem('captchaCode', e.target.value); + Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value); handleCaptcha({ captcha_code: { value: e.target.value, diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index 6d76af2a..201774c6 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -5,10 +5,11 @@ import { useTranslation } from 'react-i18next'; import { Modal } from '@answer/components'; import { useReportModal, useToast } from '@answer/hooks'; -import { deleteQuestion, deleteAnswer } from '@answer/api'; -import { isLogin } from '@answer/utils'; import Share from '../Share'; +import { deleteQuestion, deleteAnswer } from '@/services'; +import { tryNormalLogged } from '@/utils/guards'; + interface IProps { type: 'answer' | 'question'; qid: string; @@ -98,7 +99,7 @@ const Index: FC = ({ }; const handleAction = (action) => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } if (action === 'delete') { diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 295b9034..69c6ad9d 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -3,7 +3,7 @@ import { Row, Col, ListGroup } from 'react-bootstrap'; import { NavLink, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQuestionList } from '@answer/api'; +import { useQuestionList } from '@/services'; import type * as Type from '@answer/common/interface'; import { Icon, diff --git a/ui/src/components/Share/index.tsx b/ui/src/components/Share/index.tsx index d5866d98..74b3de34 100644 --- a/ui/src/components/Share/index.tsx +++ b/ui/src/components/Share/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { FacebookShareButton, TwitterShareButton } from 'next-share'; import copy from 'copy-to-clipboard'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; interface IProps { type: 'answer' | 'question'; @@ -15,7 +15,7 @@ interface IProps { } const Index: FC = ({ type, qid, aid, title }) => { - const user = userInfoStore((state) => state.user); + const user = loggedUserInfoStore((state) => state.user); const [show, setShow] = useState(false); const [showTip, setShowTip] = useState(false); const [canSystemShare, setSystemShareState] = useState(false); diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index 790b8242..f939b674 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -6,7 +6,7 @@ import { marked } from 'marked'; import classNames from 'classnames'; import { useTagModal } from '@answer/hooks'; -import { queryTags } from '@answer/api'; +import { queryTags } from '@/services'; import type * as Type from '@answer/common/interface'; import './index.scss'; diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index 3fc26200..dd44f68f 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -2,14 +2,17 @@ import React, { useState, useEffect } from 'react'; import { Button, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { resendEmail, checkImgCode } from '@answer/api'; +import { resendEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@answer/components/Modal'; import type { ImgCodeRes, ImgCodeReq, FormDataType, } from '@answer/common/interface'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; + +import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; +import Storage from '@/utils/storage'; interface IProps { visible: boolean; @@ -19,7 +22,7 @@ const Index: React.FC = ({ visible = false }) => { const { t } = useTranslation('translation', { keyPrefix: 'inactive' }); const [isSuccess, setSuccess] = useState(false); const [showModal, setModalState] = useState(false); - const { e_mail } = userInfoStore((state) => state.user); + const { e_mail } = loggedUserInfoStore((state) => state.user); const [formData, setFormData] = useState({ captcha_code: { value: '', @@ -47,7 +50,7 @@ const Index: React.FC = ({ visible = false }) => { } let obj: ImgCodeReq = {}; if (imgCode.verify) { - const code = localStorage.getItem('captchaCode') || ''; + const code = Storage.get(CAPTCHA_CODE_STORAGE_KEY) || ''; obj = { captcha_code: code, captcha_id: imgCode.captcha_id, diff --git a/ui/src/hooks/useChangeModal/index.tsx b/ui/src/hooks/useChangeModal/index.tsx index b46aa80d..76b9f2ea 100644 --- a/ui/src/hooks/useChangeModal/index.tsx +++ b/ui/src/hooks/useChangeModal/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { changeUserStatus } from '@answer/api'; +import { changeUserStatus } from '@/services'; import { Modal as AnswerModal } from '@answer/components'; const div = document.createElement('div'); diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx index 2566ccd6..2f7c8f18 100644 --- a/ui/src/hooks/useReportModal/index.tsx +++ b/ui/src/hooks/useReportModal/index.tsx @@ -4,10 +4,11 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { reportList, postReport, closeQuestion, putReport } from '@answer/api'; import { useToast } from '@answer/hooks'; import type * as Type from '@answer/common/interface'; +import { reportList, postReport, closeQuestion, putReport } from '@/services'; + interface Params { isBackend?: boolean; type: Type.ReportType; diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index 495ecc1d..deecc0e2 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -6,6 +6,8 @@ import Backend from 'i18next-http-backend'; import en from './locales/en.json'; import zh from './locales/zh_CN.json'; +import { DEFAULT_LANG } from '@/common/constants'; + i18next // load translation using http .use(Backend) @@ -21,7 +23,7 @@ i18next }, }, // debug: process.env.NODE_ENV === 'development', - fallbackLng: process.env.REACT_APP_LANG || 'en_US', + fallbackLng: process.env.REACT_APP_LANG || DEFAULT_LANG, interpolation: { escapeValue: false, }, diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 8553972a..1943b61a 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -3,14 +3,26 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; + +import { pullLoggedUser } from '@/utils/guards'; + import './i18n/init'; import './index.scss'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement, ); -root.render( - - - , -); + +async function bootstrapApp() { + /** + * NOTICE: must pre init logged user info for router + */ + await pullLoggedUser(); + root.render( + + + , + ); +} + +bootstrapApp(); diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index f9a9de27..ead61b57 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -14,7 +14,7 @@ import { } from '@answer/components'; import { ADMIN_LIST_STATUS } from '@answer/common/constants'; import { useEditStatusModal } from '@answer/hooks'; -import { useAnswerSearch, changeAnswerStatus } from '@answer/api'; +import { useAnswerSearch, changeAnswerStatus } from '@/services'; import * as Type from '@answer/common/interface'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Flags/index.tsx b/ui/src/pages/Admin/Flags/index.tsx index 5eb14ec4..ed10eaa5 100644 --- a/ui/src/pages/Admin/Flags/index.tsx +++ b/ui/src/pages/Admin/Flags/index.tsx @@ -12,7 +12,7 @@ import { } from '@answer/components'; import { useReportModal } from '@answer/hooks'; import * as Type from '@answer/common/interface'; -import { useFlagSearch } from '@answer/api'; +import { useFlagSearch } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 87e473d3..9caf81e8 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; import { useToast } from '@answer/hooks'; import { siteInfoStore } from '@answer/stores'; -import { useGeneralSetting, updateGeneralSetting } from '@answer/api'; +import { useGeneralSetting, updateGeneralSetting } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 66f7ef4c..e22d1046 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -14,7 +14,7 @@ import { updateInterfaceSetting, useInterfaceSetting, useThemeOptions, -} from '@answer/api'; +} from '@/services'; import { interfaceStore } from '@answer/stores'; import { UploadImg } from '@answer/components'; diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 373faf94..3235a819 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -18,7 +18,7 @@ import { useQuestionSearch, changeQuestionStatus, deleteQuestion, -} from '@answer/api'; +} from '@/services'; import * as Type from '@answer/common/interface'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index 6662b11f..b4efe613 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -3,7 +3,7 @@ import { Button, Form, Table, Badge } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQueryUsers } from '@answer/api'; +import { useQueryUsers } from '@/services'; import { Pagination, FormatTime, diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 69c5195d..bad408bf 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -1,60 +1,43 @@ -import { FC, useEffect } from 'react'; +import { FC, useEffect, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { - userInfoStore, - siteInfoStore, - interfaceStore, - toastStore, -} from '@answer/stores'; +import { siteInfoStore, interfaceStore, toastStore } from '@answer/stores'; import { Header, AdminHeader, Footer, Toast } from '@answer/components'; -import { useSiteSettings, useCheckUserStatus } from '@answer/api'; +import { useSiteSettings } from '@/services'; import Storage from '@/utils/storage'; +import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; let isMounted = false; const Layout: FC = () => { const { siteInfo, update: siteStoreUpdate } = siteInfoStore(); const { update: interfaceStoreUpdate } = interfaceStore(); const { data: siteSettings } = useSiteSettings(); - const { data: userStatus } = useCheckUserStatus(); - useEffect(() => { - if (siteSettings) { - siteStoreUpdate(siteSettings.general); - interfaceStoreUpdate(siteSettings.interface); - } - }, [siteSettings]); - const updateUser = userInfoStore((state) => state.update); const { msg: toastMsg, variant, clear: toastClear } = toastStore(); const { i18n } = useTranslation(); const closeToast = () => { toastClear(); }; + + useEffect(() => { + if (siteSettings) { + siteStoreUpdate(siteSettings.general); + interfaceStoreUpdate(siteSettings.interface); + } + }, [siteSettings]); if (!isMounted) { isMounted = true; - const lang = Storage.get('LANG'); - const user = Storage.get('userInfo'); - if (user) { - updateUser(user); - } + const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); if (lang) { i18n.changeLanguage(lang); } } - if (userStatus?.status) { - const user = Storage.get('userInfo'); - if (userStatus.status !== user.status) { - user.status = userStatus?.status; - updateUser(user); - } - } - return ( @@ -76,4 +59,4 @@ const Layout: FC = () => { ); }; -export default Layout; +export default memo(Layout); diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index abcc86f6..f202e0dc 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -14,7 +14,7 @@ import { useQueryRevisions, postAnswer, useQueryQuestionByTitle, -} from '@answer/api'; +} from '@/services'; import type * as Type from '@answer/common/interface'; import SearchQuestion from './components/SearchQuestion'; diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index b7bd25d8..4ffed114 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -11,7 +11,7 @@ import { FormatTime, htmlRender, } from '@answer/components'; -import { acceptanceAnswer } from '@answer/api'; +import { acceptanceAnswer } from '@/services'; import { scrollTop } from '@answer/utils'; import { AnswerItem } from '@answer/common/interface'; diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index 8085b172..10c18a47 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -13,7 +13,7 @@ import { htmlRender, } from '@answer/components'; import { formatCount } from '@answer/utils'; -import { following } from '@answer/api'; +import { following } from '@/services'; interface Props { data: any; diff --git a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx index bada8207..c6cf29d7 100644 --- a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx @@ -3,16 +3,16 @@ import { Card, ListGroup } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useSimilarQuestion } from '@answer/api'; +import { useSimilarQuestion } from '@/services'; import { Icon } from '@answer/components'; -import { userInfoStore } from '@/stores'; +import { loggedUserInfoStore } from '@/stores'; interface Props { id: string; } const Index: FC = ({ id }) => { - const { user } = userInfoStore(); + const { user } = loggedUserInfoStore(); const { t } = useTranslation('translation', { keyPrefix: 'related_question', }); diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx index 6343f7ef..d18eb4a8 100644 --- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx @@ -6,7 +6,7 @@ import { marked } from 'marked'; import classNames from 'classnames'; import { Editor, Modal } from '@answer/components'; -import { postAnswer } from '@answer/api'; +import { postAnswer } from '@/services'; import { FormDataType } from '@answer/common/interface'; interface Props { diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 2098b6a6..0eb6a15f 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -2,9 +2,9 @@ import { useEffect, useState } from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; -import { questionDetail, getAnswers } from '@answer/api'; +import { questionDetail, getAnswers } from '@/services'; import { Pagination, PageTitle } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { scrollTop } from '@answer/utils'; import { usePageUsers } from '@answer/hooks'; import type { @@ -37,7 +37,7 @@ const Index = () => { list: [], }); const { setUsers } = usePageUsers(); - const userInfo = userInfoStore((state) => state.user); + const userInfo = loggedUserInfoStore((state) => state.user); const isAuthor = userInfo?.username === question?.user_info?.username; const requestAnswers = async () => { const res = await getAnswers({ diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index 118fd33c..6a3d5127 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -11,7 +11,7 @@ import { useQueryAnswerInfo, modifyAnswer, useQueryRevisions, -} from '@answer/api'; +} from '@/services'; import type * as Type from '@answer/common/interface'; import './index.scss'; diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index c44f67bd..1558bc35 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -3,8 +3,8 @@ import { useSearchParams, Link } from 'react-router-dom'; import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { following } from '@answer/api'; -import { isLogin } from '@answer/utils'; +import { following } from '@/services'; +import { tryNormalLogged } from '@/utils/guards'; interface Props { data; @@ -20,7 +20,7 @@ const Index: FC = ({ data }) => { const [followed, setFollowed] = useState(data?.is_follower); const follow = () => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } following({ diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index d08968f4..0582684d 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { Pagination, PageTitle } from '@answer/components'; -import { useSearch } from '@answer/api'; +import { useSearch } from '@/services'; import { Head, SearchHead, SearchItem, Tips, Empty } from './components'; diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index cb736c92..3098108c 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import * as Type from '@answer/common/interface'; import { PageTitle, FollowingTags } from '@answer/components'; -import { useTagInfo, useFollow } from '@answer/api'; +import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; diff --git a/ui/src/pages/Tags/Edit/index.tsx b/ui/src/pages/Tags/Edit/index.tsx index 2b0541e1..12019642 100644 --- a/ui/src/pages/Tags/Edit/index.tsx +++ b/ui/src/pages/Tags/Edit/index.tsx @@ -7,8 +7,8 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { Editor, EditorRef, PageTitle } from '@answer/components'; -import { useTagInfo, modifyTag, useQueryRevisions } from '@answer/api'; -import { userInfoStore } from '@answer/stores'; +import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; +import { loggedUserInfoStore } from '@answer/stores'; import type * as Type from '@answer/common/interface'; interface FormDataItem { @@ -40,7 +40,7 @@ const initFormData = { }, }; const Ask = () => { - const { is_admin = false } = userInfoStore((state) => state.user); + const { is_admin = false } = loggedUserInfoStore((state) => state.user); const { tagId } = useParams(); const navigate = useNavigate(); diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 96cd814a..31c8ac29 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -17,7 +17,7 @@ import { useQuerySynonymsTags, saveSynonymsTags, deleteTag, -} from '@answer/api'; +} from '@/services'; const TagIntroduction = () => { const [isEdit, setEditState] = useState(false); diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index 2f9719f6..63fc7276 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -3,7 +3,7 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQueryTags, following } from '@answer/api'; +import { useQueryTags, following } from '@/services'; import { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components'; import { formatCount } from '@answer/utils'; diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index 9ae6d195..e9619ced 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -2,7 +2,7 @@ import { FC, memo, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { resetPassword, checkImgCode } from '@answer/api'; +import { resetPassword, checkImgCode } from '@/services'; import type { ImgCodeRes, PasswordResetReq, diff --git a/ui/src/pages/Users/AccountForgot/index.tsx b/ui/src/pages/Users/AccountForgot/index.tsx index b6a34610..5065e9b6 100644 --- a/ui/src/pages/Users/AccountForgot/index.tsx +++ b/ui/src/pages/Users/AccountForgot/index.tsx @@ -2,10 +2,9 @@ import React, { useState, useEffect } from 'react'; import { Container, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { isLogin } from '@answer/utils'; - import SendEmail from './components/sendEmail'; +import { tryNormalLogged } from '@/utils/guards'; import { PageTitle } from '@/components'; const Index: React.FC = () => { @@ -19,7 +18,7 @@ const Index: React.FC = () => { }; useEffect(() => { - isLogin(); + tryNormalLogged(); }, []); return ( diff --git a/ui/src/pages/Users/ActiveEmail/index.tsx b/ui/src/pages/Users/ActiveEmail/index.tsx index dee3fcb7..e2b3491e 100644 --- a/ui/src/pages/Users/ActiveEmail/index.tsx +++ b/ui/src/pages/Users/ActiveEmail/index.tsx @@ -1,15 +1,15 @@ import { FC, memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { activateAccount } from '@answer/api'; -import { userInfoStore } from '@answer/stores'; +import { activateAccount } from '@/services'; +import { loggedUserInfoStore } from '@answer/stores'; import { getQueryString } from '@answer/utils'; import { PageTitle } from '@/components'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'page_title' }); - const updateUser = userInfoStore((state) => state.update); + const updateUser = loggedUserInfoStore((state) => state.update); useEffect(() => { const code = getQueryString('code'); diff --git a/ui/src/pages/Users/ConfirmNewEmail/index.tsx b/ui/src/pages/Users/ConfirmNewEmail/index.tsx index cbd9ad70..9adaf80e 100644 --- a/ui/src/pages/Users/ConfirmNewEmail/index.tsx +++ b/ui/src/pages/Users/ConfirmNewEmail/index.tsx @@ -3,7 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap'; import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { changeEmailVerify } from '@answer/api'; +import { changeEmailVerify } from '@/services'; import { PageTitle } from '@/components'; diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index 8d18a500..6d9af5c2 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -1,26 +1,30 @@ import React, { FormEvent, useState, useEffect } from 'react'; import { Container, Form, Button, Col } from 'react-bootstrap'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { login, checkImgCode } from '@answer/api'; import type { LoginReqParams, ImgCodeRes, FormDataType, } from '@answer/common/interface'; import { PageTitle, Unactivate } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; -import { isLogin, getQueryString } from '@answer/utils'; +import { loggedUserInfoStore } from '@answer/stores'; +import { getQueryString } from '@answer/utils'; +import { login, checkImgCode } from '@/services'; +import { deriveUserStat, tryNormalLogged } from '@/utils/guards'; +import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; +import { RouteAlias } from '@/router/alias'; import { PicAuthCodeModal } from '@/components/Modal'; import Storage from '@/utils/storage'; const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'login' }); + const navigate = useNavigate(); const [refresh, setRefresh] = useState(0); - const updateUser = userInfoStore((state) => state.update); - const storeUser = userInfoStore((state) => state.user); + const updateUser = loggedUserInfoStore((state) => state.update); + const storeUser = loggedUserInfoStore((state) => state.user); const [formData, setFormData] = useState({ e_mail: { value: '', @@ -102,15 +106,16 @@ const Index: React.FC = () => { login(params) .then((res) => { updateUser(res); - if (res.mail_status === 2) { + const userStat = deriveUserStat(); + if (!userStat.isActivated) { // inactive setStep(2); setRefresh((pre) => pre + 1); - } - if (res.mail_status === 1) { - const path = Storage.get('ANSWER_PATH') || '/'; - Storage.remove('ANSWER_PATH'); - window.location.replace(path); + } else { + const path = + Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home; + Storage.remove(REDIRECT_PATH_STORAGE_KEY); + navigate(path, { replace: true }); } setModalState(false); @@ -154,7 +159,7 @@ const Index: React.FC = () => { if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { setStep(2); } else { - isLogin(); + tryNormalLogged(); } }, []); diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index aef9be45..687e7773 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -8,7 +8,7 @@ import { clearUnreadNotification, clearNotificationStatus, readNotification, -} from '@answer/api'; +} from '@/services'; import { PageTitle } from '@answer/components'; import Inbox from './components/Inbox'; diff --git a/ui/src/pages/Users/PasswordReset/index.tsx b/ui/src/pages/Users/PasswordReset/index.tsx index abffc6ba..af97001f 100644 --- a/ui/src/pages/Users/PasswordReset/index.tsx +++ b/ui/src/pages/Users/PasswordReset/index.tsx @@ -3,19 +3,19 @@ import { Container, Col, Form, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { replacementPassword } from '@answer/api'; -import { userInfoStore } from '@answer/stores'; -import { getQueryString, isLogin } from '@answer/utils'; +import { loggedUserInfoStore } from '@answer/stores'; +import { getQueryString } from '@answer/utils'; import type { FormDataType } from '@answer/common/interface'; -import Storage from '@/utils/storage'; +import { replacementPassword } from '@/services'; +import { tryNormalLogged } from '@/utils/guards'; import { PageTitle } from '@/components'; const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'password_reset' }); const [step, setStep] = useState(1); - const clearUser = userInfoStore((state) => state.clear); + const clearUser = loggedUserInfoStore((state) => state.clear); const [formData, setFormData] = useState({ pass: { value: '', @@ -105,7 +105,6 @@ const Index: React.FC = () => { .then(() => { // clear login information then to login page clearUser(); - Storage.remove('token'); setStep(2); }) .catch((err) => { @@ -118,7 +117,7 @@ const Index: React.FC = () => { }; useEffect(() => { - isLogin(); + tryNormalLogged(); }, []); return ( <> diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index 7c099a4b..fa8c2200 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -4,12 +4,12 @@ import { useTranslation } from 'react-i18next'; import { useParams, useSearchParams } from 'react-router-dom'; import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { usePersonalInfoByName, usePersonalTop, usePersonalListByTabName, -} from '@answer/api'; +} from '@/services'; import { UserInfo, @@ -30,7 +30,7 @@ const Personal: FC = () => { const page = searchParams.get('page') || 1; const order = searchParams.get('order') || 'newest'; const { t } = useTranslation('translation', { keyPrefix: 'personal' }); - const sessionUser = userInfoStore((state) => state.user); + const sessionUser = loggedUserInfoStore((state) => state.user); const isSelf = sessionUser?.username === username; const { data: userInfo } = usePersonalInfoByName(username); diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx index 2f067962..08afacf2 100644 --- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx +++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx @@ -3,7 +3,7 @@ import { Form, Button, Col } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { register } from '@answer/api'; +import { register } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import userStore from '@/stores/userInfo'; diff --git a/ui/src/pages/Users/Register/index.tsx b/ui/src/pages/Users/Register/index.tsx index c50c353c..5f8370a0 100644 --- a/ui/src/pages/Users/Register/index.tsx +++ b/ui/src/pages/Users/Register/index.tsx @@ -3,10 +3,11 @@ import { Container } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { PageTitle, Unactivate } from '@answer/components'; -import { isLogin } from '@answer/utils'; import SignUpForm from './components/SignUpForm'; +import { tryNormalLogged } from '@/utils/guards'; + const Index: React.FC = () => { const [showForm, setShowForm] = useState(true); const { t } = useTranslation('translation', { keyPrefix: 'login' }); @@ -16,7 +17,7 @@ const Index: React.FC = () => { }; useEffect(() => { - isLogin(); + tryNormalLogged(); }, []); return ( diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index 84182a53..495fd1d4 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -3,7 +3,7 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; -import { getUserInfo, changeEmail } from '@answer/api'; +import { getLoggedUserInfo, changeEmail } from '@/services'; import { useToast } from '@answer/hooks'; const reg = /(?<=.{2}).+(?=@)/gi; @@ -23,7 +23,7 @@ const Index: FC = () => { const [userInfo, setUserInfo] = useState(); const toast = useToast(); useEffect(() => { - getUserInfo().then((resp) => { + getLoggedUserInfo().then((resp) => { setUserInfo(resp); }); }, []); diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx index 23265763..eac17839 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx @@ -2,7 +2,7 @@ import React, { FC, FormEvent, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { modifyPassword } from '@answer/api'; +import { modifyPassword } from '@/services'; import { useToast } from '@answer/hooks'; import type { FormDataType } from '@answer/common/interface'; diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index ca7ce38d..a256a65f 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -6,10 +6,11 @@ import dayjs from 'dayjs'; import en from 'dayjs/locale/en'; import zh from 'dayjs/locale/zh-cn'; -import { languages } from '@answer/api'; +import { languages } from '@/services'; import type { LangsType, FormDataType } from '@answer/common/interface'; import { useToast } from '@answer/hooks'; +import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; const Index = () => { @@ -34,8 +35,8 @@ const Index = () => { const handleSubmit = (event: FormEvent) => { event.preventDefault(); - Storage.set('LANG', formData.lang.value); - dayjs.locale(formData.lang.value === 'en_US' ? en : zh); + Storage.set(CURRENT_LANG_STORAGE_KEY, formData.lang.value); + dayjs.locale(formData.lang.value === DEFAULT_LANG ? en : zh); i18n.changeLanguage(formData.lang.value); toast.onShow({ msg: t('update', { keyPrefix: 'toast' }), @@ -45,7 +46,7 @@ const Index = () => { useEffect(() => { getLangs(); - const lang = Storage.get('LANG'); + const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); if (lang) { setFormData({ lang: { @@ -60,7 +61,6 @@ const Index = () => {
{t('lang.label')} - { @@ -20,7 +20,7 @@ const Index = () => { }); const getProfile = () => { - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { setFormData({ notice_switch: { value: res.notice_status === 1, diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 49266ceb..ecfd3449 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -4,10 +4,10 @@ import { Trans, useTranslation } from 'react-i18next'; import { marked } from 'marked'; -import { modifyUserInfo, uploadAvatar, getUserInfo } from '@answer/api'; +import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import { UploadImg, Avatar } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { useToast } from '@answer/hooks'; const Index: React.FC = () => { @@ -15,7 +15,7 @@ const Index: React.FC = () => { keyPrefix: 'settings.profile', }); const toast = useToast(); - const { user, update } = userInfoStore(); + const { user, update } = loggedUserInfoStore(); const [formData, setFormData] = useState({ display_name: { value: '', @@ -164,7 +164,7 @@ const Index: React.FC = () => { }; const getProfile = () => { - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { formData.display_name.value = res.display_name; formData.username.value = res.username; formData.bio.value = res.bio; diff --git a/ui/src/pages/Users/Settings/index.tsx b/ui/src/pages/Users/Settings/index.tsx index 45f3080c..8967cd4e 100644 --- a/ui/src/pages/Users/Settings/index.tsx +++ b/ui/src/pages/Users/Settings/index.tsx @@ -3,7 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { getUserInfo } from '@answer/api'; +import { getLoggedUserInfo } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import Nav from './components/Nav'; @@ -43,7 +43,7 @@ const Index: React.FC = () => { }, }); const getProfile = () => { - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { formData.display_name.value = res.display_name; formData.bio.value = res.bio; formData.avatar.value = res.avatar; diff --git a/ui/src/pages/Users/Suspended/index.tsx b/ui/src/pages/Users/Suspended/index.tsx index 293603d0..4c381d44 100644 --- a/ui/src/pages/Users/Suspended/index.tsx +++ b/ui/src/pages/Users/Suspended/index.tsx @@ -1,12 +1,12 @@ import { useTranslation } from 'react-i18next'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { PageTitle } from '@/components'; const Suspended = () => { const { t } = useTranslation('translation', { keyPrefix: 'suspended' }); - const userInfo = userInfoStore((state) => state.user); + const userInfo = loggedUserInfoStore((state) => state.user); if (userInfo.status !== 'forbidden') { window.location.replace('/'); diff --git a/ui/src/router/alias.ts b/ui/src/router/alias.ts new file mode 100644 index 00000000..f6959ed3 --- /dev/null +++ b/ui/src/router/alias.ts @@ -0,0 +1,8 @@ +export const RouteAlias = { + home: '/', + login: '/users/login', + register: '/users/register', + activation: '/users/login?status=inactive', + activationFailed: '/users/account-activation/failed', + suspended: '/users/account-suspended', +}; diff --git a/ui/src/router/guarder.ts b/ui/src/router/guarder.ts new file mode 100644 index 00000000..8fd8a69b --- /dev/null +++ b/ui/src/router/guarder.ts @@ -0,0 +1,42 @@ +import { + pullLoggedUser, + isLoggedAndNormal, + isAdminLogged, + isLogged, + isNotLogged, + isNotLoggedOrNormal, + isLoggedAndInactive, + isLoggedAndSuspended, + isNotLoggedOrInactive, +} from '@/utils/guards'; + +const RouteGuarder = { + base: async () => { + return isNotLoggedOrNormal(); + }, + logged: async () => { + return isLogged(); + }, + notLogged: async () => { + return isNotLogged(); + }, + notLoggedOrInactive: async () => { + return isNotLoggedOrInactive(); + }, + loggedAndNormal: async () => { + await pullLoggedUser(true); + return isLoggedAndNormal(); + }, + loggedAndInactive: async () => { + return isLoggedAndInactive(); + }, + loggedAndSuspended: async () => { + return isLoggedAndSuspended(); + }, + adminLogged: async () => { + await pullLoggedUser(true); + return isAdminLogged(); + }, +}; + +export default RouteGuarder; diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 99d44723..a7452ff8 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -1,14 +1,13 @@ import React, { Suspense, lazy } from 'react'; -import { RouteObject, createBrowserRouter } from 'react-router-dom'; +import { RouteObject, createBrowserRouter, redirect } from 'react-router-dom'; -import Layout from '@answer/pages/Layout'; - -import routeConfig, { RouteNode } from '@/router/route-config'; -import RouteRules from '@/router/route-rules'; +import Layout from '@/pages/Layout'; +import baseRoutes, { RouteNode } from '@/router/routes'; +import { floppyNavigation } from '@/utils'; const routes: RouteObject[] = []; -const routeGen = (routeNodes: RouteNode[], root: RouteObject[]) => { +const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => { routeNodes.forEach((rn) => { if (rn.path === '/') { rn.element = ; @@ -18,40 +17,37 @@ const routeGen = (routeNodes: RouteNode[], root: RouteObject[]) => { * ref: https://webpack.js.org/api/module-methods/#import-1 */ rn.page = rn.page.replace('pages/', ''); - const Control = lazy(() => import(`@/pages/${rn.page}`)); + const Ctrl = lazy(() => import(`@/pages/${rn.page}`)); rn.element = ( - + ); } root.push(rn); - if (Array.isArray(rn.rules)) { - const ruleFunc: Function[] = []; - if (typeof rn.loader === 'function') { - ruleFunc.push(rn.loader); - } - rn.rules.forEach((ruleKey) => { - const func = RouteRules[ruleKey]; - if (typeof func === 'function') { - ruleFunc.push(func); + if (rn.guard) { + const { guard } = rn; + const loaderRef = rn.loader; + rn.loader = async (args) => { + const gr = await guard(args); + if (gr?.redirect && floppyNavigation.differentCurrent(gr.redirect)) { + return redirect(gr.redirect); } - }); - rn.loader = ({ params }) => { - ruleFunc.forEach((func) => { - func(params); - }); + let ret; + if (typeof loaderRef === 'function') { + ret = await loaderRef(args); + } + return ret; }; } const children = Array.isArray(rn.children) ? rn.children : null; if (children) { rn.children = []; - routeGen(children, rn.children); + routeWrapper(children, rn.children); } }); }; -routeGen(routeConfig, routes); +routeWrapper(baseRoutes, routes); -const router = createBrowserRouter(routes); -export default router; +export { routes, createBrowserRouter }; diff --git a/ui/src/router/route-rules.ts b/ui/src/router/route-rules.ts deleted file mode 100644 index e7c2b83c..00000000 --- a/ui/src/router/route-rules.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { isLogin } from '@answer/utils'; - -const RouteRules = { - isLoginAndNormal: () => { - return isLogin(true); - }, -}; - -export default RouteRules; diff --git a/ui/src/router/route-config.ts b/ui/src/router/routes.ts similarity index 90% rename from ui/src/router/route-config.ts rename to ui/src/router/routes.ts index 6267482e..ad964c38 100644 --- a/ui/src/router/route-config.ts +++ b/ui/src/router/routes.ts @@ -1,14 +1,18 @@ import { RouteObject } from 'react-router-dom'; +import RouteGuarder from '@/router/guarder'; + export interface RouteNode extends RouteObject { page: string; children?: RouteNode[]; - rules?: string[]; + guard?: Function; } -const routeConfig: RouteNode[] = [ + +const routes: RouteNode[] = [ { path: '/', page: 'pages/Layout', + guard: RouteGuarder.base, children: [ // question and answer { @@ -31,12 +35,12 @@ const routeConfig: RouteNode[] = [ { path: 'questions/ask', page: 'pages/Questions/Ask', - rules: ['isLoginAndNormal'], + guard: RouteGuarder.loggedAndNormal, }, { path: 'posts/:qid/edit', page: 'pages/Questions/Ask', - rules: ['isLoginAndNormal'], + guard: RouteGuarder.loggedAndNormal, }, { path: 'posts/:qid/:aid/edit', @@ -105,18 +109,22 @@ const routeConfig: RouteNode[] = [ { path: 'users/login', page: 'pages/Users/Login', + guard: RouteGuarder.notLoggedOrInactive, }, { path: 'users/register', page: 'pages/Users/Register', + guard: RouteGuarder.notLogged, }, { path: 'users/account-recovery', page: 'pages/Users/AccountForgot', + guard: RouteGuarder.loggedAndNormal, }, { path: 'users/password-reset', page: 'pages/Users/PasswordReset', + guard: RouteGuarder.loggedAndNormal, }, { path: 'users/account-activation', @@ -142,6 +150,7 @@ const routeConfig: RouteNode[] = [ { path: 'admin', page: 'pages/Admin', + guard: RouteGuarder.adminLogged, children: [ { index: true, @@ -192,4 +201,4 @@ const routeConfig: RouteNode[] = [ ], }, ]; -export default routeConfig; +export default routes; diff --git a/ui/src/services/client/index.ts b/ui/src/services/client/index.ts index dbea39bf..1f1af66a 100644 --- a/ui/src/services/client/index.ts +++ b/ui/src/services/client/index.ts @@ -1,6 +1,5 @@ export * from './activity'; export * from './personal'; -export * from './user'; export * from './notification'; export * from './question'; export * from './search'; diff --git a/ui/src/services/client/notification.ts b/ui/src/services/client/notification.ts index 18b73343..dd9d880e 100644 --- a/ui/src/services/client/notification.ts +++ b/ui/src/services/client/notification.ts @@ -2,9 +2,10 @@ import useSWR from 'swr'; import qs from 'qs'; import request from '@answer/utils/request'; -import { isLogin } from '@answer/utils'; import type * as Type from '@answer/common/interface'; +import { tryNormalLogged } from '@/utils/guards'; + export const useQueryNotifications = (params) => { const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, { skipNulls: true, @@ -33,7 +34,7 @@ export const useQueryNotificationStatus = () => { const apiUrl = '/answer/api/v1/notification/status'; return useSWR<{ inbox: number; achievement: number }>( - isLogin() ? apiUrl : null, + tryNormalLogged() ? apiUrl : null, request.instance.get, { refreshInterval: 3000, diff --git a/ui/src/services/client/tag.ts b/ui/src/services/client/tag.ts index 87e46743..634b4472 100644 --- a/ui/src/services/client/tag.ts +++ b/ui/src/services/client/tag.ts @@ -1,9 +1,10 @@ import useSWR from 'swr'; import request from '@answer/utils/request'; -import { isLogin } from '@answer/utils'; import type * as Type from '@answer/common/interface'; +import { tryNormalLogged } from '@/utils/guards'; + export const deleteTag = (id) => { return request.delete('/answer/api/v1/tag', { tag_id: id, @@ -24,7 +25,7 @@ export const saveSynonymsTags = (params) => { export const useFollowingTags = () => { let apiUrl = ''; - if (isLogin()) { + if (tryNormalLogged()) { apiUrl = '/answer/api/v1/tags/following'; } const { data, error, mutate } = useSWR(apiUrl, request.instance.get); diff --git a/ui/src/services/client/user.ts b/ui/src/services/client/user.ts deleted file mode 100644 index abe9ee3a..00000000 --- a/ui/src/services/client/user.ts +++ /dev/null @@ -1,17 +0,0 @@ -import useSWR from 'swr'; - -import request from '@answer/utils/request'; - -export const useCheckUserStatus = () => { - const apiUrl = '/answer/api/v1/user/status'; - const hasToken = localStorage.getItem('token'); - const { data, error } = useSWR<{ status: string }, Error>( - hasToken ? apiUrl : null, - request.instance.get, - ); - return { - data, - isLoading: !data && !error, - error, - }; -}; diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 6cef9f19..6f2cf83a 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -115,7 +115,7 @@ export const resendEmail = (params?: Type.ImgCodeReq) => { * @description get login userinfo * @returns {UserInfo} */ -export const getUserInfo = () => { +export const getLoggedUserInfo = () => { return request.get('/answer/api/v1/user/info'); }; diff --git a/ui/src/services/api.ts b/ui/src/services/index.ts similarity index 100% rename from ui/src/services/api.ts rename to ui/src/services/index.ts diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts index 0962911e..6bc6377e 100644 --- a/ui/src/stores/index.ts +++ b/ui/src/stores/index.ts @@ -1,12 +1,12 @@ import toastStore from './toast'; -import userInfoStore from './userInfo'; +import loggedUserInfoStore from './userInfo'; import globalStore from './global'; import siteInfoStore from './siteInfo'; import interfaceStore from './interface'; export { toastStore, - userInfoStore, + loggedUserInfoStore, globalStore, siteInfoStore, interfaceStore, diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index bbfb640e..9aa540b5 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -3,6 +3,11 @@ import create from 'zustand'; import type { UserInfoRes } from '@answer/common/interface'; import Storage from '@answer/utils/storage'; +import { + LOGGED_USER_STORAGE_KEY, + LOGGED_TOKEN_STORAGE_KEY, +} from '@/common/constants'; + interface UserInfoStore { user: UserInfoRes; update: (params: UserInfoRes) => void; @@ -19,23 +24,23 @@ const initUser: UserInfoRes = { location: '', website: '', status: '', - mail_status: 0, + mail_status: 1, }; -const userInfoStore = create((set) => ({ +const loggedUserInfoStore = create((set) => ({ user: initUser, update: (params) => set(() => { - Storage.set('token', params.access_token); - Storage.set('userInfo', params); + Storage.set(LOGGED_TOKEN_STORAGE_KEY, params.access_token); + Storage.set(LOGGED_USER_STORAGE_KEY, params); return { user: params }; }), clear: () => set(() => { - // Storage.remove('token'); - Storage.remove('userInfo'); + Storage.remove(LOGGED_TOKEN_STORAGE_KEY); + Storage.remove(LOGGED_USER_STORAGE_KEY); return { user: initUser }; }), })); -export default userInfoStore; +export default loggedUserInfoStore; diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts new file mode 100644 index 00000000..169dc1ad --- /dev/null +++ b/ui/src/utils/common.ts @@ -0,0 +1,80 @@ +function getQueryString(name: string): string { + const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`); + const r = window.location.search.substr(1).match(reg); + if (r != null) return unescape(r[2]); + return ''; +} + +function thousandthDivision(num) { + const reg = /\d{1,3}(?=(\d{3})+$)/g; + return `${num}`.replace(reg, '$&,'); +} + +function formatCount($num: number): string { + let res = String($num); + if (!Number.isFinite($num)) { + res = '0'; + } else if ($num < 10000) { + res = thousandthDivision($num); + } else if ($num < 1000000) { + res = `${Math.round($num / 100) / 10}k`; + } else if ($num >= 1000000) { + res = `${Math.round($num / 100000) / 10}m`; + } + return res; +} + +function scrollTop(element) { + if (!element) { + return; + } + const offset = 120; + const bodyRect = document.body.getBoundingClientRect().top; + const elementRect = element.getBoundingClientRect().top; + const elementPosition = elementRect - bodyRect; + const offsetPosition = elementPosition - offset; + + window.scrollTo({ + top: offsetPosition, + }); +} + +/** + * Extract user info from markdown + * @param markdown string + * @returns Array<{displayName: string, userName: string}> + */ +function matchedUsers(markdown) { + const globalReg = /\B@([\w|]+)/g; + const reg = /\B@([\w\\_\\.]+)/; + + const users = markdown.match(globalReg); + if (!users) { + return []; + } + return users.map((user) => { + const matched = user.match(reg); + return { + userName: matched[1], + }; + }); +} + +/** + * Identify user information from markdown + * @param markdown string + * @returns string + */ +function parseUserInfo(markdown) { + const globalReg = /\B@([\w\\_\\.\\-]+)/g; + return markdown.replace(globalReg, '[@$1](/u/$1)'); +} + +export { + getQueryString, + thousandthDivision, + formatCount, + scrollTop, + matchedUsers, + parseUserInfo, +}; diff --git a/ui/src/utils/floppyNavigation.ts b/ui/src/utils/floppyNavigation.ts new file mode 100644 index 00000000..7edbbaa0 --- /dev/null +++ b/ui/src/utils/floppyNavigation.ts @@ -0,0 +1,40 @@ +import { RouteAlias } from '@/router/alias'; +import Storage from '@/utils/storage'; +import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; + +const differentCurrent = (target: string, base?: string) => { + base ||= window.location.origin; + const targetUrl = new URL(target, base); + return targetUrl.toString() !== window.location.href; +}; + +/** + * only navigate if not same as current url + * @param pathname + * @param callback + */ +const navigate = (pathname: string, callback: Function) => { + if (differentCurrent(pathname)) { + callback(); + } +}; + +/** + * auto navigate to login page with redirect info + */ +const navigateToLogin = () => { + const { pathname } = window.location; + if (pathname !== RouteAlias.login && pathname !== RouteAlias.register) { + const redirectUrl = window.location.href; + Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl); + } + navigate(RouteAlias.login, () => { + window.location.replace(RouteAlias.login); + }); +}; + +export const floppyNavigation = { + differentCurrent, + navigate, + navigateToLogin, +}; diff --git a/ui/src/utils/guards.ts b/ui/src/utils/guards.ts new file mode 100644 index 00000000..f0faf55b --- /dev/null +++ b/ui/src/utils/guards.ts @@ -0,0 +1,182 @@ +import { getLoggedUserInfo } from '@/services'; +import { loggedUserInfoStore } from '@/stores'; +import { RouteAlias } from '@/router/alias'; +import Storage from '@/utils/storage'; +import { LOGGED_USER_STORAGE_KEY } from '@/common/constants'; +import { floppyNavigation } from '@/utils/floppyNavigation'; + +type UserStat = { + isLogged: boolean; + isActivated: boolean; + isSuspended: boolean; + isNormal: boolean; + isAdmin: boolean; +}; +export const deriveUserStat = (): UserStat => { + const stat: UserStat = { + isLogged: false, + isActivated: false, + isSuspended: false, + isNormal: false, + isAdmin: false, + }; + const { user } = loggedUserInfoStore.getState(); + if (user.id && user.username) { + stat.isLogged = true; + } + if (stat.isLogged && user.mail_status === 1) { + stat.isActivated = true; + } + if (stat.isLogged && user.status === 'forbidden') { + stat.isSuspended = true; + } + if (stat.isLogged && stat.isActivated && !stat.isSuspended) { + stat.isNormal = true; + } + if (stat.isNormal && user.is_admin === true) { + stat.isAdmin = true; + } + + return stat; +}; + +type GuardResult = { + ok: boolean; + redirect?: string; +}; +let pullLock = false; +let dedupeTimestamp = 0; +export const pullLoggedUser = async (forceRePull = false) => { + // only pull once if not force re-pull + if (pullLock && !forceRePull) { + return; + } + // dedupe pull requests in this time span in 10 seconds + if (Date.now() - dedupeTimestamp < 1000 * 10) { + return; + } + dedupeTimestamp = Date.now(); + const loggedUserInfo = await getLoggedUserInfo().catch((ex) => { + dedupeTimestamp = 0; + if (!deriveUserStat().isLogged) { + // load fallback userInfo from local storage + const storageLoggedUserInfo = Storage.get(LOGGED_USER_STORAGE_KEY); + if (storageLoggedUserInfo) { + loggedUserInfoStore.getState().update(storageLoggedUserInfo); + } + } + console.error(ex); + }); + if (loggedUserInfo) { + pullLock = true; + loggedUserInfoStore.getState().update(loggedUserInfo); + } +}; + +export const isLogged = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + if (!userStat.isLogged) { + ret.ok = false; + ret.redirect = RouteAlias.login; + } + return ret; +}; + +export const isNotLogged = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + if (userStat.isLogged) { + ret.ok = false; + ret.redirect = RouteAlias.home; + } + return ret; +}; + +export const isLoggedAndInactive = () => { + const ret: GuardResult = { ok: false, redirect: undefined }; + const userStat = deriveUserStat(); + if (!userStat.isActivated) { + ret.ok = true; + ret.redirect = RouteAlias.activation; + } + return ret; +}; + +export const isLoggedAndSuspended = () => { + const ret: GuardResult = { ok: false, redirect: undefined }; + const userStat = deriveUserStat(); + if (userStat.isSuspended) { + ret.redirect = RouteAlias.suspended; + ret.ok = true; + } + return ret; +}; + +export const isLoggedAndNormal = () => { + const ret: GuardResult = { ok: false, redirect: undefined }; + const userStat = deriveUserStat(); + if (userStat.isNormal) { + ret.ok = true; + } else if (!userStat.isActivated) { + ret.redirect = RouteAlias.activation; + } else if (!userStat.isSuspended) { + ret.redirect = RouteAlias.suspended; + } else if (!userStat.isLogged) { + ret.redirect = RouteAlias.login; + } + return ret; +}; + +export const isNotLoggedOrNormal = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + const gr = isLoggedAndNormal(); + if (!gr.ok && userStat.isLogged) { + ret.ok = false; + ret.redirect = gr.redirect; + } + return ret; +}; + +export const isNotLoggedOrInactive = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + if (userStat.isLogged || userStat.isActivated) { + ret.ok = false; + ret.redirect = RouteAlias.home; + } + return ret; +}; + +export const isAdminLogged = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + if (!userStat.isAdmin) { + ret.redirect = RouteAlias.home; + ret.ok = false; + } + return ret; +}; + +/** + * try user was logged and all state ok + * @param autoLogin + */ +export const tryNormalLogged = (autoLogin: boolean = false) => { + const gr = isLoggedAndNormal(); + if (gr.ok) { + return true; + } + + if (gr.redirect === RouteAlias.login && autoLogin) { + floppyNavigation.navigateToLogin(); + } else if (gr.redirect) { + floppyNavigation.navigate(gr.redirect, () => { + // @ts-ignore + window.location.replace(gr.redirect); + }); + } + + return false; +}; diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts index 20cde293..a1eaf02c 100644 --- a/ui/src/utils/index.ts +++ b/ui/src/utils/index.ts @@ -1,114 +1,6 @@ -import { LOGIN_NEED_BACK } from '@answer/common/constants'; +export * from './common'; +export * as guards from './guards'; -import Storage from './storage'; - -function getQueryString(name: string): string { - const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`); - const r = window.location.search.substr(1).match(reg); - if (r != null) return unescape(r[2]); - return ''; -} - -function thousandthDivision(num) { - const reg = /\d{1,3}(?=(\d{3})+$)/g; - return `${num}`.replace(reg, '$&,'); -} - -function formatCount($num: number): string { - let res = String($num); - if (!Number.isFinite($num)) { - res = '0'; - } else if ($num < 10000) { - res = thousandthDivision($num); - } else if ($num < 1000000) { - res = `${Math.round($num / 100) / 10}k`; - } else if ($num >= 1000000) { - res = `${Math.round($num / 100000) / 10}m`; - } - return res; -} - -function isLogin(needToLogin?: boolean): boolean { - const user = Storage.get('userInfo'); - const path = window.location.pathname; - - // User deleted or suspended - if (user.username && user.status === 'forbidden') { - if (path !== '/users/account-suspended') { - window.location.pathname = '/users/account-suspended'; - } - return false; - } - - // login and active - if (user.username && user.mail_status === 1) { - if (LOGIN_NEED_BACK.includes(path)) { - window.location.replace('/'); - } - return true; - } - - // un login or inactivated - if ((!user.username || user.mail_status === 2) && needToLogin) { - Storage.set('ANSWER_PATH', path); - window.location.href = '/users/login'; - } - - return false; -} - -function scrollTop(element) { - if (!element) { - return; - } - const offset = 120; - const bodyRect = document.body.getBoundingClientRect().top; - const elementRect = element.getBoundingClientRect().top; - const elementPosition = elementRect - bodyRect; - const offsetPosition = elementPosition - offset; - - window.scrollTo({ - top: offsetPosition, - }); -} - -/** - * Extract user info from markdown - * @param markdown string - * @returns Array<{displayName: string, userName: string}> - */ -function matchedUsers(markdown) { - const globalReg = /\B@([\w|]+)/g; - const reg = /\B@([\w\\_\\.]+)/; - - const users = markdown.match(globalReg); - if (!users) { - return []; - } - return users.map((user) => { - const matched = user.match(reg); - return { - userName: matched[1], - }; - }); -} - -/** - * Identify user infromation from markdown - * @param markdown string - * @returns string - */ -function parseUserInfo(markdown) { - const globalReg = /\B@([\w\\_\\.\\-]+)/g; - return markdown.replace(globalReg, '[@$1](/u/$1)'); -} - -export { - getQueryString, - thousandthDivision, - formatCount, - isLogin, - scrollTop, - matchedUsers, - parseUserInfo, -}; +export { default as request } from './request'; +export { default as Storage } from './storage'; +export { floppyNavigation } from './floppyNavigation'; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index d532d707..c9f9e67f 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -2,9 +2,17 @@ import axios, { AxiosResponse } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import { Modal } from '@answer/components'; -import { userInfoStore, toastStore } from '@answer/stores'; +import { loggedUserInfoStore, toastStore } from '@answer/stores'; import Storage from './storage'; +import { floppyNavigation } from './floppyNavigation'; + +import { + LOGGED_TOKEN_STORAGE_KEY, + CURRENT_LANG_STORAGE_KEY, + DEFAULT_LANG, +} from '@/common/constants'; +import { RouteAlias } from '@/router/alias'; const API = { development: '', @@ -25,12 +33,11 @@ class Request { constructor(config: AxiosRequestConfig) { this.instance = axios.create(config); - this.instance.interceptors.request.use( (requestConfig: AxiosRequestConfig) => { - const token = Storage.get('token') || ''; + const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || ''; // default lang en_US - const lang = Storage.get('LANG') || 'en_US'; + const lang = Storage.get(CURRENT_LANG_STORAGE_KEY) || DEFAULT_LANG; requestConfig.headers = { Authorization: token, 'Accept-Language': lang, @@ -54,23 +61,23 @@ class Request { return data; }, (error) => { - const { status, data, msg } = error.response; - const { data: realData, msg: realMsg = '' } = data; + const { status, data: respData, msg: respMsg } = error.response; + const { data, msg = '' } = respData; if (status === 400) { // show error message - if (realData instanceof Object && realData.err_type) { - if (realData.err_type === 'toast') { + if (data instanceof Object && data.err_type) { + if (data.err_type === 'toast') { // toast error message toastStore.getState().show({ - msg: realMsg, + msg, variant: 'danger', }); } - if (realData.type === 'modal') { + if (data.type === 'modal') { // modal error message Modal.confirm({ - content: realMsg, + content: msg, }); } @@ -78,63 +85,56 @@ class Request { } if ( - realData instanceof Object && - Object.keys(realData).length > 0 && - realData.key + data instanceof Object && + Object.keys(data).length > 0 && + data.key ) { // handle form error - return Promise.reject({ ...realData, isError: true }); + return Promise.reject({ ...data, isError: true }); } - if (!realData || Object.keys(realData).length <= 0) { + if (!data || Object.keys(data).length <= 0) { // default error msg will show modal Modal.confirm({ - content: realMsg, + content: msg, + }); + return Promise.reject(false); + } + } + // 401: Re-login required + if (status === 401) { + // clear userinfo + loggedUserInfoStore.getState().clear(); + floppyNavigation.navigateToLogin(); + return Promise.reject(false); + } + if (status === 403) { + // Permission interception + if (data?.type === 'url_expired') { + // url expired + floppyNavigation.navigate(RouteAlias.activationFailed, () => { + window.location.replace(RouteAlias.activationFailed); + }); + return Promise.reject(false); + } + if (data?.type === 'inactive') { + // inactivated + floppyNavigation.navigate(RouteAlias.activation, () => { + window.location.href = RouteAlias.activation; + }); + return Promise.reject(false); + } + + if (data?.type === 'suspended') { + floppyNavigation.navigate(RouteAlias.suspended, () => { + window.location.replace(RouteAlias.suspended); }); return Promise.reject(false); } } - if (status === 401) { - // clear userinfo; - Storage.remove('token'); - userInfoStore.getState().clear(); - // need login - const { pathname } = window.location; - if (pathname !== '/users/login' && pathname !== '/users/register') { - Storage.set('ANSWER_PATH', window.location.pathname); - } - window.location.href = '/users/login'; - - return Promise.reject(false); - } - - if (status === 403) { - // Permission interception - - if (realData?.type === 'inactive') { - // inactivated - window.location.href = '/users/login?status=inactive'; - return Promise.reject(false); - } - - if (realData?.type === 'url_expired') { - // url expired - window.location.href = '/users/account-activation/failed'; - return Promise.reject(false); - } - - if (realData?.type === 'suspended') { - if (window.location.pathname !== '/users/account-suspended') { - window.location.href = '/users/account-suspended'; - } - - return Promise.reject(false); - } - } - toastStore.getState().show({ - msg: `statusCode: ${status}; ${msg || ''}`, + msg: `statusCode: ${status}; ${respMsg || ''}`, variant: 'danger', }); return Promise.reject(false); @@ -178,6 +178,4 @@ class Request { } } -// export const Request; - export default new Request(baseConfig); diff --git a/ui/src/utils/storage.ts b/ui/src/utils/storage.ts index bf14d85e..0ab3b115 100644 --- a/ui/src/utils/storage.ts +++ b/ui/src/utils/storage.ts @@ -3,13 +3,12 @@ const Storage = { const value = localStorage.getItem(key); if (value) { try { - const v = JSON.parse(value); - return v; + return JSON.parse(value); } catch { return value; } } - return false; + return undefined; }, set: (key: string, value: any): void => { if (typeof value === 'string') { diff --git a/ui/tsconfig.json b/ui/tsconfig.json index c3804747..d7c5decf 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -26,7 +26,6 @@ "@answer/components/*": ["src/components/*"], "@answer/stores": ["src/stores"], "@answer/stores/*": ["src/stores/*"], - "@answer/api": ["src/services/api.ts"], "@answer/services/*": ["src/services/*"], "@answer/hooks": ["src/hooks"], "@answer/common": ["src/common"], From e2cf6d2b3d8fd87dd6eb9bbfdcd78598aff5ec7b Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Mon, 31 Oct 2022 10:34:51 +0800 Subject: [PATCH 002/157] feat(router-guard): set route guard for activation/* and confirm-new-email --- ui/src/router/guarder.ts | 4 ---- ui/src/router/routes.ts | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/src/router/guarder.ts b/ui/src/router/guarder.ts index 8fd8a69b..8c5bf7c2 100644 --- a/ui/src/router/guarder.ts +++ b/ui/src/router/guarder.ts @@ -2,7 +2,6 @@ import { pullLoggedUser, isLoggedAndNormal, isAdminLogged, - isLogged, isNotLogged, isNotLoggedOrNormal, isLoggedAndInactive, @@ -14,9 +13,6 @@ const RouteGuarder = { base: async () => { return isNotLoggedOrNormal(); }, - logged: async () => { - return isLogged(); - }, notLogged: async () => { return isNotLogged(); }, diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index ad964c38..5abf7e4b 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -126,9 +126,11 @@ const routes: RouteNode[] = [ page: 'pages/Users/PasswordReset', guard: RouteGuarder.loggedAndNormal, }, + // TODO: guard '/account-activation/*', '/users/confirm-new-email' { path: 'users/account-activation', page: 'pages/Users/ActiveEmail', + guard: RouteGuarder.loggedAndInactive, }, { path: 'users/account-activation/success', @@ -145,6 +147,7 @@ const routes: RouteNode[] = [ { path: '/users/account-suspended', page: 'pages/Users/Suspended', + guard: RouteGuarder.loggedAndSuspended, }, // for admin { From d3cd9a94dd733310aafb8365d53969b8fcf9ef07 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Mon, 31 Oct 2022 11:39:33 +0800 Subject: [PATCH 003/157] refactor(import): fix import order --- ui/src/components/QuestionList/index.tsx | 3 ++- ui/src/components/TagSelector/index.tsx | 3 ++- ui/src/components/Unactivate/index.tsx | 2 +- ui/src/hooks/useChangeModal/index.tsx | 3 ++- ui/src/pages/Admin/Answers/index.tsx | 3 ++- ui/src/pages/Admin/Flags/index.tsx | 1 + ui/src/pages/Admin/General/index.tsx | 1 + ui/src/pages/Admin/Interface/index.tsx | 5 +++-- ui/src/pages/Admin/Questions/index.tsx | 3 ++- ui/src/pages/Admin/Users/index.tsx | 3 ++- ui/src/pages/Layout/index.tsx | 2 +- ui/src/pages/Questions/Ask/index.tsx | 7 ++++--- .../Questions/Detail/components/Answer/index.tsx | 3 ++- .../Questions/Detail/components/Question/index.tsx | 1 + .../Detail/components/RelatedQuestions/index.tsx | 2 +- .../Questions/Detail/components/WriteAnswer/index.tsx | 3 ++- ui/src/pages/Questions/Detail/index.tsx | 3 ++- ui/src/pages/Questions/EditAnswer/index.tsx | 3 ++- ui/src/pages/Search/index.tsx | 3 ++- ui/src/pages/Tags/Detail/index.tsx | 2 +- ui/src/pages/Tags/Edit/index.tsx | 3 ++- ui/src/pages/Tags/Info/index.tsx | 1 + ui/src/pages/Tags/index.tsx | 3 ++- .../Users/AccountForgot/components/sendEmail.tsx | 2 +- ui/src/pages/Users/ActiveEmail/index.tsx | 2 +- ui/src/pages/Users/ConfirmNewEmail/index.tsx | 1 - ui/src/pages/Users/Notifications/index.tsx | 9 +++++---- ui/src/pages/Users/Personal/index.tsx | 11 ++++++----- .../Users/Register/components/SignUpForm/index.tsx | 2 +- .../Settings/Account/components/ModifyEmail/index.tsx | 3 ++- .../Settings/Account/components/ModifyPass/index.tsx | 3 ++- ui/src/pages/Users/Settings/Interface/index.tsx | 2 +- ui/src/pages/Users/Settings/Notification/index.tsx | 3 ++- ui/src/pages/Users/Settings/Profile/index.tsx | 3 ++- ui/src/pages/Users/Settings/index.tsx | 2 +- 35 files changed, 65 insertions(+), 41 deletions(-) diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 69c6ad9d..1be1f847 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -3,7 +3,6 @@ import { Row, Col, ListGroup } from 'react-bootstrap'; import { NavLink, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQuestionList } from '@/services'; import type * as Type from '@answer/common/interface'; import { Icon, @@ -15,6 +14,8 @@ import { QueryGroup, } from '@answer/components'; +import { useQuestionList } from '@/services'; + const QuestionOrderKeys: Type.QuestionOrderBy[] = [ 'newest', 'active', diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index f939b674..cedb85a3 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -6,9 +6,10 @@ import { marked } from 'marked'; import classNames from 'classnames'; import { useTagModal } from '@answer/hooks'; -import { queryTags } from '@/services'; import type * as Type from '@answer/common/interface'; +import { queryTags } from '@/services'; + import './index.scss'; interface IProps { diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index dd44f68f..eeb3a1af 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import { Button, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { resendEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@answer/components/Modal'; import type { ImgCodeRes, @@ -11,6 +10,7 @@ import type { } from '@answer/common/interface'; import { loggedUserInfoStore } from '@answer/stores'; +import { resendEmail, checkImgCode } from '@/services'; import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/hooks/useChangeModal/index.tsx b/ui/src/hooks/useChangeModal/index.tsx index 76b9f2ea..f484331c 100644 --- a/ui/src/hooks/useChangeModal/index.tsx +++ b/ui/src/hooks/useChangeModal/index.tsx @@ -4,9 +4,10 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { changeUserStatus } from '@/services'; import { Modal as AnswerModal } from '@answer/components'; +import { changeUserStatus } from '@/services'; + const div = document.createElement('div'); const root = ReactDOM.createRoot(div); diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index ead61b57..2a25f53c 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -14,9 +14,10 @@ import { } from '@answer/components'; import { ADMIN_LIST_STATUS } from '@answer/common/constants'; import { useEditStatusModal } from '@answer/hooks'; -import { useAnswerSearch, changeAnswerStatus } from '@/services'; import * as Type from '@answer/common/interface'; +import { useAnswerSearch, changeAnswerStatus } from '@/services'; + import '../index.scss'; const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted']; diff --git a/ui/src/pages/Admin/Flags/index.tsx b/ui/src/pages/Admin/Flags/index.tsx index ed10eaa5..a7842429 100644 --- a/ui/src/pages/Admin/Flags/index.tsx +++ b/ui/src/pages/Admin/Flags/index.tsx @@ -12,6 +12,7 @@ import { } from '@answer/components'; import { useReportModal } from '@answer/hooks'; import * as Type from '@answer/common/interface'; + import { useFlagSearch } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 9caf81e8..3154caec 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; import { useToast } from '@answer/hooks'; import { siteInfoStore } from '@answer/stores'; + import { useGeneralSetting, updateGeneralSetting } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index e22d1046..7c83e10e 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -8,6 +8,9 @@ import { FormDataType, AdminSettingsInterface, } from '@answer/common/interface'; +import { interfaceStore } from '@answer/stores'; +import { UploadImg } from '@answer/components'; + import { languages, uploadAvatar, @@ -15,8 +18,6 @@ import { useInterfaceSetting, useThemeOptions, } from '@/services'; -import { interfaceStore } from '@answer/stores'; -import { UploadImg } from '@answer/components'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 3235a819..1a188512 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -14,12 +14,13 @@ import { } from '@answer/components'; import { ADMIN_LIST_STATUS } from '@answer/common/constants'; import { useEditStatusModal, useReportModal } from '@answer/hooks'; +import * as Type from '@answer/common/interface'; + import { useQuestionSearch, changeQuestionStatus, deleteQuestion, } from '@/services'; -import * as Type from '@answer/common/interface'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index b4efe613..825e7f23 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -3,7 +3,6 @@ import { Button, Form, Table, Badge } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQueryUsers } from '@/services'; import { Pagination, FormatTime, @@ -14,6 +13,8 @@ import { import * as Type from '@answer/common/interface'; import { useChangeModal } from '@answer/hooks'; +import { useQueryUsers } from '@/services'; + import '../index.scss'; const UserFilterKeys: Type.UserFilterBy[] = [ diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index bad408bf..7affe2f0 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -7,8 +7,8 @@ import { SWRConfig } from 'swr'; import { siteInfoStore, interfaceStore, toastStore } from '@answer/stores'; import { Header, AdminHeader, Footer, Toast } from '@answer/components'; -import { useSiteSettings } from '@/services'; +import { useSiteSettings } from '@/services'; import Storage from '@/utils/storage'; import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index f202e0dc..f233150e 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -7,6 +7,10 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { Editor, EditorRef, TagSelector, PageTitle } from '@answer/components'; +import type * as Type from '@answer/common/interface'; + +import SearchQuestion from './components/SearchQuestion'; + import { saveQuestion, questionDetail, @@ -15,9 +19,6 @@ import { postAnswer, useQueryQuestionByTitle, } from '@/services'; -import type * as Type from '@answer/common/interface'; - -import SearchQuestion from './components/SearchQuestion'; interface FormDataItem { title: Type.FormValue; diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index 4ffed114..e97238cf 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -11,10 +11,11 @@ import { FormatTime, htmlRender, } from '@answer/components'; -import { acceptanceAnswer } from '@/services'; import { scrollTop } from '@answer/utils'; import { AnswerItem } from '@answer/common/interface'; +import { acceptanceAnswer } from '@/services'; + interface Props { data: AnswerItem; /** router answer id */ diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index 10c18a47..aa3a38ec 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -13,6 +13,7 @@ import { htmlRender, } from '@answer/components'; import { formatCount } from '@answer/utils'; + import { following } from '@/services'; interface Props { diff --git a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx index c6cf29d7..8493408c 100644 --- a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx @@ -3,9 +3,9 @@ import { Card, ListGroup } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useSimilarQuestion } from '@/services'; import { Icon } from '@answer/components'; +import { useSimilarQuestion } from '@/services'; import { loggedUserInfoStore } from '@/stores'; interface Props { diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx index d18eb4a8..9659b286 100644 --- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx @@ -6,9 +6,10 @@ import { marked } from 'marked'; import classNames from 'classnames'; import { Editor, Modal } from '@answer/components'; -import { postAnswer } from '@/services'; import { FormDataType } from '@answer/common/interface'; +import { postAnswer } from '@/services'; + interface Props { visible?: boolean; data: { diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 0eb6a15f..2f469415 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; -import { questionDetail, getAnswers } from '@/services'; import { Pagination, PageTitle } from '@answer/components'; import { loggedUserInfoStore } from '@answer/stores'; import { scrollTop } from '@answer/utils'; @@ -22,6 +21,8 @@ import { Alert, } from './components'; +import { questionDetail, getAnswers } from '@/services'; + import './index.scss'; const Index = () => { diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index 6a3d5127..c87733f4 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -7,12 +7,13 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { Editor, EditorRef, Icon, PageTitle } from '@answer/components'; +import type * as Type from '@answer/common/interface'; + import { useQueryAnswerInfo, modifyAnswer, useQueryRevisions, } from '@/services'; -import type * as Type from '@answer/common/interface'; import './index.scss'; diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index 0582684d..914565ae 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -4,10 +4,11 @@ import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { Pagination, PageTitle } from '@answer/components'; -import { useSearch } from '@/services'; import { Head, SearchHead, SearchItem, Tips, Empty } from './components'; +import { useSearch } from '@/services'; + const Index = () => { const { t } = useTranslation('translation'); const [searchParams] = useSearchParams(); diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index 3098108c..e37ab0ab 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next'; import * as Type from '@answer/common/interface'; import { PageTitle, FollowingTags } from '@answer/components'; -import { useTagInfo, useFollow } from '@/services'; +import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; diff --git a/ui/src/pages/Tags/Edit/index.tsx b/ui/src/pages/Tags/Edit/index.tsx index 12019642..58e7d59c 100644 --- a/ui/src/pages/Tags/Edit/index.tsx +++ b/ui/src/pages/Tags/Edit/index.tsx @@ -7,10 +7,11 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { Editor, EditorRef, PageTitle } from '@answer/components'; -import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; import { loggedUserInfoStore } from '@answer/stores'; import type * as Type from '@answer/common/interface'; +import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; + interface FormDataItem { displayName: Type.FormValue; slugName: Type.FormValue; diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 31c8ac29..d8c4ad75 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -12,6 +12,7 @@ import { Modal, PageTitle, } from '@answer/components'; + import { useTagInfo, useQuerySynonymsTags, diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index 63fc7276..80256d94 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -3,10 +3,11 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQueryTags, following } from '@/services'; import { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components'; import { formatCount } from '@answer/utils'; +import { useQueryTags, following } from '@/services'; + const sortBtns = ['popular', 'name', 'newest']; const Tags = () => { diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index e9619ced..e95b1354 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -2,13 +2,13 @@ import { FC, memo, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { resetPassword, checkImgCode } from '@/services'; import type { ImgCodeRes, PasswordResetReq, FormDataType, } from '@answer/common/interface'; +import { resetPassword, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; interface IProps { diff --git a/ui/src/pages/Users/ActiveEmail/index.tsx b/ui/src/pages/Users/ActiveEmail/index.tsx index e2b3491e..f50fbc3e 100644 --- a/ui/src/pages/Users/ActiveEmail/index.tsx +++ b/ui/src/pages/Users/ActiveEmail/index.tsx @@ -1,10 +1,10 @@ import { FC, memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { activateAccount } from '@/services'; import { loggedUserInfoStore } from '@answer/stores'; import { getQueryString } from '@answer/utils'; +import { activateAccount } from '@/services'; import { PageTitle } from '@/components'; const Index: FC = () => { diff --git a/ui/src/pages/Users/ConfirmNewEmail/index.tsx b/ui/src/pages/Users/ConfirmNewEmail/index.tsx index 9adaf80e..d849ffa5 100644 --- a/ui/src/pages/Users/ConfirmNewEmail/index.tsx +++ b/ui/src/pages/Users/ConfirmNewEmail/index.tsx @@ -4,7 +4,6 @@ import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { changeEmailVerify } from '@/services'; - import { PageTitle } from '@/components'; const Index: FC = () => { diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index 687e7773..47e69a04 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -3,16 +3,17 @@ import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useParams, useNavigate } from 'react-router-dom'; +import { PageTitle } from '@answer/components'; + +import Inbox from './components/Inbox'; +import Achievements from './components/Achievements'; + import { useQueryNotifications, clearUnreadNotification, clearNotificationStatus, readNotification, } from '@/services'; -import { PageTitle } from '@answer/components'; - -import Inbox from './components/Inbox'; -import Achievements from './components/Achievements'; const PAGE_SIZE = 10; diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index fa8c2200..fcab49ff 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -5,11 +5,6 @@ import { useParams, useSearchParams } from 'react-router-dom'; import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components'; import { loggedUserInfoStore } from '@answer/stores'; -import { - usePersonalInfoByName, - usePersonalTop, - usePersonalListByTabName, -} from '@/services'; import { UserInfo, @@ -24,6 +19,12 @@ import { Votes, } from './components'; +import { + usePersonalInfoByName, + usePersonalTop, + usePersonalListByTabName, +} from '@/services'; + const Personal: FC = () => { const { tabName = 'overview', username = '' } = useParams(); const [searchParams] = useSearchParams(); diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx index 08afacf2..4f072fe7 100644 --- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx +++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx @@ -3,9 +3,9 @@ import { Form, Button, Col } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { register } from '@/services'; import type { FormDataType } from '@answer/common/interface'; +import { register } from '@/services'; import userStore from '@/stores/userInfo'; interface Props { diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index 495fd1d4..4ed6b0ff 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -3,9 +3,10 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; -import { getLoggedUserInfo, changeEmail } from '@/services'; import { useToast } from '@answer/hooks'; +import { getLoggedUserInfo, changeEmail } from '@/services'; + const reg = /(?<=.{2}).+(?=@)/gi; const Index: FC = () => { diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx index eac17839..2c7ed9ef 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx @@ -2,10 +2,11 @@ import React, { FC, FormEvent, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { modifyPassword } from '@/services'; import { useToast } from '@answer/hooks'; import type { FormDataType } from '@answer/common/interface'; +import { modifyPassword } from '@/services'; + const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.account', diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index a256a65f..f38f92e8 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -6,10 +6,10 @@ import dayjs from 'dayjs'; import en from 'dayjs/locale/en'; import zh from 'dayjs/locale/zh-cn'; -import { languages } from '@/services'; import type { LangsType, FormDataType } from '@answer/common/interface'; import { useToast } from '@answer/hooks'; +import { languages } from '@/services'; import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/pages/Users/Settings/Notification/index.tsx b/ui/src/pages/Users/Settings/Notification/index.tsx index 8015901a..6d0cab61 100644 --- a/ui/src/pages/Users/Settings/Notification/index.tsx +++ b/ui/src/pages/Users/Settings/Notification/index.tsx @@ -3,9 +3,10 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import type { FormDataType } from '@answer/common/interface'; -import { setNotice, getLoggedUserInfo } from '@/services'; import { useToast } from '@answer/hooks'; +import { setNotice, getLoggedUserInfo } from '@/services'; + const Index = () => { const toast = useToast(); const { t } = useTranslation('translation', { diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index ecfd3449..661c222a 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -4,12 +4,13 @@ import { Trans, useTranslation } from 'react-i18next'; import { marked } from 'marked'; -import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import { UploadImg, Avatar } from '@answer/components'; import { loggedUserInfoStore } from '@answer/stores'; import { useToast } from '@answer/hooks'; +import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services'; + const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.profile', diff --git a/ui/src/pages/Users/Settings/index.tsx b/ui/src/pages/Users/Settings/index.tsx index 8967cd4e..671707fe 100644 --- a/ui/src/pages/Users/Settings/index.tsx +++ b/ui/src/pages/Users/Settings/index.tsx @@ -3,11 +3,11 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { getLoggedUserInfo } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import Nav from './components/Nav'; +import { getLoggedUserInfo } from '@/services'; import { PageTitle } from '@/components'; const Index: React.FC = () => { From 04b1b46ef25975f6cbfeea40d8fe4d6a88fad610 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Mon, 31 Oct 2022 16:10:12 +0800 Subject: [PATCH 004/157] refactor(services): update some service import issue --- ui/src/pages/Admin/Smtp/index.tsx | 2 +- ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx | 6 +++--- ui/src/pages/Users/ConfirmNewEmail/index.tsx | 9 ++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx index f6714255..17abd481 100644 --- a/ui/src/pages/Admin/Smtp/index.tsx +++ b/ui/src/pages/Admin/Smtp/index.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; import { useToast } from '@answer/hooks'; -import { useSmtpSetting, updateSmtpSetting } from '@answer/api'; +import { useSmtpSetting, updateSmtpSetting } from '@/services'; import pattern from '@/common/pattern'; const Smtp: FC = () => { diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx index 28de25c5..4d419091 100644 --- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx +++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx @@ -3,14 +3,14 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { changeEmail, checkImgCode } from '@answer/api'; import type { ImgCodeRes, PasswordResetReq, FormDataType, } from '@answer/common/interface'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; +import { changeEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; const Index: FC = () => { @@ -34,7 +34,7 @@ const Index: FC = () => { }); const [showModal, setModalState] = useState(false); const navigate = useNavigate(); - const { user: userInfo, update: updateUser } = userInfoStore(); + const { user: userInfo, update: updateUser } = loggedUserInfoStore(); const getImgCode = () => { checkImgCode({ diff --git a/ui/src/pages/Users/ConfirmNewEmail/index.tsx b/ui/src/pages/Users/ConfirmNewEmail/index.tsx index 41695a89..79b55ee7 100644 --- a/ui/src/pages/Users/ConfirmNewEmail/index.tsx +++ b/ui/src/pages/Users/ConfirmNewEmail/index.tsx @@ -3,9 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap'; import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { changeEmailVerify, getUserInfo } from '@/services'; -import { userInfoStore } from '@answer/stores'; - +import { loggedUserInfoStore } from '@/stores'; +import { changeEmailVerify, getLoggedUserInfo } from '@/services'; import { PageTitle } from '@/components'; const Index: FC = () => { @@ -13,7 +12,7 @@ const Index: FC = () => { const [searchParams] = useSearchParams(); const [step, setStep] = useState('loading'); - const updateUser = userInfoStore((state) => state.update); + const updateUser = loggedUserInfoStore((state) => state.update); useEffect(() => { const code = searchParams.get('code'); @@ -22,7 +21,7 @@ const Index: FC = () => { changeEmailVerify({ code }) .then(() => { setStep('success'); - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { // update user info updateUser(res); }); From d72425b343043abc42291df22c99111afe36ea19 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Mon, 31 Oct 2022 18:40:02 +0800 Subject: [PATCH 005/157] fix(router-guard): fix some router guard logic --- ui/src/router/guarder.ts | 18 +++++++++++------- ui/src/utils/guards.ts | 34 ++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/ui/src/router/guarder.ts b/ui/src/router/guarder.ts index 8c5bf7c2..e3b81598 100644 --- a/ui/src/router/guarder.ts +++ b/ui/src/router/guarder.ts @@ -7,17 +7,12 @@ import { isLoggedAndInactive, isLoggedAndSuspended, isNotLoggedOrInactive, + isNotLoggedOrNotSuspend, } from '@/utils/guards'; const RouteGuarder = { base: async () => { - return isNotLoggedOrNormal(); - }, - notLogged: async () => { - return isNotLogged(); - }, - notLoggedOrInactive: async () => { - return isNotLoggedOrInactive(); + return isNotLoggedOrNotSuspend(); }, loggedAndNormal: async () => { await pullLoggedUser(true); @@ -33,6 +28,15 @@ const RouteGuarder = { await pullLoggedUser(true); return isAdminLogged(); }, + notLogged: async () => { + return isNotLogged(); + }, + notLoggedOrNormal: async () => { + return isNotLoggedOrNormal(); + }, + notLoggedOrInactive: async () => { + return isNotLoggedOrInactive(); + }, }; export default RouteGuarder; diff --git a/ui/src/utils/guards.ts b/ui/src/utils/guards.ts index f0faf55b..47f79605 100644 --- a/ui/src/utils/guards.ts +++ b/ui/src/utils/guards.ts @@ -94,21 +94,21 @@ export const isNotLogged = () => { }; export const isLoggedAndInactive = () => { - const ret: GuardResult = { ok: false, redirect: undefined }; + const ret: GuardResult = { ok: true, redirect: undefined }; const userStat = deriveUserStat(); - if (!userStat.isActivated) { - ret.ok = true; - ret.redirect = RouteAlias.activation; + if (userStat.isActivated) { + ret.ok = false; + ret.redirect = RouteAlias.home; } return ret; }; export const isLoggedAndSuspended = () => { - const ret: GuardResult = { ok: false, redirect: undefined }; + const ret: GuardResult = { ok: true, redirect: undefined }; const userStat = deriveUserStat(); - if (userStat.isSuspended) { - ret.redirect = RouteAlias.suspended; - ret.ok = true; + if (!userStat.isSuspended) { + ret.ok = false; + ret.redirect = RouteAlias.home; } return ret; }; @@ -120,7 +120,7 @@ export const isLoggedAndNormal = () => { ret.ok = true; } else if (!userStat.isActivated) { ret.redirect = RouteAlias.activation; - } else if (!userStat.isSuspended) { + } else if (userStat.isSuspended) { ret.redirect = RouteAlias.suspended; } else if (!userStat.isLogged) { ret.redirect = RouteAlias.login; @@ -139,12 +139,26 @@ export const isNotLoggedOrNormal = () => { return ret; }; +export const isNotLoggedOrNotSuspend = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + const gr = isLoggedAndNormal(); + if (!gr.ok && userStat.isSuspended) { + ret.ok = false; + ret.redirect = gr.redirect; + } + return ret; +}; + export const isNotLoggedOrInactive = () => { const ret: GuardResult = { ok: true, redirect: undefined }; const userStat = deriveUserStat(); - if (userStat.isLogged || userStat.isActivated) { + if (userStat.isActivated) { ret.ok = false; ret.redirect = RouteAlias.home; + } else if (userStat.isSuspended) { + ret.ok = false; + ret.redirect = RouteAlias.suspended; } return ret; }; From 2ad6a40d764a89c333381caa9309b56aec815859 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 31 Oct 2022 18:48:27 +0800 Subject: [PATCH 006/157] chore: add install pages --- ui/src/i18n/locales/en.json | 60 +++++++++++++++++++ .../Install/components/FifthStep/index.tsx | 33 ++++++++++ .../Install/components/FirstStep/index.tsx | 31 ++++++++++ .../Install/components/FourthStep/index.tsx | 53 ++++++++++++++++ .../Install/components/Progress/index.tsx | 22 +++++++ .../Install/components/SecondStep/index.tsx | 57 ++++++++++++++++++ .../Install/components/ThirdStep/index.tsx | 39 ++++++++++++ ui/src/pages/Install/components/index.ts | 7 +++ ui/src/pages/Install/index.tsx | 44 ++++++++++++++ ui/src/router/routes.ts | 4 ++ 10 files changed, 350 insertions(+) create mode 100644 ui/src/pages/Install/components/FifthStep/index.tsx create mode 100644 ui/src/pages/Install/components/FirstStep/index.tsx create mode 100644 ui/src/pages/Install/components/FourthStep/index.tsx create mode 100644 ui/src/pages/Install/components/Progress/index.tsx create mode 100644 ui/src/pages/Install/components/SecondStep/index.tsx create mode 100644 ui/src/pages/Install/components/ThirdStep/index.tsx create mode 100644 ui/src/pages/Install/components/index.ts create mode 100644 ui/src/pages/Install/index.tsx diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 94277ee9..0bf94f19 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -732,6 +732,66 @@ "x_answers": "answers", "x_questions": "questions" }, + "install": { + "title": "Answer", + "next": "Next", + "done": "Done", + "choose_lang": { + "label": "Please choose a language" + }, + "database_engine": { + "label": "Database Engine" + }, + "username": { + "label": "Username", + "placeholder": "root" + }, + "password": { + "label": "Password", + "placeholder": "root" + }, + "database_host": { + "label": "Database Host", + "placeholder": "db:3306" + }, + "database_name": { + "label": "Database Name", + "placeholder": "answer" + }, + "table_prefix": { + "label": "Table Prefix (optional)", + "placeholder": "answer_" + }, + "config_yaml": { + "title": "Create config.yaml", + "label": "The config.yaml file created.", + "description": "You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it.", + "info": "After you’ve done that, click “Next” button." + }, + "site_information": "Site Information", + "admin_account": "Admin Account", + "site_name": { + "label": "Site Name" + }, + "contact_email": { + "label": "Contact Email", + "text": "Email address of key contact responsible for this site." + }, + "admin_name": { + "label": "Name" + }, + "admin_password": { + "label": "Password", + "text": "You will need this password to log in. Please store it in a secure location." + }, + "admin_email": { + "label": "Email", + "text": "You will need this email to log in." + }, + "ready_title": "Your Answer is Ready!", + "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", + "good_luck": "Have fun, and good luck!" + }, "page_404": { "description": "Unfortunately, this page doesn't exist.", "back_home": "Back to homepage" diff --git a/ui/src/pages/Install/components/FifthStep/index.tsx b/ui/src/pages/Install/components/FifthStep/index.tsx new file mode 100644 index 00000000..63f008e0 --- /dev/null +++ b/ui/src/pages/Install/components/FifthStep/index.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import { Button } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+
{t('ready_title')}
+

+ + If you ever feel like changing more settings, visit + admin section; find it in the site menu. + +

+

{t('good_luck')}

+ +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx new file mode 100644 index 00000000..156f568d --- /dev/null +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( + + + {t('choose_lang.label')} + + + + + +
+ + +
+ + ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx new file mode 100644 index 00000000..25fb47ef --- /dev/null +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -0,0 +1,53 @@ +import { FC } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+
{t('site_information')}
+ + {t('site_name.label')} + + + + {t('contact_email.label')} + + {t('contact_email.text')} + + +
{t('admin_account')}
+ + {t('admin_name.label')} + + + + + {t('admin_password.label')} + + {t('admin_password.text')} + + + + {t('admin_email.label')} + + {t('admin_email.text')} + + +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/Progress/index.tsx b/ui/src/pages/Install/components/Progress/index.tsx new file mode 100644 index 00000000..97f33ffe --- /dev/null +++ b/ui/src/pages/Install/components/Progress/index.tsx @@ -0,0 +1,22 @@ +import { FC, memo } from 'react'; +import { ProgressBar } from 'react-bootstrap'; + +interface IProps { + step: number; +} + +const Index: FC = ({ step }) => { + return ( +
+ + {step}/5 +
+ ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx new file mode 100644 index 00000000..7b21ab73 --- /dev/null +++ b/ui/src/pages/Install/components/SecondStep/index.tsx @@ -0,0 +1,57 @@ +import { FC } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} + +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+ + {t('database_engine.label')} + + + + + + + {t('username.label')} + + + + + {t('password.label')} + + + + + {t('database_host.label')} + + + + + {t('database_name.label')} + + + + + {t('table_prefix.label')} + + + +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/ThirdStep/index.tsx b/ui/src/pages/Install/components/ThirdStep/index.tsx new file mode 100644 index 00000000..4d3f702f --- /dev/null +++ b/ui/src/pages/Install/components/ThirdStep/index.tsx @@ -0,0 +1,39 @@ +import { FC } from 'react'; +import { Form, Button, FormGroup } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} + +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+
{t('config_yaml.title')}
+
{t('config_yaml.label')}
+
+

+ }} + /> +

+
+ + + +
{t('config_yaml.info')}
+
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/index.ts b/ui/src/pages/Install/components/index.ts new file mode 100644 index 00000000..ecc22539 --- /dev/null +++ b/ui/src/pages/Install/components/index.ts @@ -0,0 +1,7 @@ +import FirstStep from './FirstStep'; +import SecondStep from './SecondStep'; +import ThirdStep from './ThirdStep'; +import FourthStep from './FourthStep'; +import Fifth from './FifthStep'; + +export { FirstStep, SecondStep, ThirdStep, FourthStep, Fifth }; diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx new file mode 100644 index 00000000..e1425893 --- /dev/null +++ b/ui/src/pages/Install/index.tsx @@ -0,0 +1,44 @@ +import { FC, useState } from 'react'; +import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import { + FirstStep, + SecondStep, + ThirdStep, + FourthStep, + Fifth, +} from './components'; + +const Index: FC = () => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + const [step] = useState(5); + + return ( +
+ + + +

{t('title')}

+ + + show error msg + + + + + + + + + + + + +
+
+
+ ); +}; + +export default Index; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 5b49fb1a..69498f5d 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -211,5 +211,9 @@ const routes: RouteNode[] = [ }, ], }, + { + path: 'install', + page: 'pages/Install', + }, ]; export default routes; From 05f48669092a8c7d8cc1580be1bf54010f8a0c7d Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 1 Nov 2022 10:04:08 +0800 Subject: [PATCH 007/157] fix(notifications): fix message disappear when reclick notification type --- ui/src/pages/Users/Notifications/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index 47e69a04..b24367b7 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -47,6 +47,9 @@ const Notifications = () => { const handleTypeChange = (evt, val) => { evt.preventDefault(); + if (type === val) { + return; + } setPage(1); setNotificationData([]); navigate(`/users/notifications/${val}`); From e4f08e138675beb97552a2c1e8f68ad2d94ddd06 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 1 Nov 2022 10:36:19 +0800 Subject: [PATCH 008/157] fix(router): add general route error element --- ui/src/router/index.tsx | 2 ++ ui/src/utils/request.ts | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index a7452ff8..84eb37a6 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -2,6 +2,7 @@ import React, { Suspense, lazy } from 'react'; import { RouteObject, createBrowserRouter, redirect } from 'react-router-dom'; import Layout from '@/pages/Layout'; +import ErrorBoundary from '@/pages/50X'; import baseRoutes, { RouteNode } from '@/router/routes'; import { floppyNavigation } from '@/utils'; @@ -11,6 +12,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => { routeNodes.forEach((rn) => { if (rn.path === '/') { rn.element = ; + rn.errorElement = ; } else { /** * cannot use a fully dynamic import statement diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index c9f9e67f..d80cadf5 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -132,11 +132,12 @@ class Request { return Promise.reject(false); } } - - toastStore.getState().show({ - msg: `statusCode: ${status}; ${respMsg || ''}`, - variant: 'danger', - }); + if (respMsg) { + toastStore.getState().show({ + msg: `statusCode: ${status}; ${respMsg || ''}`, + variant: 'danger', + }); + } return Promise.reject(false); }, ); From a741bd5cfc40df72ed6b7623016b4b425e086a72 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 1 Nov 2022 11:34:39 +0800 Subject: [PATCH 009/157] fix: add upgrad and maintenance page --- ui/src/i18n/locales/en.json | 23 +++++++++++-- ui/src/index.scss | 3 ++ ui/src/pages/Install/index.tsx | 27 ++++++++++++++-- ui/src/pages/Maintenance/index.tsx | 23 +++++++++++++ ui/src/pages/Upgrade/index.tsx | 52 ++++++++++++++++++++++++++++++ ui/src/router/routes.ts | 8 +++++ 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 ui/src/pages/Maintenance/index.tsx create mode 100644 ui/src/pages/Upgrade/index.tsx diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 0bf94f19..ecbb7b7c 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -28,7 +28,10 @@ "confirm_email": "Confirm Email", "account_suspended": "Account Suspended", "admin": "Admin", - "change_email": "Modify Email" + "change_email": "Modify Email", + "install": "Answer Installation", + "upgrade": "Answer Upgrade", + "maintenance": "Webite Maintenance" }, "notifications": { "title": "Notifications", @@ -790,7 +793,20 @@ }, "ready_title": "Your Answer is Ready!", "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", - "good_luck": "Have fun, and good luck!" + "good_luck": "Have fun, and good luck!", + "warning": "Warning", + "warning_description": "The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now.", + "installed": "Already installed", + "installed_description": "You appear to have already installed. To reinstall please clear your old database tables first." + }, + "upgrade": { + "title": "Answer", + "update_btn": "Update data", + "update_title": "Data update required", + "update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.<1>The update process may take a little while, so please be patient.", + "done_title": "No update required", + "done_btn": "Done", + "done_desscription": "Your Answer data is already up-to-date." }, "page_404": { "description": "Unfortunately, this page doesn't exist.", @@ -800,6 +816,9 @@ "description": "The server encountered an error and could not complete your request.", "back_home": "Back to homepage" }, + "page_maintenance": { + "description": "We are under maintenance, we’ll be back soon." + }, "admin": { "admin_header": { "title": "Admin" diff --git a/ui/src/index.scss b/ui/src/index.scss index e81a900c..dd25289f 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -77,6 +77,9 @@ a { .page-wrap { min-height: calc(100vh - 148px); } +.page-wrap2 { + min-height: 100vh; +} .btn-no-border, .btn-no-border:hover, diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index e1425893..8de7cb11 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -1,6 +1,6 @@ import { FC, useState } from 'react'; import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; -import { useTranslation } from 'react-i18next'; +import { useTranslation, Trans } from 'react-i18next'; import { FirstStep, @@ -10,12 +10,15 @@ import { Fifth, } from './components'; +import { PageTitle } from '@/components'; + const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step] = useState(5); + const [step] = useState(7); return (
+ @@ -32,6 +35,26 @@ const Index: FC = () => { + {step === 6 && ( +
+
{t('warning')}
+

+ + The file config.yaml already exists. If you + need to reset any of the configuration items in this + file, please delete it first. You may try{' '} + installing now. + +

+
+ )} + + {step === 7 && ( +
+
{t('installed')}
+

{t('installed_description')}

+
+ )} diff --git a/ui/src/pages/Maintenance/index.tsx b/ui/src/pages/Maintenance/index.tsx new file mode 100644 index 00000000..3a2c2d86 --- /dev/null +++ b/ui/src/pages/Maintenance/index.tsx @@ -0,0 +1,23 @@ +import { Container } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import { PageTitle } from '@/components'; + +const Index = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'page_maintenance', + }); + return ( + + +
+ (=‘_‘=) +
+
{t('description')}
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Upgrade/index.tsx b/ui/src/pages/Upgrade/index.tsx new file mode 100644 index 00000000..aee2a37a --- /dev/null +++ b/ui/src/pages/Upgrade/index.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react'; +import { Container, Row, Col, Card, Button } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import { PageTitle } from '@/components'; + +const Index = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'upgrade', + }); + const [step, setStep] = useState(1); + + const handleUpdate = () => { + setStep(2); + }; + return ( + + + + +

{t('title')}

+ + + {step === 1 && ( + <> +
{t('update_title')}
+ }} + /> + + + )} + + {step === 2 && ( + <> +
{t('done_title')}
+

{t('done_desscription')}

+ + + )} +
+
+ +
+
+ ); +}; + +export default Index; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 69498f5d..9436298a 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -215,5 +215,13 @@ const routes: RouteNode[] = [ path: 'install', page: 'pages/Install', }, + { + path: '/maintenance', + page: 'pages/Maintenance', + }, + { + path: '/upgrade', + page: 'pages/Upgrade', + }, ]; export default routes; From 2ff08c8b43abc66a163449abc258212214510dfd Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 1 Nov 2022 16:26:42 +0800 Subject: [PATCH 010/157] feat(admin): question,answer,users adds filter --- ui/src/common/interface.ts | 2 ++ ui/src/pages/Admin/Answers/index.tsx | 14 +++++++++++--- ui/src/pages/Admin/Questions/index.tsx | 14 +++++++++++--- ui/src/pages/Admin/Users/index.tsx | 20 ++++++++++++-------- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 8dbc8b0d..7516a2d7 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -228,6 +228,7 @@ export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted'; export interface AdminContentsReq extends Paging { status: AdminContentsFilterBy; + query?: string; } /** @@ -263,6 +264,7 @@ export interface AdminSettingsInterface { logo: string; language: string; theme: string; + time_zone: string; } export interface AdminSettingsSmtp { diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 2a25f53c..89a0ae26 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -23,10 +23,11 @@ import '../index.scss'; const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted']; const Answers: FC = () => { - const [urlSearchParams] = useSearchParams(); + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('status') || answerFilterItems[0]; const PAGE_SIZE = 20; const curPage = Number(urlSearchParams.get('page')) || 1; + const curQuery = urlSearchParams.get('query') || ''; const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' }); const { @@ -37,6 +38,7 @@ const Answers: FC = () => { page_size: PAGE_SIZE, page: curPage, status: curFilter as Type.AdminContentsFilterBy, + query: curQuery, }); const count = listData?.count || 0; @@ -78,6 +80,11 @@ const Answers: FC = () => { }); }; + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; return ( <>

{t('page_title')}

@@ -90,10 +97,11 @@ const Answers: FC = () => { />
diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 1a188512..c939bc06 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -32,9 +32,10 @@ const questionFilterItems: Type.AdminContentsFilterBy[] = [ const PAGE_SIZE = 20; const Questions: FC = () => { - const [urlSearchParams] = useSearchParams(); + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('status') || questionFilterItems[0]; const curPage = Number(urlSearchParams.get('page')) || 1; + const curQuery = urlSearchParams.get('query') || ''; const { t } = useTranslation('translation', { keyPrefix: 'admin.questions' }); const { @@ -45,6 +46,7 @@ const Questions: FC = () => { page_size: PAGE_SIZE, page: curPage, status: curFilter as Type.AdminContentsFilterBy, + query: curQuery, }); const count = listData?.count || 0; @@ -97,6 +99,11 @@ const Questions: FC = () => { }); }; + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; return ( <>

{t('page_title')}

@@ -109,10 +116,11 @@ const Questions: FC = () => { /> diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index de434084..4c129930 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react'; +import { FC } from 'react'; import { Button, Form, Table, Badge } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -34,11 +34,11 @@ const bgMap = { const PAGE_SIZE = 10; const Users: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.users' }); - const [userName, setUserName] = useState(''); - const [urlSearchParams] = useSearchParams(); + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0]; const curPage = Number(urlSearchParams.get('page') || '1'); + const curQuery = urlSearchParams.get('query') || ''; const { data, isLoading, @@ -46,7 +46,7 @@ const Users: FC = () => { } = useQueryUsers({ page: curPage, page_size: PAGE_SIZE, - ...(userName ? { username: userName } : {}), + query: curQuery, ...(curFilter === 'all' ? {} : { status: curFilter }), }); const changeModal = useChangeModal({ @@ -60,6 +60,11 @@ const Users: FC = () => { }); }; + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; return ( <>

{t('title')}

@@ -72,11 +77,10 @@ const Users: FC = () => { /> setUserName(e.target.value)} - placeholder="Filter by name" + value={curQuery} + onChange={handleFilter} + placeholder={t('filter.placeholder')} style={{ width: '12.25rem' }} /> From e45ecdc0ef9904eabb256f86ed6e95b3d9867527 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 11:12:05 +0800 Subject: [PATCH 011/157] refactor: Answer links for improved question management --- ui/src/pages/Admin/Answers/index.tsx | 9 ++++++--- ui/src/services/admin/answer.ts | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 89a0ae26..aa2c47d4 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -28,6 +28,7 @@ const Answers: FC = () => { const PAGE_SIZE = 20; const curPage = Number(urlSearchParams.get('page')) || 1; const curQuery = urlSearchParams.get('query') || ''; + const questionId = urlSearchParams.get('questionId') || ''; const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' }); const { @@ -39,6 +40,7 @@ const Answers: FC = () => { page: curPage, status: curFilter as Type.AdminContentsFilterBy, query: curQuery, + question_id: questionId, }); const count = listData?.count || 0; @@ -105,12 +107,12 @@ const Answers: FC = () => { style={{ width: '12.25rem' }} /> - +
- + - + {curFilter !== 'deleted' && } @@ -141,6 +143,7 @@ const Answers: FC = () => { __html: li.description, }} className="last-p text-truncate-2 fs-14" + style={{ maxWidth: '30rem' }} /> diff --git a/ui/src/services/admin/answer.ts b/ui/src/services/admin/answer.ts index 6fd0fbb6..cd6bc824 100644 --- a/ui/src/services/admin/answer.ts +++ b/ui/src/services/admin/answer.ts @@ -4,7 +4,9 @@ import qs from 'qs'; import request from '@answer/utils/request'; import type * as Type from '@answer/common/interface'; -export const useAnswerSearch = (params: Type.AdminContentsReq) => { +export const useAnswerSearch = ( + params: Type.AdminContentsReq & { question_id?: string }, +) => { const apiUrl = `/answer/admin/api/answer/page?${qs.stringify(params)}`; const { data, error, mutate } = useSWR( [apiUrl], From f6545f2de3732cc583af0726e9dbb6ad0ded8871 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 11:12:57 +0800 Subject: [PATCH 012/157] refactor: update en.json --- ui/src/i18n/locales/en.json | 45 +++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 94277ee9..a7d94f6d 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -759,7 +759,30 @@ "dashboard": { "title": "Dashboard", "welcome": "Welcome to Answer Admin !", - "version": "Version" + "site_statistics": "Site Statistics", + "questions": "Questions:", + "answers": "Answers:", + "comments": "Comments:", + "votes": "Votes:", + "active_users": "Active users:", + "flags": "Flags:", + "site_health_status": "Site Health Status", + "version": "Version:", + "https": "HTTPS:", + "uploading_files": "Uploading files:", + "smtp": "SMTP:", + "timezone": "Timezone:", + "system_info": "System Info", + "storage_used": "Storage used:", + "uptime": "Uptime:", + "answer_links": "Answer Links", + "documents": "Documents", + "feedback": "Feedback", + "review": "Review", + "config": "Config", + "update_to": "Update to", + "latest": "Latest", + "check_failed": "Check failed" }, "flags": { "title": "Flags", @@ -816,7 +839,10 @@ "inactive": "Inactive", "suspended": "Suspended", "deleted": "Deleted", - "normal": "Normal" + "normal": "Normal", + "filter": { + "placeholder": "Filter by name, user:id" + } }, "questions": { "page_title": "Questions", @@ -829,7 +855,10 @@ "created": "Created", "status": "Status", "action": "Action", - "change": "Change" + "change": "Change", + "filter": { + "placeholder": "Filter by title, question:id" + } }, "answers": { "page_title": "Answers", @@ -840,7 +869,10 @@ "created": "Created", "status": "Status", "action": "Action", - "change": "Change" + "change": "Change", + "filter": { + "placeholder": "Filter by title, answer:id" + } }, "general": { "page_title": "General", @@ -876,6 +908,11 @@ "label": "Interface Language", "msg": "Interface language cannot be empty.", "text": "User interface language. It will change when you refresh the page." + }, + "timezone": { + "label": "Timezone", + "msg": "Timezone cannot be empty.", + "text": "Choose a UTC (Coordinated Universal Time) time offset." } }, "smtp": { From 5a3bd799f772ed5a4bcd60c448925482dc282d33 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 11:24:12 +0800 Subject: [PATCH 013/157] refactor: refactor: Answer links for improved question management --- ui/src/pages/Admin/Questions/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index c939bc06..c3111001 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; import { Button, Form, Table, Stack, Badge } from 'react-bootstrap'; -import { useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { @@ -156,12 +156,11 @@ const Questions: FC = () => { -

{t('title')}

- - - {step === 1 && ( - <> -
{t('update_title')}
- }} - /> - - - )} +
+ + + +
+

{t('title')}

+ + + {step === 1 && ( + <> +
{t('update_title')}
+ }} + /> + + + )} - {step === 2 && ( - <> -
{t('done_title')}
-

{t('done_desscription')}

- - - )} -
-
- - - + {step === 2 && ( + <> +
{t('done_title')}
+

{t('done_desscription')}

+ + + )} + + + + + + ); }; diff --git a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx index 980a2df3..e249067f 100644 --- a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx +++ b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx @@ -34,7 +34,7 @@ const Index: FC = ({ visible, tabName, data }) => { : null} -
+
{tabName === 'bookmarks' && ( <> diff --git a/ui/src/pages/Users/Personal/components/NavBar/index.tsx b/ui/src/pages/Users/Personal/components/NavBar/index.tsx index 75fe68ee..4c98ce50 100644 --- a/ui/src/pages/Users/Personal/components/NavBar/index.tsx +++ b/ui/src/pages/Users/Personal/components/NavBar/index.tsx @@ -44,7 +44,10 @@ const list = [ const Index: FC = ({ slug, tabName = 'overview', isSelf }) => { const { t } = useTranslation('translation', { keyPrefix: 'personal' }); return ( -
+ From 784f9631f7df5a6b8bcfa3ee13ae0a20944f12e2 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:30:02 +0800 Subject: [PATCH 027/157] update install api --- internal/base/server/install.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/base/server/install.go b/internal/base/server/install.go index d4b44d90..f2d28bbe 100644 --- a/internal/base/server/install.go +++ b/internal/base/server/install.go @@ -2,16 +2,28 @@ package server import ( "embed" + "fmt" + "io/fs" "net/http" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" ) +const UIStaticPath = "build/static" + type _resource struct { fs embed.FS } +// Open to implement the interface by http.FS required +func (r *_resource) Open(name string) (fs.File, error) { + name = fmt.Sprintf(UIStaticPath+"/%s", name) + log.Debugf("open static path %s", name) + return r.fs.Open(name) +} + // NewHTTPServer new http server. func NewInstallHTTPServer() *gin.Engine { r := gin.New() @@ -20,7 +32,10 @@ func NewInstallHTTPServer() *gin.Engine { r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK??") }) // gin.SetMode(gin.ReleaseMode) - r.StaticFS("/static", http.FS(ui.Build)) + + r.StaticFS("/static", http.FS(&_resource{ + fs: ui.Build, + })) return r } From 847c9a343e139bdb3e4504b14e8193fd789c43ad Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 2 Nov 2022 17:33:04 +0800 Subject: [PATCH 028/157] fix: path alias --- ui/src/pages/Install/components/FirstStep/index.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx index 2a2226d6..5690ca8e 100644 --- a/ui/src/pages/Install/components/FirstStep/index.tsx +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -2,13 +2,8 @@ import { FC, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type { - LangsType, - FormValue, - FormDataType, -} from '@answer/common/interface'; +import type { LangsType, FormValue, FormDataType } from '@/common/interface'; import Progress from '../Progress'; - import { languages } from '@/services'; interface Props { From 6ece061e375743d64da3d10462b9acf671c85326 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:40:11 +0800 Subject: [PATCH 029/157] update install api --- internal/base/server/install.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/base/server/install.go b/internal/base/server/install.go index f2d28bbe..db1bcc62 100644 --- a/internal/base/server/install.go +++ b/internal/base/server/install.go @@ -37,5 +37,23 @@ func NewInstallHTTPServer() *gin.Engine { fs: ui.Build, })) + installApi := r.Group("") + installApi.GET("/install", Install) + return r } + +func Install(c *gin.Context) { + filePath := "" + var file []byte + var err error + filePath = "build/index.html" + c.Header("content-type", "text/html;charset=utf-8") + file, err = ui.Build.ReadFile(filePath) + if err != nil { + log.Error(err) + c.Status(http.StatusNotFound) + return + } + c.String(http.StatusOK, string(file)) +} From cd7f80a189441c87791dce16819a9c6ae359d9d7 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:55:58 +0800 Subject: [PATCH 030/157] add demo install api --- internal/router/ui.go | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/internal/router/ui.go b/internal/router/ui.go index 3498265c..3350c6be 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -7,6 +7,7 @@ import ( "net/http" "os" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -66,6 +67,55 @@ func (a *UIRouter) Register(r *gin.Engine) { fs: ui.Build, })) + // Install godoc + // @Summary Install + // @Description Install + // @Tags Install + // @Accept json + // @Produce json + // @Success 200 {object} handler.RespBody{} + // @Router /install [get] + r.GET("/install", func(c *gin.Context) { + filePath := "" + var file []byte + var err error + filePath = "build/index.html" + c.Header("content-type", "text/html;charset=utf-8") + file, err = ui.Build.ReadFile(filePath) + if err != nil { + log.Error(err) + c.Status(http.StatusNotFound) + return + } + c.String(http.StatusOK, string(file)) + }) + + // Install godoc + // @Summary Install + // @Description Install + // @Tags Install + // @Accept json + // @Produce json + // @Param data body schema.FollowReq true "follow" + // @Success 200 {object} handler.RespBody{} + // @Router /install/db/check [put] + r.PUT("/install/db/check", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + // Install godoc + // @Summary Install + // @Description Install + // @Tags Install + // @Accept json + // @Produce json + // @Param data body schema.FollowReq true "follow" + // @Success 200 {object} handler.RespBody{} + // @Router /install [put] + r.PUT("/install", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + // specify the not router for default routes and redirect r.NoRoute(func(c *gin.Context) { name := c.Request.URL.Path From 1556d815cfbb4fadf393568f603926bdead8b514 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:02:23 +0800 Subject: [PATCH 031/157] add install api --- internal/router/ui.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/router/ui.go b/internal/router/ui.go index 655d03a0..d74fbe6c 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -7,9 +7,8 @@ import ( "net/http" "os" - - "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/i18n" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -119,6 +118,10 @@ func (a *UIRouter) Register(r *gin.Engine) { handler.HandleResponse(c, nil, gin.H{}) }) + r.PUT("/install/siteconfig", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + // specify the not router for default routes and redirect r.NoRoute(func(c *gin.Context) { name := c.Request.URL.Path From cfcc4c69e3077adbfbbde9189715e1779526969d Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:07:59 +0800 Subject: [PATCH 032/157] update dashboardInfo --- internal/schema/dashboard_schema.go | 5 +++-- internal/service/dashboard/dashboard_service.go | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index f14c9e9b..74eb8ca9 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -7,8 +7,9 @@ type DashboardInfo struct { VoteCount int64 `json:"vote_count"` UserCount int64 `json:"user_count"` ReportCount int64 `json:"report_count"` - UploadingFiles string `json:"uploading_files"` //Allowed or Not allowed - SMTP string `json:"smtp"` //Enabled or Disabled + UploadingFiles bool `json:"uploading_files"` + SMTP bool `json:"smtp"` + HTTPS bool `json:"https"` TimeZone string `json:"time_zone"` OccupyingStorageSpace string `json:"occupying_storage_space"` AppStartTime string `json:"app_start_time"` diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index aaeabcfc..2abfa415 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -97,8 +97,9 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.UserCount = userCount dashboardInfo.ReportCount = reportCount - dashboardInfo.UploadingFiles = "Allowed" - dashboardInfo.SMTP = "Enabled" + dashboardInfo.UploadingFiles = true + dashboardInfo.SMTP = true + dashboardInfo.HTTPS = true dashboardInfo.OccupyingStorageSpace = "1MB" dashboardInfo.AppStartTime = "102" return dashboardInfo, nil From 44da3c24fd806c1536efe39448f7ccffce807cad Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 18:16:27 +0800 Subject: [PATCH 033/157] feat(admin): dashboard module --- ui/src/common/interface.ts | 18 +++++++ .../components/AnswerLinks/index.tsx | 31 +++++++++++ .../components/HealthStatus/index.tsx | 54 +++++++++++++++++++ .../Dashboard/components/Statistics/index.tsx | 51 ++++++++++++++++++ .../Dashboard/components/SystemInfo/index.tsx | 34 ++++++++++++ .../pages/Admin/Dashboard/components/index.ts | 6 +++ ui/src/pages/Admin/Dashboard/index.tsx | 29 ++++++++++ ui/src/services/admin/settings.ts | 13 +++++ ui/src/utils/common.ts | 16 ++++++ 9 files changed, 252 insertions(+) create mode 100644 ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx create mode 100644 ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx create mode 100644 ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx create mode 100644 ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx create mode 100644 ui/src/pages/Admin/Dashboard/components/index.ts diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 732964d1..98f7c046 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -323,3 +323,21 @@ export interface SearchResItem { export interface SearchRes extends ListResult { extra: any; } + +export interface AdminDashboard { + info: { + question_count: number; + answer_count: number; + comment_count: number; + vote_count: number; + user_count: number; + report_count: number; + uploading_files: boolean; + smtp: boolean; + time_zone: string; + occupying_storage_space: string; + app_start_time: number; + app_version: string; + https: boolean; + }; +} diff --git a/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx new file mode 100644 index 00000000..8f5048f8 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx @@ -0,0 +1,31 @@ +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +const AnswerLinks = () => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('answer_links')}
+ +
+ + {t('documents')} + + + + + {t('feedback')} + + + + + + ); +}; + +export default AnswerLinks; diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx new file mode 100644 index 00000000..72c3b9a6 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { Card, Row, Col, Badge } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; + +import type * as Type from '@answer/common/interface'; + +interface IProps { + data: Type.AdminDashboard['info']; +} + +const HealthStatus: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('site_health_status')}
+ +
+ {t('version')} + 90 + + {t('update_to')} {data.app_version} + + + + {t('https')} + {data.https ? t('yes') : t('yes')} + + + {t('uploading_files')} + + {data.uploading_files ? t('allowed') : t('not_allowed')} + + + + {t('smtp')} + {data.smtp ? t('enabled') : t('disabled')} + + {t('config')} + + + + {t('timezone')} + {data.time_zone} + + + + + ); +}; + +export default HealthStatus; diff --git a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx new file mode 100644 index 00000000..43e1fa2d --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx @@ -0,0 +1,51 @@ +import { FC } from 'react'; +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@answer/common/interface'; + +interface IProps { + data: Type.AdminDashboard['info']; +} +const Statistics: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('site_statistics')}
+ +
+ {t('questions')} + {data.question_count} + + + {t('answers')} + {data.answer_count} + + + {t('comments')} + {data.comment_count} + + + {t('votes')} + {data.vote_count} + + + {t('active_users')} + {data.user_count} + + + {t('flags')} + {data.report_count} + + {t('review')} + + + + + + ); +}; + +export default Statistics; diff --git a/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx new file mode 100644 index 00000000..2bf222f5 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react'; +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@answer/common/interface'; + +import { formatUptime } from '@/utils'; + +interface IProps { + data: Type.AdminDashboard['info']; +} +const SystemInfo: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('system_info')}
+ +
+ {t('storage_used')} + {data.occupying_storage_space} + + + {t('uptime')} + {formatUptime(data.app_start_time)} + + + + + ); +}; + +export default SystemInfo; diff --git a/ui/src/pages/Admin/Dashboard/components/index.ts b/ui/src/pages/Admin/Dashboard/components/index.ts new file mode 100644 index 00000000..877f643f --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/index.ts @@ -0,0 +1,6 @@ +import SystemInfo from './SystemInfo'; +import Statistics from './Statistics'; +import AnswerLinks from './AnswerLinks'; +import HealthStatus from './HealthStatus'; + +export { SystemInfo, Statistics, AnswerLinks, HealthStatus }; diff --git a/ui/src/pages/Admin/Dashboard/index.tsx b/ui/src/pages/Admin/Dashboard/index.tsx index 45c19721..47393831 100644 --- a/ui/src/pages/Admin/Dashboard/index.tsx +++ b/ui/src/pages/Admin/Dashboard/index.tsx @@ -1,12 +1,41 @@ import { FC } from 'react'; +import { Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import { + AnswerLinks, + HealthStatus, + Statistics, + SystemInfo, +} from './components'; + +import { useDashBoard } from '@/services'; + const Dashboard: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + const { data } = useDashBoard(); + + if (!data) { + return null; + } return ( <>

{t('title')}

{t('welcome')}

+ + + + + + + + + + + + + + {process.env.REACT_APP_VERSION && (

{`${t('version')} `} diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index 274e24eb..5f370260 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -70,3 +70,16 @@ export const updateSmtpSetting = (params: Type.AdminSettingsSmtp) => { const apiUrl = `/answer/admin/api/setting/smtp`; return request.put(apiUrl, params); }; + +export const useDashBoard = () => { + const apiUrl = `/answer/admin/api/dashboard`; + const { data, error } = useSWR( + [apiUrl], + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + }; +}; diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index 169dc1ad..dcc2d084 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -1,3 +1,5 @@ +import i18next from 'i18next'; + function getQueryString(name: string): string { const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`); const r = window.location.search.substr(1).match(reg); @@ -70,6 +72,19 @@ function parseUserInfo(markdown) { return markdown.replace(globalReg, '[@$1](/u/$1)'); } +function formatUptime(value) { + const t = i18next.t.bind(i18next); + const second = parseInt(value, 10); + + if (second > 60 * 60 && second < 60 * 60 * 24) { + return `${Math.floor(second / 3600)} ${t('dates.hour')}`; + } + if (second > 60 * 60 * 24) { + return `${Math.floor(second / 3600 / 24)} ${t('dates.day')}`; + } + + return `< 1 ${t('dates.hour')}`; +} export { getQueryString, thousandthDivision, @@ -77,4 +92,5 @@ export { scrollTop, matchedUsers, parseUserInfo, + formatUptime, }; From 364a5a0e189b9defd0981fd7eba2a889cc5b565a Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 18:16:54 +0800 Subject: [PATCH 034/157] refactor(i18n): update en.json --- ui/src/i18n/locales/en.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index ec9012b2..8bc92db1 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -293,7 +293,9 @@ "now": "now", "x_seconds_ago": "{{count}}s ago", "x_minutes_ago": "{{count}}m ago", - "x_hours_ago": "{{count}}h ago" + "x_hours_ago": "{{count}}h ago", + "hour": "hour", + "day": "day" }, "comment": { "btn_add_comment": "Add comment", @@ -866,7 +868,13 @@ "config": "Config", "update_to": "Update to", "latest": "Latest", - "check_failed": "Check failed" + "check_failed": "Check failed", + "yes": "Yes", + "no": "No", + "not_allowed": "Not allowed", + "allowed": "Allowed", + "enabled": "Enabled", + "disabled": "Disabled" }, "flags": { "title": "Flags", From 41ff3dbbc798a0e7ca5717022204811ab0ba1111 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 18:19:05 +0800 Subject: [PATCH 035/157] refactor: Modify the import package path --- ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx | 2 +- ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx | 2 +- ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx index 72c3b9a6..1b1c7ced 100644 --- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -3,7 +3,7 @@ import { Card, Row, Col, Badge } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import type * as Type from '@answer/common/interface'; +import type * as Type from '@/common/interface'; interface IProps { data: Type.AdminDashboard['info']; diff --git a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx index 43e1fa2d..9e6c979e 100644 --- a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx @@ -2,7 +2,7 @@ import { FC } from 'react'; import { Card, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; +import type * as Type from '@/common/interface'; interface IProps { data: Type.AdminDashboard['info']; diff --git a/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx index 2bf222f5..cbc065c7 100644 --- a/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx @@ -2,8 +2,7 @@ import { FC } from 'react'; import { Card, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; - +import type * as Type from '@/common/interface'; import { formatUptime } from '@/utils'; interface IProps { From 549f816d3e6a62749353fbdab5ceabec06759547 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 18:24:26 +0800 Subject: [PATCH 036/157] style: format --- ui/src/pages/Admin/Dashboard/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/Admin/Dashboard/index.tsx b/ui/src/pages/Admin/Dashboard/index.tsx index 47393831..2037016e 100644 --- a/ui/src/pages/Admin/Dashboard/index.tsx +++ b/ui/src/pages/Admin/Dashboard/index.tsx @@ -2,6 +2,8 @@ import { FC } from 'react'; import { Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import { useDashBoard } from '@/services'; + import { AnswerLinks, HealthStatus, @@ -9,8 +11,6 @@ import { SystemInfo, } from './components'; -import { useDashBoard } from '@/services'; - const Dashboard: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); const { data } = useDashBoard(); From 6f999a5a289aeb82005db95edf9894c9a7518196 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:33:23 +0800 Subject: [PATCH 037/157] update install api --- cmd/answer/command.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/answer/command.go b/cmd/answer/command.go index 27655443..07213134 100644 --- a/cmd/answer/command.go +++ b/cmd/answer/command.go @@ -4,7 +4,6 @@ import ( "fmt" "os" - "github.com/answerdev/answer/internal/base/server" "github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/migrations" "github.com/spf13/cobra" @@ -60,8 +59,8 @@ To run answer, use: Short: "init answer application", Long: `init answer application`, Run: func(_ *cobra.Command, _ []string) { - installwebapi := server.NewInstallHTTPServer() - installwebapi.Run(":8088") + // installwebapi := server.NewInstallHTTPServer() + // installwebapi.Run(":8088") cli.InstallAllInitialEnvironment(dataDirPath) c, err := readConfig() if err != nil { From 2ad8b8f4a992198ff4ec639a2b7321d06918b271 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 10:33:48 +0800 Subject: [PATCH 038/157] style: reformat get site info code --- internal/base/constant/constant.go | 5 +++ internal/base/reason/reason.go | 1 + internal/controller/siteinfo_controller.go | 25 ++++------- internal/router/answer_api_router.go | 2 +- internal/service/siteinfo_service.go | 48 ++++++++++------------ 5 files changed, 37 insertions(+), 44 deletions(-) diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 7722adf8..5a182716 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -47,3 +47,8 @@ var ( 8: ReportObjectType, } ) + +const ( + SiteTypeGeneral = "general" + SiteTypeInterface = "interface" +) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index abe1bd23..30e9f532 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -38,4 +38,5 @@ const ( LangNotFound = "error.lang.not_found" ReportHandleFailed = "error.report.handle_failed" ReportNotFound = "error.report.not_found" + SiteInfoNotFound = "error.site_info.not_found" ) diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index abcbc178..7148b7ad 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -18,30 +18,21 @@ func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoCo } } -// GetInfo godoc -// @Summary Get siteinfo -// @Description Get siteinfo +// GetSiteInfo get site info +// @Summary get site info +// @Description get site info // @Tags site // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} // @Router /answer/api/v1/siteinfo [get] -func (sc *SiteinfoController) GetInfo(ctx *gin.Context) { - var ( - resp = &schema.SiteInfoResp{} - general schema.SiteGeneralResp - face schema.SiteInterfaceResp - err error - ) - - general, err = sc.siteInfoService.GetSiteGeneral(ctx) - resp.General = &general +func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) { + var err error + resp := &schema.SiteInfoResp{} + resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) if err != nil { handler.HandleResponse(ctx, err, resp) return } - - face, err = sc.siteInfoService.GetSiteInterface(ctx) - resp.Face = &face - + resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 56a972a8..dac54f33 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -131,7 +131,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) //siteinfo - r.GET("/siteinfo", a.siteinfoController.GetInfo) + r.GET("/siteinfo", a.siteinfoController.GetSiteInfo) } func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go index a4f23fd3..09631493 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo_service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" @@ -25,38 +26,33 @@ func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService } } -func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) { - var ( - siteType = "general" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteGeneralResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) +// GetSiteGeneral get site info general +func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } if !exist { - return + return nil, errors.BadRequest(reason.SiteInfoNotFound) } - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + resp = &schema.SiteGeneralResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } -func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) { - var ( - siteType = "interface" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteInterfaceResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) - if !exist { - return +// GetSiteInterface get site info interface +func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err } - - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { From d091d8d5667770bd1c61fb6c0c29660f08f49ccd Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 15:02:27 +0800 Subject: [PATCH 039/157] feat: add time zone config --- docs/docs.go | 30 ++++++++++++------- docs/swagger.json | 30 ++++++++++++------- docs/swagger.yaml | 24 ++++++++++----- .../siteinfo_controller.go | 24 +++++++-------- internal/schema/siteinfo_schema.go | 13 ++++---- 5 files changed, 75 insertions(+), 46 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 4f0bfb5a..16ac77ca 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -509,14 +509,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -544,14 +544,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -580,14 +580,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "get site interface", "parameters": [ { "description": "general", @@ -626,14 +626,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -5348,7 +5348,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5362,6 +5363,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5369,7 +5374,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5383,6 +5389,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/swagger.json b/docs/swagger.json index e856d606..8476352c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -497,14 +497,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -532,14 +532,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -568,14 +568,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "get site interface", "parameters": [ { "description": "general", @@ -614,14 +614,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -5336,7 +5336,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5350,6 +5351,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5357,7 +5362,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5371,6 +5377,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index adc19f98..81c96d96 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1024,9 +1024,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.SiteInterfaceResp: properties: @@ -1039,9 +1043,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.TagItem: properties: @@ -1675,7 +1683,7 @@ paths: - admin /answer/admin/api/siteinfo/general: get: - description: Get siteinfo general + description: get site general information produces: - application/json responses: @@ -1690,11 +1698,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo general + summary: get site general information tags: - admin put: - description: Get siteinfo interface + description: update site general information parameters: - description: general in: body @@ -1711,12 +1719,12 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site general information tags: - admin /answer/admin/api/siteinfo/interface: get: - description: Get siteinfo interface + description: get site interface parameters: - description: general in: body @@ -1738,11 +1746,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: get site interface tags: - admin put: - description: Get siteinfo interface + description: update site info interface parameters: - description: general in: body @@ -1759,7 +1767,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site info interface tags: - admin /answer/admin/api/theme/options: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 30bfd1bf..821b517e 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -18,9 +18,9 @@ func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoCo } } -// GetGeneral godoc -// @Summary Get siteinfo general -// @Description Get siteinfo general +// GetGeneral get site general information +// @Summary get site general information +// @Description get site general information // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -31,9 +31,9 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// GetInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// GetInterface get site interface +// @Summary get site interface +// @Description get site interface // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -45,9 +45,9 @@ func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// UpdateGeneral godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// UpdateGeneral update site general information +// @Summary update site general information +// @Description update site general information // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -63,9 +63,9 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) } -// UpdateInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// UpdateInterface update site interface +// @Summary update site info interface +// @Description update site info interface // @Security ApiKeyAuth // @Tags admin // @Produce json diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index ff93e72d..446b986d 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -2,16 +2,17 @@ package schema // SiteGeneralReq site general request type SiteGeneralReq struct { - Name string `validate:"required,gt=1,lte=128" comment:"site name" form:"name" json:"name"` - ShortDescription string `validate:"required,gt=3,lte=255" comment:"short site description" form:"short_description" json:"short_description"` - Description string `validate:"required,gt=3,lte=2000" comment:"site description" form:"description" json:"description"` + Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` + ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` + Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` } // SiteInterfaceReq site interface request type SiteInterfaceReq struct { - Logo string `validate:"omitempty,gt=0,lte=256" comment:"logo" form:"logo" json:"logo"` - Theme string `validate:"required,gt=1,lte=128" comment:"theme" form:"theme" json:"theme"` - Language string `validate:"required,gt=1,lte=128" comment:"interface language" form:"language" json:"language"` + Logo string `validate:"omitempty,gt=0,lte=256" form:"logo" json:"logo"` + Theme string `validate:"required,gt=1,lte=128" form:"theme" json:"theme"` + Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"` + TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"` } // SiteGeneralResp site general response From b2256a5d1b1df5e6fcfbc797f1ab53c9a39cdbc4 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 16:01:17 +0800 Subject: [PATCH 040/157] fix: swagger wrong request parameter --- docs/docs.go | 11 ----------- docs/swagger.json | 11 ----------- docs/swagger.yaml | 7 ------- internal/controller_backyard/siteinfo_controller.go | 1 - 4 files changed, 30 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 16ac77ca..1b81becf 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -588,17 +588,6 @@ const docTemplate = `{ "admin" ], "summary": "get site interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], "responses": { "200": { "description": "OK", diff --git a/docs/swagger.json b/docs/swagger.json index 8476352c..425eb948 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -576,17 +576,6 @@ "admin" ], "summary": "get site interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], "responses": { "200": { "description": "OK", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 81c96d96..c8db90c2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1725,13 +1725,6 @@ paths: /answer/admin/api/siteinfo/interface: get: description: get site interface - parameters: - - description: general - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.AddCommentReq' produces: - application/json responses: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 821b517e..339765aa 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -39,7 +39,6 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp} // @Router /answer/admin/api/siteinfo/interface [get] -// @Param data body schema.AddCommentReq true "general" func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) From b7f7fb7201258a7e11fe7779d50706dd1d65e683 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 10:33:48 +0800 Subject: [PATCH 041/157] style: reformat get site info code --- internal/base/constant/constant.go | 5 +++ internal/base/reason/reason.go | 1 + internal/controller/siteinfo_controller.go | 25 ++++------- internal/router/answer_api_router.go | 2 +- internal/service/siteinfo_service.go | 48 ++++++++++------------ 5 files changed, 37 insertions(+), 44 deletions(-) diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 7722adf8..5a182716 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -47,3 +47,8 @@ var ( 8: ReportObjectType, } ) + +const ( + SiteTypeGeneral = "general" + SiteTypeInterface = "interface" +) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index abe1bd23..30e9f532 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -38,4 +38,5 @@ const ( LangNotFound = "error.lang.not_found" ReportHandleFailed = "error.report.handle_failed" ReportNotFound = "error.report.not_found" + SiteInfoNotFound = "error.site_info.not_found" ) diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index abcbc178..7148b7ad 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -18,30 +18,21 @@ func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoCo } } -// GetInfo godoc -// @Summary Get siteinfo -// @Description Get siteinfo +// GetSiteInfo get site info +// @Summary get site info +// @Description get site info // @Tags site // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} // @Router /answer/api/v1/siteinfo [get] -func (sc *SiteinfoController) GetInfo(ctx *gin.Context) { - var ( - resp = &schema.SiteInfoResp{} - general schema.SiteGeneralResp - face schema.SiteInterfaceResp - err error - ) - - general, err = sc.siteInfoService.GetSiteGeneral(ctx) - resp.General = &general +func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) { + var err error + resp := &schema.SiteInfoResp{} + resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) if err != nil { handler.HandleResponse(ctx, err, resp) return } - - face, err = sc.siteInfoService.GetSiteInterface(ctx) - resp.Face = &face - + resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index dbe34405..0415222b 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -134,7 +134,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) //siteinfo - r.GET("/siteinfo", a.siteinfoController.GetInfo) + r.GET("/siteinfo", a.siteinfoController.GetSiteInfo) } func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go index a4f23fd3..09631493 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo_service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" @@ -25,38 +26,33 @@ func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService } } -func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) { - var ( - siteType = "general" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteGeneralResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) +// GetSiteGeneral get site info general +func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } if !exist { - return + return nil, errors.BadRequest(reason.SiteInfoNotFound) } - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + resp = &schema.SiteGeneralResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } -func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) { - var ( - siteType = "interface" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteInterfaceResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) - if !exist { - return +// GetSiteInterface get site info interface +func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err } - - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { From b1d2eb0c1d9f23c21859c1fa39b09ebd9bafeab5 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 11:12:20 +0800 Subject: [PATCH 042/157] feat: dashboard add time zone --- cmd/answer/wire_gen.go | 9 ++-- internal/controller/siteinfo_controller.go | 6 +-- .../siteinfo_controller.go | 6 +-- .../service/dashboard/dashboard_service.go | 40 +++++++++------ internal/service/provider.go | 5 +- .../{ => siteinfo}/siteinfo_service.go | 2 +- internal/service/siteinfo_common/siteinfo.go | 1 + .../siteinfo_common/siteinfo_service.go | 50 +++++++++++++++++++ 8 files changed, 93 insertions(+), 26 deletions(-) rename internal/service/{ => siteinfo}/siteinfo_service.go (99%) create mode 100644 internal/service/siteinfo_common/siteinfo_service.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 2d902504..67a921fa 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -59,6 +59,8 @@ import ( "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/internal/service/siteinfo" + "github.com/answerdev/answer/internal/service/siteinfo_common" tag2 "github.com/answerdev/answer/internal/service/tag" "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/uploader" @@ -149,7 +151,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo) + siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) @@ -168,9 +171,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_backyard.NewThemeController() - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) + siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) - siteinfoController := controller.NewSiteinfoController(siteInfoService) + siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index 7148b7ad..5f547eb7 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -3,16 +3,16 @@ package controller import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" ) type SiteinfoController struct { - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewSiteinfoController new siteinfo controller. -func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController { +func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController { return &SiteinfoController{ siteInfoService: siteInfoService, } diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 339765aa..3a554c22 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -3,16 +3,16 @@ package controller_backyard import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo" "github.com/gin-gonic/gin" ) type SiteInfoController struct { - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo.SiteInfoService } // NewSiteInfoController new siteinfo controller. -func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController { +func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController { return &SiteInfoController{ siteInfoService: siteInfoService, } diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index 2abfa415..fcea03dd 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -10,17 +10,19 @@ import ( "github.com/answerdev/answer/internal/service/config" questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/report_common" + "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" ) type DashboardService struct { - questionRepo questioncommon.QuestionRepo - answerRepo answercommon.AnswerRepo - commentRepo comment_common.CommentCommonRepo - voteRepo activity_common.VoteRepo - userRepo usercommon.UserRepo - reportRepo report_common.ReportRepo - configRepo config.ConfigRepo + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + commentRepo comment_common.CommentCommonRepo + voteRepo activity_common.VoteRepo + userRepo usercommon.UserRepo + reportRepo report_common.ReportRepo + configRepo config.ConfigRepo + siteInfoService *siteinfo_common.SiteInfoCommonService } func NewDashboardService( @@ -31,16 +33,17 @@ func NewDashboardService( userRepo usercommon.UserRepo, reportRepo report_common.ReportRepo, configRepo config.ConfigRepo, - + siteInfoService *siteinfo_common.SiteInfoCommonService, ) *DashboardService { return &DashboardService{ - questionRepo: questionRepo, - answerRepo: answerRepo, - commentRepo: commentRepo, - voteRepo: voteRepo, - userRepo: userRepo, - reportRepo: reportRepo, - configRepo: configRepo, + questionRepo: questionRepo, + answerRepo: answerRepo, + commentRepo: commentRepo, + voteRepo: voteRepo, + userRepo: userRepo, + reportRepo: reportRepo, + configRepo: configRepo, + siteInfoService: siteInfoService, } } @@ -90,6 +93,12 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI if err != nil { return dashboardInfo, err } + + siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx) + if err != nil { + return dashboardInfo, err + } + dashboardInfo.QuestionCount = questionCount dashboardInfo.AnswerCount = answerCount dashboardInfo.CommentCount = commentCount @@ -102,5 +111,6 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.HTTPS = true dashboardInfo.OccupyingStorageSpace = "1MB" dashboardInfo.AppStartTime = "102" + dashboardInfo.TimeZone = siteInfoInterface.TimeZone return dashboardInfo, nil } diff --git a/internal/service/provider.go b/internal/service/provider.go index 4562572a..5c0e4ab9 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -22,6 +22,8 @@ import ( "github.com/answerdev/answer/internal/service/report_backyard" "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" + "github.com/answerdev/answer/internal/service/siteinfo" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/internal/service/tag" tagcommon "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/uploader" @@ -62,7 +64,8 @@ var ProviderSetService = wire.NewSet( report_backyard.NewReportBackyardService, user_backyard.NewUserBackyardService, reason.NewReasonService, - NewSiteInfoService, + siteinfo_common.NewSiteInfoCommonService, + siteinfo.NewSiteInfoService, notficationcommon.NewNotificationCommon, notification.NewNotificationService, activity.NewAnswerActivityService, diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go similarity index 99% rename from internal/service/siteinfo_service.go rename to internal/service/siteinfo/siteinfo_service.go index 09631493..cc84ba32 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -1,4 +1,4 @@ -package service +package siteinfo import ( "context" diff --git a/internal/service/siteinfo_common/siteinfo.go b/internal/service/siteinfo_common/siteinfo.go index ff9066b0..1c501d2d 100644 --- a/internal/service/siteinfo_common/siteinfo.go +++ b/internal/service/siteinfo_common/siteinfo.go @@ -2,6 +2,7 @@ package siteinfo_common import ( "context" + "github.com/answerdev/answer/internal/entity" ) diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go new file mode 100644 index 00000000..5dd565d0 --- /dev/null +++ b/internal/service/siteinfo_common/siteinfo_service.go @@ -0,0 +1,50 @@ +package siteinfo_common + +import ( + "context" + "encoding/json" + + "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/schema" + "github.com/segmentfault/pacman/errors" +) + +type SiteInfoCommonService struct { + siteInfoRepo SiteInfoRepo +} + +func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService { + return &SiteInfoCommonService{ + siteInfoRepo: siteInfoRepo, + } +} + +// GetSiteGeneral get site info general +func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + + resp = &schema.SiteGeneralResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil +} + +// GetSiteInterface get site info interface +func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil +} From 0306ee01edd197508ee03864f23fa32daf711080 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 3 Nov 2022 11:46:54 +0800 Subject: [PATCH 043/157] update install api --- internal/base/server/install.go | 17 +++++++++++++++++ internal/router/ui.go | 28 +++++++--------------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/internal/base/server/install.go b/internal/base/server/install.go index db1bcc62..da5652cc 100644 --- a/internal/base/server/install.go +++ b/internal/base/server/install.go @@ -6,6 +6,7 @@ import ( "io/fs" "net/http" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -40,6 +41,22 @@ func NewInstallHTTPServer() *gin.Engine { installApi := r.Group("") installApi.GET("/install", Install) + installApi.POST("/installation/db/check", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + installApi.POST("/installation/config-file/check", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + installApi.POST("/installation/init", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + installApi.POST("/installation/base-info", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + return r } diff --git a/internal/router/ui.go b/internal/router/ui.go index d74fbe6c..99bcaf07 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -92,33 +92,19 @@ func (a *UIRouter) Register(r *gin.Engine) { c.String(http.StatusOK, string(file)) }) - // Install godoc - // @Summary Install - // @Description Install - // @Tags Install - // @Accept json - // @Produce json - // @Param data body schema.FollowReq true "follow" - // @Success 200 {object} handler.RespBody{} - // @Router /install/db/check [put] - r.PUT("/install/db/check", func(c *gin.Context) { + r.POST("/installation/db/check", func(c *gin.Context) { handler.HandleResponse(c, nil, gin.H{}) }) - // Install godoc - // @Summary Install - // @Description Install - // @Tags Install - // @Accept json - // @Produce json - // @Param data body schema.FollowReq true "follow" - // @Success 200 {object} handler.RespBody{} - // @Router /install [put] - r.PUT("/install", func(c *gin.Context) { + r.POST("/installation/config-file/check", func(c *gin.Context) { handler.HandleResponse(c, nil, gin.H{}) }) - r.PUT("/install/siteconfig", func(c *gin.Context) { + r.POST("/installation/init", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + r.POST("/installation/base-info", func(c *gin.Context) { handler.HandleResponse(c, nil, gin.H{}) }) From 08d1facf2264b74ef6bf23d970ae7b178d85aabe Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 14:26:11 +0800 Subject: [PATCH 044/157] fix: To regenerate the wire --- cmd/answer/wire_gen.go | 3 +-- internal/controller/lang_controller.go | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 57f5a5f6..0fc32099 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -96,7 +96,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, configRepo := config.NewConfigRepo(dataData) emailRepo := export.NewEmailRepo(dataData) emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) + siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService) langController := controller.NewLangController(i18nTranslator, siteInfoService) authRepo := auth.NewAuthRepo(dataData) authService := auth2.NewAuthService(authRepo) @@ -172,7 +172,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_backyard.NewThemeController() - siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 384d711c..ed17d541 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -5,18 +5,18 @@ import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { translator i18n.Translator - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo.SiteInfoService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController { +func NewLangController(tr i18n.Translator, siteInfoService *siteinfo.SiteInfoService) *LangController { return &LangController{translator: tr, siteInfoService: siteInfoService} } From 1bcd3abbcbce947a0e6c3bd9d53b28735ea9ce7b Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 14:40:43 +0800 Subject: [PATCH 045/157] feat: add default language option --- internal/base/translator/provider.go | 12 ++++++++++-- internal/controller/lang_controller.go | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index a81576bd..e047ceb2 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -21,8 +21,13 @@ type LangOption struct { Value string `json:"value"` } -// LanguageOptions language -var LanguageOptions []*LangOption +// DefaultLangOption default language option. If user config the language is default, the language option is admin choose. +const DefaultLangOption = "Default" + +var ( + // LanguageOptions language + LanguageOptions []*LangOption +) // NewTranslator new a translator func NewTranslator(c *I18n) (tr i18n.Translator, err error) { @@ -49,6 +54,9 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) { // CheckLanguageIsValid check user input language is valid func CheckLanguageIsValid(lang string) bool { + if lang == DefaultLangOption { + return true + } for _, option := range LanguageOptions { if option.Value == lang { return true diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 384d711c..8d4242db 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -64,7 +64,7 @@ func (u *LangController) GetUserLangOptions(ctx *gin.Context) { options := translator.LanguageOptions if len(siteInterfaceResp.Language) > 0 { defaultOption := []*translator.LangOption{ - {Label: "Default", Value: siteInterfaceResp.Language}, + {Label: translator.DefaultLangOption, Value: siteInterfaceResp.Language}, } options = append(defaultOption, options...) } From 0ce3b14bd25266c8c6c463c57dec4af29d961901 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 16:01:17 +0800 Subject: [PATCH 046/157] fix: swagger wrong request parameter --- docs/docs.go | 152 +++++++----------- docs/swagger.json | 152 +++++++----------- docs/swagger.yaml | 100 +++++------- .../siteinfo_controller.go | 1 - 4 files changed, 153 insertions(+), 252 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 7a37d7e3..1b81becf 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -62,12 +62,6 @@ const docTemplate = `{ "description": "answer id or question title", "name": "query", "in": "query" - }, - { - "type": "string", - "description": "question id", - "name": "question_id", - "in": "query" } ], "responses": { @@ -119,8 +113,41 @@ const docTemplate = `{ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Get language options", "produces": [ "application/json" @@ -482,14 +509,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -517,14 +544,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -553,25 +580,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], + "summary": "get site interface", "responses": { "200": { "description": "OK", @@ -599,14 +615,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -1427,6 +1443,11 @@ const docTemplate = `{ }, "/answer/api/v1/language/options": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Get language options", "produces": [ "application/json" @@ -3381,52 +3402,6 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/interface": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "UserUpdateInterface update user interface config", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "UserUpdateInterface update user interface config", - "parameters": [ - { - "type": "string", - "description": "access-token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "UpdateInfoRequest", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", @@ -4858,10 +4833,6 @@ const docTemplate = `{ "description": "is admin", "type": "boolean" }, - "language": { - "description": "language", - "type": "string" - }, "last_login_date": { "description": "last login date", "type": "integer" @@ -4958,10 +4929,6 @@ const docTemplate = `{ "description": "is admin", "type": "boolean" }, - "language": { - "description": "language", - "type": "string" - }, "last_login_date": { "description": "last login date", "type": "integer" @@ -5370,7 +5337,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5384,6 +5352,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5391,7 +5363,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5405,6 +5378,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5617,19 +5594,6 @@ const docTemplate = `{ } } }, - "schema.UpdateUserInterfaceRequest": { - "type": "object", - "required": [ - "language" - ], - "properties": { - "language": { - "description": "language", - "type": "string", - "maxLength": 100 - } - } - }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 405c5d2a..425eb948 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -50,12 +50,6 @@ "description": "answer id or question title", "name": "query", "in": "query" - }, - { - "type": "string", - "description": "question id", - "name": "question_id", - "in": "query" } ], "responses": { @@ -107,8 +101,41 @@ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Get language options", "produces": [ "application/json" @@ -470,14 +497,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -505,14 +532,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -541,25 +568,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], + "summary": "get site interface", "responses": { "200": { "description": "OK", @@ -587,14 +603,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -1415,6 +1431,11 @@ }, "/answer/api/v1/language/options": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Get language options", "produces": [ "application/json" @@ -3369,52 +3390,6 @@ } } }, - "/answer/api/v1/user/interface": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "UserUpdateInterface update user interface config", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "UserUpdateInterface update user interface config", - "parameters": [ - { - "type": "string", - "description": "access-token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "UpdateInfoRequest", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", @@ -4846,10 +4821,6 @@ "description": "is admin", "type": "boolean" }, - "language": { - "description": "language", - "type": "string" - }, "last_login_date": { "description": "last login date", "type": "integer" @@ -4946,10 +4917,6 @@ "description": "is admin", "type": "boolean" }, - "language": { - "description": "language", - "type": "string" - }, "last_login_date": { "description": "last login date", "type": "integer" @@ -5358,7 +5325,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5372,6 +5340,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5379,7 +5351,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5393,6 +5366,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5605,19 +5582,6 @@ } } }, - "schema.UpdateUserInterfaceRequest": { - "type": "object", - "required": [ - "language" - ], - "properties": { - "language": { - "description": "language", - "type": "string", - "maxLength": 100 - } - } - }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d2a9dab8..c8db90c2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -652,9 +652,6 @@ definitions: is_admin: description: is admin type: boolean - language: - description: language - type: string last_login_date: description: last login date type: integer @@ -726,9 +723,6 @@ definitions: is_admin: description: is admin type: boolean - language: - description: language - type: string last_login_date: description: last login date type: integer @@ -1030,9 +1024,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.SiteInterfaceResp: properties: @@ -1045,9 +1043,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.TagItem: properties: @@ -1201,15 +1203,6 @@ definitions: - synonym_tag_list - tag_id type: object - schema.UpdateUserInterfaceRequest: - properties: - language: - description: language - maxLength: 100 - type: string - required: - - language - type: object schema.UpdateUserStatusReq: properties: status: @@ -1409,10 +1402,6 @@ paths: in: query name: query type: string - - description: question id - in: query - name: question_id - type: string produces: - application/json responses: @@ -1449,6 +1438,23 @@ paths: summary: AdminSetAnswerStatus tags: - admin + /answer/admin/api/dashboard: + get: + consumes: + - application/json + description: DashboardInfo + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: DashboardInfo + tags: + - admin /answer/admin/api/language/options: get: description: Get language options @@ -1459,6 +1465,8 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] summary: Get language options tags: - Lang @@ -1675,7 +1683,7 @@ paths: - admin /answer/admin/api/siteinfo/general: get: - description: Get siteinfo general + description: get site general information produces: - application/json responses: @@ -1690,11 +1698,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo general + summary: get site general information tags: - admin put: - description: Get siteinfo interface + description: update site general information parameters: - description: general in: body @@ -1711,19 +1719,12 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site general information tags: - admin /answer/admin/api/siteinfo/interface: get: - description: Get siteinfo interface - parameters: - - description: general - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.AddCommentReq' + description: get site interface produces: - application/json responses: @@ -1738,11 +1739,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: get site interface tags: - admin put: - description: Get siteinfo interface + description: update site info interface parameters: - description: general in: body @@ -1759,7 +1760,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site info interface tags: - admin /answer/admin/api/theme/options: @@ -2250,6 +2251,8 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] summary: Get language options tags: - Lang @@ -3436,35 +3439,6 @@ paths: summary: UserUpdateInfo update user info tags: - User - /answer/api/v1/user/interface: - put: - consumes: - - application/json - description: UserUpdateInterface update user interface config - parameters: - - description: access-token - in: header - name: Authorization - required: true - type: string - - description: UpdateInfoRequest - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.UpdateUserInterfaceRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] - summary: UserUpdateInterface update user interface config - tags: - - User /answer/api/v1/user/login/email: post: consumes: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 30bfd1bf..f193bd2c 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -39,7 +39,6 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp} // @Router /answer/admin/api/siteinfo/interface [get] -// @Param data body schema.AddCommentReq true "general" func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) From eb1cfd9aa8d4959f14f32fd9a5a153a6fd674266 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 10:33:48 +0800 Subject: [PATCH 047/157] style: reformat get site info code --- internal/base/constant/constant.go | 5 +++ internal/base/reason/reason.go | 1 + internal/controller/siteinfo_controller.go | 25 ++++------- internal/router/answer_api_router.go | 2 +- internal/service/siteinfo_service.go | 48 ++++++++++------------ 5 files changed, 37 insertions(+), 44 deletions(-) diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 7722adf8..5a182716 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -47,3 +47,8 @@ var ( 8: ReportObjectType, } ) + +const ( + SiteTypeGeneral = "general" + SiteTypeInterface = "interface" +) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index abe1bd23..30e9f532 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -38,4 +38,5 @@ const ( LangNotFound = "error.lang.not_found" ReportHandleFailed = "error.report.handle_failed" ReportNotFound = "error.report.not_found" + SiteInfoNotFound = "error.site_info.not_found" ) diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index abcbc178..7148b7ad 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -18,30 +18,21 @@ func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoCo } } -// GetInfo godoc -// @Summary Get siteinfo -// @Description Get siteinfo +// GetSiteInfo get site info +// @Summary get site info +// @Description get site info // @Tags site // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} // @Router /answer/api/v1/siteinfo [get] -func (sc *SiteinfoController) GetInfo(ctx *gin.Context) { - var ( - resp = &schema.SiteInfoResp{} - general schema.SiteGeneralResp - face schema.SiteInterfaceResp - err error - ) - - general, err = sc.siteInfoService.GetSiteGeneral(ctx) - resp.General = &general +func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) { + var err error + resp := &schema.SiteInfoResp{} + resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) if err != nil { handler.HandleResponse(ctx, err, resp) return } - - face, err = sc.siteInfoService.GetSiteInterface(ctx) - resp.Face = &face - + resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 06848687..d7d0c9dc 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -131,7 +131,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) //siteinfo - r.GET("/siteinfo", a.siteinfoController.GetInfo) + r.GET("/siteinfo", a.siteinfoController.GetSiteInfo) } func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go index 03e598e4..08a88b9f 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo_service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/entity" @@ -26,38 +27,33 @@ func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService } } -func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) { - var ( - siteType = "general" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteGeneralResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) +// GetSiteGeneral get site info general +func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } if !exist { - return + return nil, errors.BadRequest(reason.SiteInfoNotFound) } - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + resp = &schema.SiteGeneralResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } -func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) { - var ( - siteType = "interface" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteInterfaceResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) - if !exist { - return +// GetSiteInterface get site info interface +func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err } - - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { From 6b13eca4ef4d34dce6db232d62654c6c393a67da Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 3 Nov 2022 14:55:08 +0800 Subject: [PATCH 048/157] add mock api --- internal/router/ui.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/internal/router/ui.go b/internal/router/ui.go index 99bcaf07..1b5b2429 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -4,6 +4,7 @@ import ( "embed" "fmt" "io/fs" + "math/rand" "net/http" "os" @@ -93,11 +94,29 @@ func (a *UIRouter) Register(r *gin.Engine) { }) r.POST("/installation/db/check", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) + num := rand.Intn(10) + if num > 5 { + err := fmt.Errorf("connection error") + handler.HandleResponse(c, err, gin.H{}) + } else { + handler.HandleResponse(c, nil, gin.H{ + "connection_success": true, + }) + } }) r.POST("/installation/config-file/check", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) + num := rand.Intn(10) + if num > 5 { + handler.HandleResponse(c, nil, gin.H{ + "exist": true, + }) + } else { + handler.HandleResponse(c, nil, gin.H{ + "exist": false, + }) + } + }) r.POST("/installation/init", func(c *gin.Context) { From 961fd21b9c557c11f2d4b578085b594441dea990 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 3 Nov 2022 14:56:47 +0800 Subject: [PATCH 049/157] add mock install api --- internal/router/ui.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/router/ui.go b/internal/router/ui.go index 1b5b2429..852f6b06 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -12,6 +12,7 @@ import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -96,7 +97,7 @@ func (a *UIRouter) Register(r *gin.Engine) { r.POST("/installation/db/check", func(c *gin.Context) { num := rand.Intn(10) if num > 5 { - err := fmt.Errorf("connection error") + err := errors.BadRequest("connection error") handler.HandleResponse(c, err, gin.H{}) } else { handler.HandleResponse(c, nil, gin.H{ From 97fa333ea053ec954440f5f08d596e3b58d6e759 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 11:12:20 +0800 Subject: [PATCH 050/157] feat: user info response handling the default language --- cmd/answer/wire_gen.go | 17 ++++--- internal/controller/lang_controller.go | 6 +-- internal/controller/siteinfo_controller.go | 6 +-- .../siteinfo_controller.go | 6 +-- internal/service/provider.go | 5 +- .../{ => siteinfo}/siteinfo_service.go | 2 +- internal/service/siteinfo_common/siteinfo.go | 1 + .../siteinfo_common/siteinfo_service.go | 50 +++++++++++++++++++ internal/service/user_service.go | 33 ++++++++---- 9 files changed, 98 insertions(+), 28 deletions(-) rename internal/service/{ => siteinfo}/siteinfo_service.go (99%) create mode 100644 internal/service/siteinfo_common/siteinfo_service.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 746321b7..0ae8cd10 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -58,6 +58,8 @@ import ( "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/internal/service/siteinfo" + "github.com/answerdev/answer/internal/service/siteinfo_common" tag2 "github.com/answerdev/answer/internal/service/tag" "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/uploader" @@ -90,19 +92,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, return nil, nil, err } siteInfoRepo := site_info.NewSiteInfo(dataData) - configRepo := config.NewConfigRepo(dataData) - emailRepo := export.NewEmailRepo(dataData) - emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) - langController := controller.NewLangController(i18nTranslator, siteInfoService) + siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) + langController := controller.NewLangController(i18nTranslator, siteInfoCommonService) authRepo := auth.NewAuthRepo(dataData) authService := auth2.NewAuthService(authRepo) + configRepo := config.NewConfigRepo(dataData) userRepo := user.NewUserRepo(dataData, configRepo) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo) userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) - userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) + emailRepo := export.NewEmailRepo(dataData) + emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) + userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) uploaderService := uploader.NewUploaderService(serviceConf) @@ -167,8 +169,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_backyard.NewThemeController() + siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) - siteinfoController := controller.NewSiteinfoController(siteInfoService) + siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 8d4242db..1e947adf 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -5,18 +5,18 @@ import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { translator i18n.Translator - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController { +func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController { return &LangController{translator: tr, siteInfoService: siteInfoService} } diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index 7148b7ad..5f547eb7 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -3,16 +3,16 @@ package controller import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" ) type SiteinfoController struct { - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewSiteinfoController new siteinfo controller. -func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController { +func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController { return &SiteinfoController{ siteInfoService: siteInfoService, } diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index f193bd2c..7ecce395 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -3,16 +3,16 @@ package controller_backyard import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo" "github.com/gin-gonic/gin" ) type SiteInfoController struct { - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo.SiteInfoService } // NewSiteInfoController new siteinfo controller. -func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController { +func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController { return &SiteInfoController{ siteInfoService: siteInfoService, } diff --git a/internal/service/provider.go b/internal/service/provider.go index 3901766e..93d091fb 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -21,6 +21,8 @@ import ( "github.com/answerdev/answer/internal/service/report_backyard" "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" + "github.com/answerdev/answer/internal/service/siteinfo" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/internal/service/tag" tagcommon "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/uploader" @@ -61,7 +63,8 @@ var ProviderSetService = wire.NewSet( report_backyard.NewReportBackyardService, user_backyard.NewUserBackyardService, reason.NewReasonService, - NewSiteInfoService, + siteinfo_common.NewSiteInfoCommonService, + siteinfo.NewSiteInfoService, notficationcommon.NewNotificationCommon, notification.NewNotificationService, activity.NewAnswerActivityService, diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go similarity index 99% rename from internal/service/siteinfo_service.go rename to internal/service/siteinfo/siteinfo_service.go index 08a88b9f..822c7bde 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -1,4 +1,4 @@ -package service +package siteinfo import ( "context" diff --git a/internal/service/siteinfo_common/siteinfo.go b/internal/service/siteinfo_common/siteinfo.go index ff9066b0..1c501d2d 100644 --- a/internal/service/siteinfo_common/siteinfo.go +++ b/internal/service/siteinfo_common/siteinfo.go @@ -2,6 +2,7 @@ package siteinfo_common import ( "context" + "github.com/answerdev/answer/internal/entity" ) diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go new file mode 100644 index 00000000..5dd565d0 --- /dev/null +++ b/internal/service/siteinfo_common/siteinfo_service.go @@ -0,0 +1,50 @@ +package siteinfo_common + +import ( + "context" + "encoding/json" + + "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/schema" + "github.com/segmentfault/pacman/errors" +) + +type SiteInfoCommonService struct { + siteInfoRepo SiteInfoRepo +} + +func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService { + return &SiteInfoCommonService{ + siteInfoRepo: siteInfoRepo, + } +} + +// GetSiteGeneral get site info general +func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + + resp = &schema.SiteGeneralResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil +} + +// GetSiteInterface get site info interface +func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil +} diff --git a/internal/service/user_service.go b/internal/service/user_service.go index fbc22741..6447184e 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -18,6 +18,7 @@ import ( "github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" "github.com/answerdev/answer/pkg/checker" "github.com/google/uuid" @@ -30,11 +31,12 @@ import ( // UserService user service type UserService struct { - userRepo usercommon.UserRepo - userActivity activity.UserActiveActivityRepo - serviceConfig *service_config.ServiceConfig - emailService *export.EmailService - authService *auth.AuthService + userRepo usercommon.UserRepo + userActivity activity.UserActiveActivityRepo + serviceConfig *service_config.ServiceConfig + emailService *export.EmailService + authService *auth.AuthService + siteInfoService *siteinfo_common.SiteInfoCommonService } func NewUserService(userRepo usercommon.UserRepo, @@ -42,13 +44,15 @@ func NewUserService(userRepo usercommon.UserRepo, emailService *export.EmailService, authService *auth.AuthService, serviceConfig *service_config.ServiceConfig, + siteInfoService *siteinfo_common.SiteInfoCommonService, ) *UserService { return &UserService{ - userRepo: userRepo, - userActivity: userActivity, - emailService: emailService, - serviceConfig: serviceConfig, - authService: authService, + userRepo: userRepo, + userActivity: userActivity, + emailService: emailService, + serviceConfig: serviceConfig, + authService: authService, + siteInfoService: siteInfoService, } } @@ -64,6 +68,15 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st resp = &schema.GetUserToSetShowResp{} resp.GetFromUserEntity(userInfo) resp.AccessToken = token + + // if user choose the default language, Use the language configured by the administrator. + if resp.Language == translator.DefaultLangOption { + siteInterface, err := us.siteInfoService.GetSiteInterface(ctx) + if err != nil { + return nil, err + } + resp.Language = siteInterface.Language + } return resp, nil } From d16ecc99628ee18f196a00f1d25bf8bd9a83cd8d Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 16:12:23 +0800 Subject: [PATCH 051/157] update: use default language option in value --- cmd/answer/wire_gen.go | 1 - internal/controller/lang_controller.go | 2 +- internal/service/user_service.go | 9 --------- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 475a61af..302a67de 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -152,7 +152,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 1e947adf..b48080ee 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -64,7 +64,7 @@ func (u *LangController) GetUserLangOptions(ctx *gin.Context) { options := translator.LanguageOptions if len(siteInterfaceResp.Language) > 0 { defaultOption := []*translator.LangOption{ - {Label: translator.DefaultLangOption, Value: siteInterfaceResp.Language}, + {Label: translator.DefaultLangOption, Value: translator.DefaultLangOption}, } options = append(defaultOption, options...) } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 6447184e..76ce0d8a 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -68,15 +68,6 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st resp = &schema.GetUserToSetShowResp{} resp.GetFromUserEntity(userInfo) resp.AccessToken = token - - // if user choose the default language, Use the language configured by the administrator. - if resp.Language == translator.DefaultLangOption { - siteInterface, err := us.siteInfoService.GetSiteInterface(ctx) - if err != nil { - return nil, err - } - resp.Language = siteInterface.Language - } return resp, nil } From 2c60e16141917ad838e81ea3bfece040dbe1802c Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Nov 2022 16:19:57 +0800 Subject: [PATCH 052/157] refactor(i18n): update en.json --- ui/src/i18n/locales/en.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 19422b24..055d6b24 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -976,6 +976,11 @@ "msg": "Site name cannot be empty.", "text": "The name of this site, as used in the title tag." }, + "url": { + "label": "Site URL", + "msg": "Site url cannot be empty.", + "text": "The address of your site." + }, "short_description": { "label": "Short Site Description (optional)", "msg": "Short site description cannot be empty.", @@ -985,6 +990,11 @@ "label": "Site Description (optional)", "msg": "Site description cannot be empty.", "text": "Describe this site in one sentence, as used in the meta description tag." + }, + "email": { + "label": "Contact Email", + "msg": "Contact email cannot be empty.", + "text": "Email address of key contact responsible for this site." } }, "interface": { @@ -1004,7 +1014,7 @@ "msg": "Interface language cannot be empty.", "text": "User interface language. It will change when you refresh the page." }, - "timezone": { + "time_zone": { "label": "Timezone", "msg": "Timezone cannot be empty.", "text": "Choose a UTC (Coordinated Universal Time) time offset." From 2a849395f0869ed6b5ee5137bea68b0f825e1700 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 3 Nov 2022 16:40:40 +0800 Subject: [PATCH 053/157] add siteinfo --- internal/schema/siteinfo_schema.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 446b986d..da78327e 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -5,6 +5,8 @@ type SiteGeneralReq struct { Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` + SiteUrl string `validate:"required,gt=1,lte=128" form:"site_url" json:"site_url"` + ContactEmail string `validate:"required,gt=1,lte=128" form:"contact_email" json:"contact_email"` } // SiteInterfaceReq site interface request From aa9687c1bdb82dfdd2158b869f0cd82dac80b51a Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Nov 2022 17:19:53 +0800 Subject: [PATCH 054/157] refactor(admin): Add form fields --- ui/src/pages/Admin/General/index.tsx | 60 +++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 14b95978..cdce67bb 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -23,6 +23,11 @@ const General: FC = () => { isInvalid: false, errorMsg: '', }, + site_url: { + value: '', + isInvalid: false, + errorMsg: '', + }, short_description: { value: '', isInvalid: false, @@ -33,10 +38,15 @@ const General: FC = () => { isInvalid: false, errorMsg: '', }, + contact_email: { + value: '', + isInvalid: false, + errorMsg: '', + }, }); const checkValidated = (): boolean => { let ret = true; - const { name } = formData; + const { name, site_url, contact_email } = formData; if (!name.value) { ret = false; formData.name = { @@ -45,6 +55,22 @@ const General: FC = () => { errorMsg: t('name.msg'), }; } + if (!site_url.value) { + ret = false; + formData.site_url = { + value: '', + isInvalid: true, + errorMsg: t('site_url.msg'), + }; + } + if (!contact_email.value) { + ret = false; + formData.contact_email = { + value: '', + isInvalid: true, + errorMsg: t('contact_email.msg'), + }; + } setFormData({ ...formData, }); @@ -61,6 +87,8 @@ const General: FC = () => { name: formData.name.value, description: formData.description.value, short_description: formData.short_description.value, + site_url: formData.site_url.value, + contact_email: formData.contact_email.value, }; updateGeneralSetting(reqParams) @@ -100,7 +128,7 @@ const General: FC = () => { Object.keys(setting).forEach((k) => { formMeta[k] = { ...formData[k], value: setting[k] }; }); - setFormData(formMeta); + setFormData({ ...formData, ...formMeta }); }, [setting]); return ( <> @@ -120,6 +148,20 @@ const General: FC = () => { {formData.name.errorMsg} + + {t('site_url.label')} + onFieldChange('site_url', evt.target.value)} + /> + {t('site_url.text')} + + {formData.site_url.errorMsg} + + {t('short_description.label')} { {formData.description.errorMsg} + + {t('contact_email.label')} + onFieldChange('contact_email', evt.target.value)} + /> + {t('contact_email.text')} + + {formData.contact_email.errorMsg} + +

{t('flags')}{data.report_count} - + {t('review')} - + From db6736ff4d709a74970d65703ac08b2205b8ab87 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Nov 2022 17:20:27 +0800 Subject: [PATCH 056/157] refactor: update interface.ts --- ui/src/common/interface.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 98f7c046..a4c7dcd2 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -258,6 +258,8 @@ export interface AdminSettingsGeneral { name: string; short_description: string; description: string; + site_url: string; + contact_email: string; } export interface AdminSettingsInterface { From 0cada831eed1829ef4cb88e240234e68f23e80d8 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Nov 2022 17:20:48 +0800 Subject: [PATCH 057/157] refactor(i18n): update en.json --- ui/src/i18n/locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 055d6b24..98779100 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -976,7 +976,7 @@ "msg": "Site name cannot be empty.", "text": "The name of this site, as used in the title tag." }, - "url": { + "site_url": { "label": "Site URL", "msg": "Site url cannot be empty.", "text": "The address of your site." @@ -991,7 +991,7 @@ "msg": "Site description cannot be empty.", "text": "Describe this site in one sentence, as used in the meta description tag." }, - "email": { + "contact_email": { "label": "Contact Email", "msg": "Contact email cannot be empty.", "text": "Email address of key contact responsible for this site." From 48654aa995430901b8d8b55723681aad1a4f5bf9 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 3 Nov 2022 17:44:59 +0800 Subject: [PATCH 058/157] fix: add installation process --- ui/config-overrides.js | 7 +- ui/src/i18n/locales/en.json | 31 +++- .../Install/components/FifthStep/index.tsx | 8 +- .../Install/components/FirstStep/index.tsx | 11 +- .../Install/components/FourthStep/index.tsx | 63 +++++++- .../Install/components/ThirdStep/index.tsx | 42 +++-- ui/src/pages/Install/index.tsx | 152 ++++++++++++++---- ui/src/services/index.ts | 1 + ui/src/services/install/index.ts | 21 +++ ui/src/utils/request.ts | 9 +- 10 files changed, 289 insertions(+), 56 deletions(-) create mode 100644 ui/src/services/install/index.ts diff --git a/ui/config-overrides.js b/ui/config-overrides.js index ac52fa63..c8812ce6 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -18,7 +18,12 @@ module.exports = { const config = configFunction(proxy, allowedHost); config.proxy = { '/answer': { - target: 'http://10.0.10.98:2060', + target: 'http://10.0.20.88:8080', + changeOrigin: true, + secure: false, + }, + '/installation': { + target: 'http://10.0.20.88:8080', changeOrigin: true, secure: false, }, diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index ec9012b2..27a50f4d 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -739,6 +739,7 @@ "title": "Answer", "next": "Next", "done": "Done", + "config_yaml_error": "Can’t create the config.yaml file.", "lang": { "label": "Please choose a language" }, @@ -779,22 +780,42 @@ "site_information": "Site Information", "admin_account": "Admin Account", "site_name": { - "label": "Site Name" + "label": "Site Name", + "msg": "Site Name cannot be empty." + }, + "site_url": { + "label": "Site URL", + "text": "The address of your site.", + "msg": { + "empty": "Site URL cannot be empty.", + "incorrect": "Site URL incorrect format." + } }, "contact_email": { "label": "Contact Email", - "text": "Email address of key contact responsible for this site." + "text": "Email address of key contact responsible for this site.", + "msg": { + "empty": "Contact Email cannot be empty.", + "incorrect": "Contact Email incorrect format." + } + }, "admin_name": { - "label": "Name" + "label": "Name", + "msg": "Name cannot be empty." }, "admin_password": { "label": "Password", - "text": "You will need this password to log in. Please store it in a secure location." + "text": "You will need this password to log in. Please store it in a secure location.", + "msg": "Password cannot be empty." }, "admin_email": { "label": "Email", - "text": "You will need this email to log in." + "text": "You will need this email to log in.", + "msg": { + "empty": "Email cannot be empty.", + "incorrect": "Email incorrect format." + } }, "ready_title": "Your Answer is Ready!", "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", diff --git a/ui/src/pages/Install/components/FifthStep/index.tsx b/ui/src/pages/Install/components/FifthStep/index.tsx index 63f008e0..ab94f30f 100644 --- a/ui/src/pages/Install/components/FifthStep/index.tsx +++ b/ui/src/pages/Install/components/FifthStep/index.tsx @@ -6,8 +6,9 @@ import Progress from '../Progress'; interface Props { visible: boolean; + siteUrl: string; } -const Index: FC = ({ visible }) => { +const Index: FC = ({ visible, siteUrl = '' }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); if (!visible) return null; @@ -17,14 +18,15 @@ const Index: FC = ({ visible }) => {

If you ever feel like changing more settings, visit - admin section; find it in the site menu. + admin section; find it in the + site menu.

{t('good_luck')}

- +
); diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx index 5690ca8e..78b4aac8 100644 --- a/ui/src/pages/Install/components/FirstStep/index.tsx +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormValue, FormDataType } from '@/common/interface'; import Progress from '../Progress'; -import { languages } from '@/services'; +import { getInstallLangOptions } from '@/services'; interface Props { data: FormValue; @@ -18,8 +18,15 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const [langs, setLangs] = useState(); const getLangs = async () => { - const res: LangsType[] = await languages(); + const res: LangsType[] = await getInstallLangOptions(); setLangs(res); + changeCallback({ + lang: { + value: res[0].value, + isInvalid: false, + errorMsg: '', + }, + }); }; const handleSubmit = () => { diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index ccd44be2..fae0c53c 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -18,6 +18,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { let bol = true; const { site_name, + site_url, contact_email, admin_name, admin_password, @@ -33,12 +34,40 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { }; } + if (!site_url.value) { + bol = false; + data.site_url = { + value: '', + isInvalid: true, + errorMsg: t('site_name.msg.empty'), + }; + } + const reg = /^(http|https):\/\//g; + if (site_url.value && !site_url.value.match(reg)) { + bol = false; + data.site_url = { + value: site_url.value, + isInvalid: true, + errorMsg: t('site_url.msg.incorrect'), + }; + } + if (!contact_email.value) { bol = false; data.contact_email = { value: '', isInvalid: true, - errorMsg: t('contact_email.msg'), + errorMsg: t('contact_email.msg.empty'), + }; + } + + const mailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/; + if (contact_email.value && !contact_email.value.match(mailReg)) { + bol = false; + data.contact_email = { + value: contact_email.value, + isInvalid: true, + errorMsg: t('contact_email.msg.incorrect'), }; } @@ -65,7 +94,16 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { data.admin_email = { value: '', isInvalid: true, - errorMsg: t('admin_email.msg'), + errorMsg: t('admin_email.msg.empty'), + }; + } + + if (admin_email.value && !admin_email.value.match(mailReg)) { + bol = false; + data.admin_email = { + value: '', + isInvalid: true, + errorMsg: t('admin_email.msg.incorrect'), }; } @@ -108,6 +146,27 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {data.site_name.errorMsg} + + {t('site_url.label')} + { + changeCallback({ + site_url: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + {t('site_url.text')} + + {data.site_url.errorMsg} + + {t('contact_email.label')} void; } -const Index: FC = ({ visible, nextCallback }) => { +const Index: FC = ({ visible, errorMsg, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); if (!visible) return null; return (
{t('config_yaml.title')}
-
{t('config_yaml.label')}
-
-

- }} - /> -

-
- - - -
{t('config_yaml.info')}
+ + {errorMsg?.msg?.length > 0 ? ( + <> +
+

+ }} + /> +

+
+ + + +
{t('config_yaml.info')}
+ + ) : ( +
{t('config_yaml.label')}
+ )} +
diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index e8c40d5a..f407e156 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -3,8 +3,13 @@ import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; import { useTranslation, Trans } from 'react-i18next'; import type { FormDataType } from '@/common/interface'; -import { Storage } from '@/utils'; import { PageTitle } from '@/components'; +import { + dbCheck, + installInit, + installBaseInfo, + checkConfigFileExists, +} from '@/services'; import { FirstStep, @@ -16,8 +21,12 @@ import { const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step, setStep] = useState(1); - const [showError] = useState(false); + const [step, setStep] = useState(3); + const [loading, setLoading] = useState(true); + const [errorData, setErrorData] = useState<{ [propName: string]: any }>({ + msg: '', + }); + const [tableExist, setTableExist] = useState(false); const [formData, setFormData] = useState({ lang: { @@ -26,7 +35,7 @@ const Index: FC = () => { errorMsg: '', }, db_type: { - value: '', + value: 'mysql', isInvalid: false, errorMsg: '', }, @@ -55,12 +64,16 @@ const Index: FC = () => { isInvalid: false, errorMsg: '', }, - site_name: { value: '', isInvalid: false, errorMsg: '', }, + site_url: { + value: '', + isInvalid: false, + errorMsg: '', + }, contact_email: { value: '', isInvalid: false, @@ -88,33 +101,107 @@ const Index: FC = () => { setFormData({ ...formData, ...params }); }; - const handleStep = () => { + const handleErr = (data) => { + window.scrollTo(0, 0); + setErrorData(data); + }; + + const handleNext = async () => { + setErrorData({ + msg: '', + }); setStep((pre) => pre + 1); }; - // const handleSubmit = () => { - // const params = { - // lang: formData.lang.value, - // db_type: formData.db_type.value, - // db_username: formData.db_username.value, - // db_password: formData.db_password.value, - // db_host: formData.db_host.value, - // db_name: formData.db_name.value, - // db_file: formData.db_file.value, - // site_name: formData.site_name.value, - // contact_email: formData.contact_email.value, - // admin_name: formData.admin_name.value, - // admin_password: formData.admin_password.value, - // admin_email: formData.admin_email.value, - // }; + const submitDatabaseForm = () => { + const params = { + lang: formData.lang.value, + db_type: formData.db_type.value, + db_username: formData.db_username.value, + db_password: formData.db_password.value, + db_host: formData.db_host.value, + db_name: formData.db_name.value, + db_file: formData.db_file.value, + }; + dbCheck(params) + .then(() => { + handleNext(); + }) + .catch((err) => { + console.log(err); + handleErr(err); + }); + }; - // console.log(params); - // }; + const checkInstall = () => { + installInit() + .then(() => { + handleNext(); + }) + .catch((err) => { + handleErr(err); + }); + }; + + const submitSiteConfig = () => { + const params = { + site_name: formData.site_name.value, + contact_email: formData.contact_email.value, + admin_name: formData.admin_name.value, + admin_password: formData.admin_password.value, + admin_email: formData.admin_email.value, + }; + installBaseInfo(params) + .then(() => { + handleNext(); + }) + .catch((err) => { + handleErr(err); + }); + }; + + const handleStep = () => { + if (step === 2) { + submitDatabaseForm(); + } else if (step === 3) { + checkInstall(); + } else if (step === 4) { + submitSiteConfig(); + } else { + handleNext(); + } + }; + + const handleInstallNow = (e) => { + e.preventDefault(); + if (tableExist) { + setStep(7); + } else { + setStep(4); + } + }; + + const configYmlCheck = () => { + checkConfigFileExists() + .then((res) => { + setTableExist(res?.db_table_exist); + if (res && res.config_file_exist) { + setStep(5); + } + }) + .finally(() => { + setLoading(false); + }); + }; useEffect(() => { - console.log('step===', Storage.get('INSTALL_STEP')); + configYmlCheck(); }, []); + if (loading) { + return
; + } + return (
@@ -124,7 +211,9 @@ const Index: FC = () => {

{t('title')}

- {showError && show error msg } + {errorData?.msg && ( + {errorData?.msg} + )} { nextCallback={handleStep} /> - + { nextCallback={handleStep} /> - + {step === 6 && (
{t('warning')}
@@ -158,7 +251,10 @@ const Index: FC = () => { The file config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try{' '} - installing now. + handleInstallNow(e)}> + installing now + + .

diff --git a/ui/src/services/index.ts b/ui/src/services/index.ts index a6923bcf..a3e936ec 100644 --- a/ui/src/services/index.ts +++ b/ui/src/services/index.ts @@ -1,3 +1,4 @@ export * from './admin'; export * from './common'; export * from './client'; +export * from './install'; diff --git a/ui/src/services/install/index.ts b/ui/src/services/install/index.ts new file mode 100644 index 00000000..31d50690 --- /dev/null +++ b/ui/src/services/install/index.ts @@ -0,0 +1,21 @@ +import request from '@/utils/request'; + +export const checkConfigFileExists = () => { + return request.post('/installation/config-file/check'); +}; + +export const dbCheck = (params) => { + return request.post('/installation/db/check', params); +}; + +export const installInit = () => { + return request.post('/installation/init'); +}; + +export const installBaseInfo = (params) => { + return request.post('/installation/base-info', params); +}; + +export const getInstallLangOptions = () => { + return request.get('/installation/language/options'); +}; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index 4e878cb3..8167448d 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -73,7 +73,14 @@ class Request { }); } - if (data.type === 'modal') { + if (data.err_type === 'alert') { + return Promise.reject({ + msg, + ...data, + }); + } + + if (data.err_type === 'modal') { // modal error message Modal.confirm({ content: msg, From a85a45425a4618a44b8e7870b0d6a471c53e57b7 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 3 Nov 2022 18:44:28 +0800 Subject: [PATCH 059/157] chore: update process --- ui/src/pages/Upgrade/index.tsx | 34 ++++++++++++++++++++++++++-------- ui/src/services/common.ts | 4 ++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/ui/src/pages/Upgrade/index.tsx b/ui/src/pages/Upgrade/index.tsx index c65c9098..c264e46c 100644 --- a/ui/src/pages/Upgrade/index.tsx +++ b/ui/src/pages/Upgrade/index.tsx @@ -1,17 +1,20 @@ import { useState } from 'react'; -import { Container, Row, Col, Card, Button } from 'react-bootstrap'; +import { Container, Row, Col, Card, Button, Spinner } from 'react-bootstrap'; import { useTranslation, Trans } from 'react-i18next'; import { PageTitle } from '@/components'; +import { upgradSystem } from '@/services'; const Index = () => { const { t } = useTranslation('translation', { keyPrefix: 'upgrade', }); - const [step, setStep] = useState(1); + const [step] = useState(1); + const [loading, setLoading] = useState(false); - const handleUpdate = () => { - setStep(2); + const handleUpdate = async () => { + await upgradSystem(); + setLoading(true); }; return (
@@ -29,9 +32,22 @@ const Index = () => { i18nKey="upgrade.update_description" components={{ 1:

}} /> - + {loading ? ( + + ) : ( + + )} )} @@ -39,7 +55,9 @@ const Index = () => { <>

{t('done_title')}

{t('done_desscription')}

- + )} diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index f27eb0a9..61462886 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -266,3 +266,7 @@ export const useSiteSettings = () => { error, }; }; + +export const upgradSystem = () => { + return request.post('/answer/api/v1/upgradation'); +}; From 914defc446b93293a494765188c0f6a47e24e8a7 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 20:09:04 +0800 Subject: [PATCH 060/157] feat: add install api and functions --- cmd/answer/command.go | 44 ++++-- cmd/answer/main.go | 20 +-- i18n/en_US.yaml | 10 ++ internal/base/conf/conf.go | 20 +++ internal/base/reason/reason.go | 3 + internal/cli/install.go | 24 +-- internal/cli/install_check.go | 16 +- internal/install/install_controller.go | 146 ++++++++++++++++++ internal/install/install_main.go | 30 ++++ internal/install/install_req.go | 81 ++++++++++ .../install.go => install/install_server.go} | 32 ++-- internal/router/ui.go | 5 + internal/schema/err_schema.go | 2 + 13 files changed, 365 insertions(+), 68 deletions(-) create mode 100644 internal/install/install_controller.go create mode 100644 internal/install/install_main.go create mode 100644 internal/install/install_req.go rename internal/{base/server/install.go => install/install_server.go} (56%) diff --git a/cmd/answer/command.go b/cmd/answer/command.go index 07213134..1ddd844b 100644 --- a/cmd/answer/command.go +++ b/cmd/answer/command.go @@ -3,8 +3,11 @@ package main import ( "fmt" "os" + "path/filepath" + "github.com/answerdev/answer/internal/base/conf" "github.com/answerdev/answer/internal/cli" + "github.com/answerdev/answer/internal/install" "github.com/answerdev/answer/internal/migrations" "github.com/spf13/cobra" ) @@ -59,20 +62,31 @@ To run answer, use: Short: "init answer application", Long: `init answer application`, Run: func(_ *cobra.Command, _ []string) { - // installwebapi := server.NewInstallHTTPServer() - // installwebapi.Run(":8088") + // check config file and database. if config file exists and database is already created, init done cli.InstallAllInitialEnvironment(dataDirPath) - c, err := readConfig() - if err != nil { - fmt.Println("read config failed: ", err.Error()) - return + // set default config file path + if len(configFilePath) == 0 { + configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName) } - fmt.Println("read config successfully") - if err := migrations.InitDB(c.Data.Database); err != nil { - fmt.Println("init database error: ", err.Error()) - return + + configFileExist := cli.CheckConfigFile(configFilePath) + if configFileExist { + fmt.Println("config file exists, try to read the config...") + c, err := conf.ReadConfig(configFilePath) + if err != nil { + fmt.Println("read config failed: ", err.Error()) + return + } + + fmt.Println("config file read successfully, try to connect database...") + if cli.CheckDB(c.Data.Database, true) { + fmt.Println("connect to database successfully and table already exists, do nothing.") + return + } } - fmt.Println("init database successfully") + + // start installation server to install + install.Run(configFilePath) }, } @@ -82,7 +96,7 @@ To run answer, use: Short: "upgrade Answer version", Long: `upgrade Answer version`, Run: func(_ *cobra.Command, _ []string) { - c, err := readConfig() + c, err := conf.ReadConfig(configFilePath) if err != nil { fmt.Println("read config failed: ", err.Error()) return @@ -102,7 +116,7 @@ To run answer, use: Long: `back up data`, Run: func(_ *cobra.Command, _ []string) { fmt.Println("Answer is backing up data") - c, err := readConfig() + c, err := conf.ReadConfig(configFilePath) if err != nil { fmt.Println("read config failed: ", err.Error()) return @@ -135,13 +149,13 @@ To run answer, use: fmt.Println("upload directory not exists [x]") } - c, err := readConfig() + c, err := conf.ReadConfig(configFilePath) if err != nil { fmt.Println("read config failed: ", err.Error()) return } - if cli.CheckDB(c.Data.Database) { + if cli.CheckDB(c.Data.Database, false) { fmt.Println("db connection successfully [✔]") } else { fmt.Println("db connection failed [x]") diff --git a/cmd/answer/main.go b/cmd/answer/main.go index 08214244..0f1c7d94 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -2,13 +2,10 @@ package main import ( "os" - "path/filepath" "github.com/answerdev/answer/internal/base/conf" - "github.com/answerdev/answer/internal/cli" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman" - "github.com/segmentfault/pacman/contrib/conf/viper" "github.com/segmentfault/pacman/contrib/log/zap" "github.com/segmentfault/pacman/contrib/server/http" "github.com/segmentfault/pacman/log" @@ -41,7 +38,7 @@ func runApp() { log.SetLogger(zap.NewLogger( log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) - c, err := readConfig() + c, err := conf.ReadConfig(configFilePath) if err != nil { panic(err) } @@ -56,21 +53,6 @@ func runApp() { } } -func readConfig() (c *conf.AllConfig, err error) { - if len(configFilePath) == 0 { - configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName) - } - c = &conf.AllConfig{} - config, err := viper.NewWithPath(configFilePath) - if err != nil { - return nil, err - } - if err = config.Parse(&c); err != nil { - return nil, err - } - return c, nil -} - func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application { return pacman.NewApp( pacman.WithName(Name), diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 824c92e6..84afa373 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -89,6 +89,15 @@ error: set_avatar: other: "Avatar set failed." + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + install: + create_config_failed: + other: "Can’t create the config.yaml file." report: spam: name: @@ -170,3 +179,4 @@ notification: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" + diff --git a/internal/base/conf/conf.go b/internal/base/conf/conf.go index 97859ac0..55846825 100644 --- a/internal/base/conf/conf.go +++ b/internal/base/conf/conf.go @@ -1,11 +1,15 @@ package conf import ( + "path/filepath" + "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/server" "github.com/answerdev/answer/internal/base/translator" + "github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/router" "github.com/answerdev/answer/internal/service/service_config" + "github.com/segmentfault/pacman/contrib/conf/viper" ) // AllConfig all config @@ -28,3 +32,19 @@ type Data struct { Database *data.Database `json:"database" mapstructure:"database"` Cache *data.CacheConf `json:"cache" mapstructure:"cache"` } + +// ReadConfig read config +func ReadConfig(configFilePath string) (c *AllConfig, err error) { + if len(configFilePath) == 0 { + configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName) + } + c = &AllConfig{} + config, err := viper.NewWithPath(configFilePath) + if err != nil { + return nil, err + } + if err = config.Parse(&c); err != nil { + return nil, err + } + return c, nil +} diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index abe1bd23..ddd4cfc1 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -38,4 +38,7 @@ const ( LangNotFound = "error.lang.not_found" ReportHandleFailed = "error.report.handle_failed" ReportNotFound = "error.report.not_found" + ReadConfigFailed = "error.config.read_config_failed" + DatabaseConnectionFailed = "error.database.connection_failed" + InstallConfigFailed = "error.install.create_config_failed" ) diff --git a/internal/cli/install.go b/internal/cli/install.go index 8c8ee64f..eee7b89a 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -27,33 +27,35 @@ func InstallAllInitialEnvironment(dataDirPath string) { UploadFilePath = filepath.Join(dataDirPath, UploadFilePath) I18nPath = filepath.Join(dataDirPath, I18nPath) - installConfigFile() installUploadDir() installI18nBundle() fmt.Println("install all initial environment done") } -func installConfigFile() { - fmt.Println("[config-file] try to install...") - defaultConfigFile := filepath.Join(ConfigFilePath, DefaultConfigFileName) +func InstallConfigFile(configFilePath string) error { + if len(configFilePath) == 0 { + configFilePath = filepath.Join(ConfigFilePath, DefaultConfigFileName) + } + fmt.Println("[config-file] try to create at ", configFilePath) // if config file already exists do nothing. - if CheckConfigFile(defaultConfigFile) { - fmt.Printf("[config-file] %s already exists\n", defaultConfigFile) - return + if CheckConfigFile(configFilePath) { + fmt.Printf("[config-file] %s already exists\n", configFilePath) + return nil } if err := dir.CreateDirIfNotExist(ConfigFilePath); err != nil { fmt.Printf("[config-file] create directory fail %s\n", err.Error()) - return + return fmt.Errorf("create directory fail %s", err.Error()) } - fmt.Printf("[config-file] create directory success, config file is %s\n", defaultConfigFile) + fmt.Printf("[config-file] create directory success, config file is %s\n", configFilePath) - if err := writerFile(defaultConfigFile, string(configs.Config)); err != nil { + if err := writerFile(configFilePath, string(configs.Config)); err != nil { fmt.Printf("[config-file] install fail %s\n", err.Error()) - return + return fmt.Errorf("write file failed %s", err) } fmt.Printf("[config-file] install success\n") + return nil } func installUploadDir() { diff --git a/internal/cli/install_check.go b/internal/cli/install_check.go index fb05e33f..947d71e0 100644 --- a/internal/cli/install_check.go +++ b/internal/cli/install_check.go @@ -2,6 +2,7 @@ package cli import ( "github.com/answerdev/answer/internal/base/data" + "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/pkg/dir" ) @@ -13,7 +14,9 @@ func CheckUploadDir() bool { return dir.CheckDirExist(UploadFilePath) } -func CheckDB(dataConf *data.Database) bool { +// CheckDB check database whether the connection is normal +// if mustInstalled is true, will check table if already exists +func CheckDB(dataConf *data.Database, mustInstalled bool) bool { db, err := data.NewDB(false, dataConf) if err != nil { return false @@ -21,5 +24,16 @@ func CheckDB(dataConf *data.Database) bool { if err = db.Ping(); err != nil { return false } + if !mustInstalled { + return true + } + + exist, err := db.IsTableExist(&entity.Version{}) + if err != nil { + return false + } + if !exist { + return false + } return true } diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go new file mode 100644 index 00000000..42a67671 --- /dev/null +++ b/internal/install/install_controller.go @@ -0,0 +1,146 @@ +package install + +import ( + "github.com/answerdev/answer/configs" + "github.com/answerdev/answer/internal/base/conf" + "github.com/answerdev/answer/internal/base/data" + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/base/translator" + "github.com/answerdev/answer/internal/cli" + "github.com/answerdev/answer/internal/migrations" + "github.com/answerdev/answer/internal/schema" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// 1、校验配置文件 post installation/config-file/check +//2、校验数据库 post installation/db/check +//3、创建配置文件和数据库 post installation/init +//4、配置网站基本信息和超级管理员信息 post installation/base-info + +// LangOptions get installation language options +// @Summary get installation language options +// @Description get installation language options +// @Tags Lang +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]*translator.LangOption} +// @Router /installation/language/options [get] +func LangOptions(ctx *gin.Context) { + handler.HandleResponse(ctx, nil, translator.LanguageOptions) +} + +// CheckConfigFile check config file if exist when installation +// @Summary check config file if exist when installation +// @Description check config file if exist when installation +// @Tags installation +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/config-file/check [post] +func CheckConfigFile(ctx *gin.Context) { + resp := &CheckConfigFileResp{} + resp.ConfigFileExist = cli.CheckConfigFile(confPath) + if !resp.ConfigFileExist { + handler.HandleResponse(ctx, nil, resp) + return + } + allConfig, err := conf.ReadConfig(confPath) + if err != nil { + log.Error(err) + err = errors.BadRequest(reason.ReadConfigFailed) + handler.HandleResponse(ctx, err, nil) + return + } + resp.DbTableExist = cli.CheckDB(allConfig.Data.Database, true) + handler.HandleResponse(ctx, nil, resp) +} + +// CheckDatabase check database if exist when installation +// @Summary check database if exist when installation +// @Description check database if exist when installation +// @Tags installation +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/db/check [post] +func CheckDatabase(ctx *gin.Context) { + req := &CheckDatabaseReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp := &CheckDatabaseResp{} + dataConf := &data.Database{ + Driver: req.DbType, + Connection: req.GetConnection(), + } + resp.ConnectionSuccess = cli.CheckDB(dataConf, true) + if !resp.ConnectionSuccess { + handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) + return + } + handler.HandleResponse(ctx, nil, resp) +} + +// InitEnvironment init environment +// @Summary init environment +// @Description init environment +// @Tags installation +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/init [post] +func InitEnvironment(ctx *gin.Context) { + req := &CheckDatabaseReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + err := cli.InstallConfigFile(confPath) + if err != nil { + handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{ + Success: false, + CreateConfigFailed: true, + DefaultConfig: string(configs.Config), + ErrType: schema.ErrTypeAlert.ErrType, + }) + return + } + + c, err := conf.ReadConfig(confPath) + if err != nil { + log.Errorf("read config failed %s", err) + err = errors.BadRequest(reason.ReadConfigFailed) + handler.HandleResponse(ctx, err, nil) + return + } + + if err := migrations.InitDB(c.Data.Database); err != nil { + log.Error("init database error: ", err.Error()) + handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) + return + } + handler.HandleResponse(ctx, nil, nil) +} + +// InitBaseInfo init base info +// @Summary init base info +// @Description init base info +// @Tags installation +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/base-info [post] +func InitBaseInfo(ctx *gin.Context) { + req := &InitBaseInfoReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + // 修改配置文件 + // 修改管理员和对应信息 + handler.HandleResponse(ctx, nil, nil) + return +} diff --git a/internal/install/install_main.go b/internal/install/install_main.go new file mode 100644 index 00000000..a961c280 --- /dev/null +++ b/internal/install/install_main.go @@ -0,0 +1,30 @@ +package install + +import ( + "os" + + "github.com/answerdev/answer/internal/base/translator" + "github.com/answerdev/answer/internal/cli" +) + +var ( + port = os.Getenv("INSTALL_PORT") + confPath = "" +) + +func Run(configPath string) { + confPath = configPath + // initialize translator for return internationalization error when installing. + _, err := translator.NewTranslator(&translator.I18n{BundleDir: cli.I18nPath}) + if err != nil { + panic(err) + } + + installServer := NewInstallHTTPServer() + if len(port) == 0 { + port = "80" + } + if err = installServer.Run(":" + port); err != nil { + panic(err) + } +} diff --git a/internal/install/install_req.go b/internal/install/install_req.go new file mode 100644 index 00000000..38070c60 --- /dev/null +++ b/internal/install/install_req.go @@ -0,0 +1,81 @@ +package install + +import ( + "fmt" + "strings" + + "xorm.io/xorm/schemas" +) + +// CheckConfigFileResp check config file if exist or not response +type CheckConfigFileResp struct { + ConfigFileExist bool `json:"config_file_exist"` + DbTableExist bool `json:"db_table_exist"` +} + +// CheckDatabaseReq check database +type CheckDatabaseReq struct { + DbType string `json:"db_type"` + DbUsername string `json:"db_username"` + DbPassword string `json:"db_password"` + DbHost string `json:"db_host"` + DbName string `json:"db_name"` + DbFile string `json:"db_file"` +} + +// GetConnection get connection string +func (r *CheckDatabaseReq) GetConnection() string { + if r.DbType == string(schemas.SQLITE) { + return r.DbFile + } + if r.DbType == string(schemas.MYSQL) { + return fmt.Sprintf("%s:%s@tcp(%s)/%s", + r.DbUsername, r.DbPassword, r.DbHost, r.DbName) + } + if r.DbType == string(schemas.POSTGRES) { + host, port := parsePgSQLHostPort(r.DbHost) + return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s", + host, port, r.DbUsername, r.DbPassword, r.DbName) + } + return "" +} + +func parsePgSQLHostPort(dbHost string) (host string, port string) { + if strings.Contains(dbHost, ":") { + idx := strings.LastIndex(dbHost, ":") + host, port = dbHost[:idx], dbHost[idx+1:] + } else if len(dbHost) > 0 { + host = dbHost + } + if host == "" { + host = "127.0.0.1" + } + if port == "" { + port = "5432" + } + return host, port +} + +// CheckDatabaseResp check database response +type CheckDatabaseResp struct { + ConnectionSuccess bool `json:"connection_success"` +} + +// InitEnvironmentResp init environment response +type InitEnvironmentResp struct { + Success bool `json:"success"` + CreateConfigFailed bool `json:"create_config_failed"` + DefaultConfig string `json:"default_config"` + ErrType string `json:"err_type"` +} + +// InitBaseInfoReq init base info request +type InitBaseInfoReq struct { + Language string `json:"language"` + SiteName string `json:"site_name"` + SiteURL string `json:"site_url"` + ContactEmail string `json:"contact_email"` + AdminName string `json:"admin_name"` + AdminPassword string `json:"admin_password"` + AdminEmail string `json:"admin_email"` +} diff --git a/internal/base/server/install.go b/internal/install/install_server.go similarity index 56% rename from internal/base/server/install.go rename to internal/install/install_server.go index da5652cc..a301c197 100644 --- a/internal/base/server/install.go +++ b/internal/install/install_server.go @@ -1,4 +1,4 @@ -package server +package install import ( "embed" @@ -6,7 +6,6 @@ import ( "io/fs" "net/http" - "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -25,42 +24,31 @@ func (r *_resource) Open(name string) (fs.File, error) { return r.fs.Open(name) } -// NewHTTPServer new http server. +// NewInstallHTTPServer new install http server. func NewInstallHTTPServer() *gin.Engine { r := gin.New() gin.SetMode(gin.DebugMode) - - r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK??") }) - - // gin.SetMode(gin.ReleaseMode) - + r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") }) r.StaticFS("/static", http.FS(&_resource{ fs: ui.Build, })) installApi := r.Group("") - installApi.GET("/install", Install) + installApi.GET("/install", WebPage) - installApi.POST("/installation/db/check", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) + installApi.GET("/installation/language/options", LangOptions) - installApi.POST("/installation/config-file/check", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) + installApi.POST("/installation/db/check", CheckDatabase) - installApi.POST("/installation/init", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) + installApi.POST("/installation/config-file/check", CheckConfigFile) - installApi.POST("/installation/base-info", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) + installApi.POST("/installation/init", InitEnvironment) + installApi.POST("/installation/base-info", InitBaseInfo) return r } -func Install(c *gin.Context) { +func WebPage(c *gin.Context) { filePath := "" var file []byte var err error diff --git a/internal/router/ui.go b/internal/router/ui.go index 852f6b06..84231b0c 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -10,6 +10,7 @@ import ( "github.com/answerdev/answer/i18n" "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" @@ -94,6 +95,10 @@ func (a *UIRouter) Register(r *gin.Engine) { c.String(http.StatusOK, string(file)) }) + r.GET("/installation/language/options", func(c *gin.Context) { + handler.HandleResponse(c, nil, translator.LanguageOptions) + }) + r.POST("/installation/db/check", func(c *gin.Context) { num := rand.Intn(10) if num > 5 { diff --git a/internal/schema/err_schema.go b/internal/schema/err_schema.go index 3270b365..8acb48ca 100644 --- a/internal/schema/err_schema.go +++ b/internal/schema/err_schema.go @@ -7,3 +7,5 @@ type ErrTypeData struct { var ErrTypeModal = ErrTypeData{ErrType: "modal"} var ErrTypeToast = ErrTypeData{ErrType: "toast"} + +var ErrTypeAlert = ErrTypeData{ErrType: "alert"} From 260ff708d2ed88dec3439f819a731eab4ba73a81 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 10:23:53 +0800 Subject: [PATCH 061/157] fix: queryGroup component add pathname param --- ui/config-overrides.js | 4 ++-- ui/src/components/QueryGroup/index.tsx | 21 +++++++++++++++++---- ui/src/components/QuestionList/index.tsx | 1 + ui/src/pages/Install/index.tsx | 2 +- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ui/config-overrides.js b/ui/config-overrides.js index c8812ce6..35510f12 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -18,12 +18,12 @@ module.exports = { const config = configFunction(proxy, allowedHost); config.proxy = { '/answer': { - target: 'http://10.0.20.88:8080', + target: 'http://10.0.10.98:2060', changeOrigin: true, secure: false, }, '/installation': { - target: 'http://10.0.20.88:8080', + target: 'http://10.0.10.98:2060', changeOrigin: true, secure: false, }, diff --git a/ui/src/components/QueryGroup/index.tsx b/ui/src/components/QueryGroup/index.tsx index fae01621..15c3ad49 100644 --- a/ui/src/components/QueryGroup/index.tsx +++ b/ui/src/components/QueryGroup/index.tsx @@ -1,6 +1,6 @@ import { FC, memo } from 'react'; import { ButtonGroup, Button, DropdownButton, Dropdown } from 'react-bootstrap'; -import { useSearchParams } from 'react-router-dom'; +import { useSearchParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; @@ -11,6 +11,7 @@ interface Props { currentSort: string; sortKey?: string; className?: string; + pathname?: string; } const MAX_BUTTON_COUNT = 3; const Index: FC = ({ @@ -19,8 +20,10 @@ const Index: FC = ({ sortKey = 'order', i18nKeyPrefix = '', className = '', + pathname = '', }) => { const [searchParams, setUrlSearchParams] = useSearchParams(); + const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: i18nKeyPrefix, @@ -36,7 +39,11 @@ const Index: FC = ({ const handleClick = (e, type) => { e.preventDefault(); const str = handleParams(type); - setUrlSearchParams(str); + if (pathname) { + navigate(`${pathname}${str}`); + } else { + setUrlSearchParams(str); + } }; const filteredData = data.filter((_, index) => index > MAX_BUTTON_COUNT - 2); @@ -69,7 +76,9 @@ const Index: FC = ({ } : {} } - href={handleParams(key)} + href={ + pathname ? `${pathname}${handleParams(key)}` : handleParams(key) + } onClick={(evt) => handleClick(evt, key)}> {t(name)} @@ -95,7 +104,11 @@ const Index: FC = ({ 'd-block d-md-none', className, )} - href={handleParams(key)} + href={ + pathname + ? `${pathname}${handleParams(key)}` + : handleParams(key) + } onClick={(evt) => handleClick(evt, key)}> {t(name)} diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index a353d9bd..469a6e58 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -115,6 +115,7 @@ const QuestionList: FC = ({ source }) => { diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index f407e156..287f4f11 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -21,7 +21,7 @@ import { const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step, setStep] = useState(3); + const [step, setStep] = useState(1); const [loading, setLoading] = useState(true); const [errorData, setErrorData] = useState<{ [propName: string]: any }>({ msg: '', From 7a8f5b67bc9597717909b5bc192f55f9f1a81120 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 10:35:01 +0800 Subject: [PATCH 062/157] feat(local): set up app with language and time zone --- ui/src/App.tsx | 1 + ui/src/i18n/init.ts | 3 +- ui/src/index.tsx | 8 +--- ui/src/pages/Admin/Interface/index.tsx | 4 +- .../Install/components/FirstStep/index.tsx | 4 +- ui/src/pages/Layout/index.tsx | 30 ++------------ ui/src/pages/Users/Login/index.tsx | 6 +-- .../pages/Users/Settings/Interface/index.tsx | 39 +++++++++++-------- ui/src/router/routes.ts | 36 ++++++++--------- ui/src/services/client/index.ts | 1 + ui/src/services/client/settings.ts | 18 +++++++++ ui/src/services/common.ts | 22 +---------- ui/src/stores/interface.ts | 11 ++---- ui/src/stores/siteInfo.ts | 10 ++--- ui/src/stores/userInfo.ts | 1 + ui/src/utils/guard.ts | 25 +++++++++++- ui/src/utils/index.ts | 3 +- ui/src/utils/localize.ts | 32 +++++++++++++++ 18 files changed, 142 insertions(+), 112 deletions(-) create mode 100644 ui/src/services/client/settings.ts create mode 100644 ui/src/utils/localize.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 878ca1ab..92e9da21 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,5 +1,6 @@ import { RouterProvider } from 'react-router-dom'; +import './i18n/init'; import { routes, createBrowserRouter } from '@/router'; function App() { diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index e8b41e31..d60413bb 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -28,7 +28,8 @@ i18next escapeValue: false, }, react: { - transSupportBasicHtmlNodes: true, // allow
and simple html elements in translations + transSupportBasicHtmlNodes: true, + // allow
and simple html elements in translations transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'], }, // backend: { diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 67e5ee50..93bc4ed8 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -2,11 +2,10 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { Guard } from '@/utils'; +import { guard } from '@/utils'; import App from './App'; -import './i18n/init'; import './index.scss'; const root = ReactDOM.createRoot( @@ -14,10 +13,7 @@ const root = ReactDOM.createRoot( ); async function bootstrapApp() { - /** - * NOTICE: must pre init logged user info for router - */ - await Guard.pullLoggedUser(); + await guard.setupApp(); root.render( diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 395b9616..8a477bdb 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -12,7 +12,7 @@ import { interfaceStore } from '@/stores'; import { UploadImg } from '@/components'; import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; import { - languages, + getLanguageOptions, uploadAvatar, updateInterfaceSetting, useInterfaceSetting, @@ -52,7 +52,7 @@ const Interface: FC = () => { }, }); const getLangs = async () => { - const res: LangsType[] = await languages(); + const res: LangsType[] = await getLanguageOptions(); setLangs(res); if (!formData.language.value) { // set default theme value diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx index 5690ca8e..4f84e3b5 100644 --- a/ui/src/pages/Install/components/FirstStep/index.tsx +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormValue, FormDataType } from '@/common/interface'; import Progress from '../Progress'; -import { languages } from '@/services'; +import { getLanguageOptions } from '@/services'; interface Props { data: FormValue; @@ -18,7 +18,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const [langs, setLangs] = useState(); const getLangs = async () => { - const res: LangsType[] = await languages(); + const res: LangsType[] = await getLanguageOptions(); setLangs(res); }; diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 363c9b01..b335a3ee 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -1,42 +1,19 @@ -import { FC, useEffect, memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { FC, memo } from 'react'; import { Outlet } from 'react-router-dom'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { siteInfoStore, interfaceStore, toastStore } from '@/stores'; +import { siteInfoStore, toastStore } from '@/stores'; import { Header, AdminHeader, Footer, Toast } from '@/components'; -import { useSiteSettings } from '@/services'; -import Storage from '@/utils/storage'; -import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; -let isMounted = false; const Layout: FC = () => { - const { siteInfo, update: siteStoreUpdate } = siteInfoStore(); - const { update: interfaceStoreUpdate } = interfaceStore(); - const { data: siteSettings } = useSiteSettings(); const { msg: toastMsg, variant, clear: toastClear } = toastStore(); - const { i18n } = useTranslation(); - + const { siteInfo } = siteInfoStore.getState(); const closeToast = () => { toastClear(); }; - useEffect(() => { - if (siteSettings) { - siteStoreUpdate(siteSettings.general); - interfaceStoreUpdate(siteSettings.interface); - } - }, [siteSettings]); - if (!isMounted) { - isMounted = true; - const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); - if (lang) { - i18n.changeLanguage(lang); - } - } - return ( @@ -47,6 +24,7 @@ const Layout: FC = () => { revalidateOnFocus: false, }}>
+ {/* TODO: move admin header to Admin/Index */}
diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index 423a6654..b8794ff4 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -10,7 +10,7 @@ import type { } from '@/common/interface'; import { PageTitle, Unactivate } from '@/components'; import { loggedUserInfoStore } from '@/stores'; -import { getQueryString, Guard, floppyNavigation } from '@/utils'; +import { getQueryString, guard, floppyNavigation } from '@/utils'; import { login, checkImgCode } from '@/services'; import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; @@ -104,7 +104,7 @@ const Index: React.FC = () => { login(params) .then((res) => { updateUser(res); - const userStat = Guard.deriveLoginState(); + const userStat = guard.deriveLoginState(); if (userStat.isNotActivated) { // inactive setStep(2); @@ -159,7 +159,7 @@ const Index: React.FC = () => { if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { setStep(2); } else { - Guard.tryNormalLogged(); + guard.tryNormalLogged(); } }, []); diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index 3a3941b9..c39bf0b9 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -2,49 +2,54 @@ import React, { useEffect, useState, FormEvent } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import dayjs from 'dayjs'; -import en from 'dayjs/locale/en'; -import zh from 'dayjs/locale/zh-cn'; - import type { LangsType, FormDataType } from '@/common/interface'; import { useToast } from '@/hooks'; -import { languages } from '@/services'; -import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; +import { getLanguageOptions, updateUserInterface } from '@/services'; +import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; +import { localize } from '@/utils'; +import { loggedUserInfoStore } from '@/stores'; const Index = () => { - const { t, i18n } = useTranslation('translation', { + const { t } = useTranslation('translation', { keyPrefix: 'settings.interface', }); + const loggedUserInfo = loggedUserInfoStore.getState().user; const toast = useToast(); const [langs, setLangs] = useState(); const [formData, setFormData] = useState({ lang: { - value: true, + // FIXME: userinfo? or userInfo.language + value: loggedUserInfo, isInvalid: false, errorMsg: '', }, }); const getLangs = async () => { - const res: LangsType[] = await languages(); + const res: LangsType[] = await getLanguageOptions(); setLangs(res); }; const handleSubmit = (event: FormEvent) => { event.preventDefault(); - - Storage.set(CURRENT_LANG_STORAGE_KEY, formData.lang.value); - dayjs.locale(formData.lang.value === DEFAULT_LANG ? en : zh); - i18n.changeLanguage(formData.lang.value); - toast.onShow({ - msg: t('update', { keyPrefix: 'toast' }), - variant: 'success', + const lang = formData.lang.value; + updateUserInterface(lang).then(() => { + loggedUserInfoStore.getState().update({ + ...loggedUserInfo, + language: lang, + }); + localize.setupAppLanguage(); + toast.onShow({ + msg: t('update', { keyPrefix: 'toast' }), + variant: 'success', + }); }); }; useEffect(() => { getLangs(); + // TODO: get default lang by interface api const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); if (lang) { setFormData({ @@ -74,7 +79,7 @@ const Index = () => { }}> {langs?.map((item) => { return ( - ); diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index b5adce7c..5723a4e2 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -1,6 +1,6 @@ import { RouteObject } from 'react-router-dom'; -import { Guard } from '@/utils'; +import { guard } from '@/utils'; import type { TGuardResult } from '@/utils/guard'; export interface RouteNode extends RouteObject { @@ -21,7 +21,7 @@ const routes: RouteNode[] = [ path: '/', page: 'pages/Layout', guard: async () => { - return Guard.notForbidden(); + return guard.notForbidden(); }, children: [ // question and answer @@ -46,14 +46,14 @@ const routes: RouteNode[] = [ path: 'questions/ask', page: 'pages/Questions/Ask', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { path: 'posts/:qid/edit', page: 'pages/Questions/Ask', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { @@ -81,7 +81,7 @@ const routes: RouteNode[] = [ path: 'tags/:tagId/edit', page: 'pages/Tags/Edit', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, // users @@ -97,7 +97,7 @@ const routes: RouteNode[] = [ path: 'users/settings', page: 'pages/Users/Settings', guard: async () => { - return Guard.logged(); + return guard.logged(); }, children: [ { @@ -130,25 +130,25 @@ const routes: RouteNode[] = [ path: 'users/login', page: 'pages/Users/Login', guard: async () => { - const notLogged = Guard.notLogged(); + const notLogged = guard.notLogged(); if (notLogged.ok) { return notLogged; } - return Guard.notActivated(); + return guard.notActivated(); }, }, { path: 'users/register', page: 'pages/Users/Register', guard: async () => { - return Guard.notLogged(); + return guard.notLogged(); }, }, { path: 'users/account-recovery', page: 'pages/Users/AccountForgot', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { @@ -160,32 +160,32 @@ const routes: RouteNode[] = [ path: 'users/password-reset', page: 'pages/Users/PasswordReset', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { path: 'users/account-activation', page: 'pages/Users/ActiveEmail', guard: async () => { - const notActivated = Guard.notActivated(); + const notActivated = guard.notActivated(); if (notActivated.ok) { return notActivated; } - return Guard.notLogged(); + return guard.notLogged(); }, }, { path: 'users/account-activation/success', page: 'pages/Users/ActivationResult', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { path: '/users/account-activation/failed', page: 'pages/Users/ActivationResult', guard: async () => { - return Guard.notActivated(); + return guard.notActivated(); }, }, { @@ -197,7 +197,7 @@ const routes: RouteNode[] = [ path: '/users/account-suspended', page: 'pages/Users/Suspended', guard: async () => { - return Guard.forbidden(); + return guard.forbidden(); }, }, // for admin @@ -205,8 +205,8 @@ const routes: RouteNode[] = [ path: 'admin', page: 'pages/Admin', guard: async () => { - await Guard.pullLoggedUser(true); - return Guard.admin(); + await guard.pullLoggedUser(true); + return guard.admin(); }, children: [ { diff --git a/ui/src/services/client/index.ts b/ui/src/services/client/index.ts index 1f1af66a..17cfb7e0 100644 --- a/ui/src/services/client/index.ts +++ b/ui/src/services/client/index.ts @@ -4,3 +4,4 @@ export * from './notification'; export * from './question'; export * from './search'; export * from './tag'; +export * from './settings'; diff --git a/ui/src/services/client/settings.ts b/ui/src/services/client/settings.ts new file mode 100644 index 00000000..a47262eb --- /dev/null +++ b/ui/src/services/client/settings.ts @@ -0,0 +1,18 @@ +// import useSWR from 'swr'; + +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; + +export const loadLang = () => { + return request.get('/answer/api/v1/language/config'); +}; + +export const getLanguageOptions = () => { + return request.get('/answer/api/v1/language/options'); +}; + +export const updateUserInterface = (lang: string) => { + return request.put('/answer/api/v1/user/interface', { + language: lang, + }); +}; diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index f27eb0a9..1ddc7f18 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -163,14 +163,6 @@ export const questionDetail = (id: string) => { ); }; -export const langConfig = () => { - return request.get('/answer/api/v1/language/config'); -}; - -export const languages = () => { - return request.get('/answer/api/v1/language/options'); -}; - export const getAnswers = (params: Type.AnswersReq) => { const apiUrl = `/answer/api/v1/answer/page?${qs.stringify(params)}`; return request.get>(apiUrl); @@ -253,16 +245,6 @@ export const changeEmailVerify = (params: { code: string }) => { return request.put('/answer/api/v1/user/email', params); }; -export const useSiteSettings = () => { - const apiUrl = `/answer/api/v1/siteinfo`; - const { data, error } = useSWR( - [apiUrl], - request.instance.get, - ); - - return { - data, - isLoading: !data && !error, - error, - }; +export const getAppSettings = () => { + return request.get('/answer/api/v1/siteinfo'); }; diff --git a/ui/src/stores/interface.ts b/ui/src/stores/interface.ts index ea964efb..eddd1db8 100644 --- a/ui/src/stores/interface.ts +++ b/ui/src/stores/interface.ts @@ -1,14 +1,10 @@ import create from 'zustand'; -interface updateParams { - logo: string; - theme: string; - language: string; -} +import { AdminSettingsInterface } from '@/common/interface'; interface InterfaceType { - interface: updateParams; - update: (params: updateParams) => void; + interface: AdminSettingsInterface; + update: (params: AdminSettingsInterface) => void; } const interfaceSetting = create((set) => ({ @@ -16,6 +12,7 @@ const interfaceSetting = create((set) => ({ logo: '', theme: '', language: '', + time_zone: '', }, update: (params) => set(() => { diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts index f5a058dc..626fbf04 100644 --- a/ui/src/stores/siteInfo.ts +++ b/ui/src/stores/siteInfo.ts @@ -1,14 +1,10 @@ import create from 'zustand'; -interface updateParams { - name: string; - description: string; - short_description: string; -} +import { AdminSettingsGeneral } from '@/common/interface'; interface SiteInfoType { - siteInfo: updateParams; - update: (params: updateParams) => void; + siteInfo: AdminSettingsGeneral; + update: (params: AdminSettingsGeneral) => void; } const siteInfo = create((set) => ({ diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index 017c3149..cf1aa860 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -25,6 +25,7 @@ const initUser: UserInfoRes = { website: '', status: '', mail_status: 1, + language: '', }; const loggedUserInfoStore = create((set) => ({ diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index bc73ab9a..c9517d7a 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -1,8 +1,9 @@ -import { getLoggedUserInfo } from '@/services'; -import { loggedUserInfoStore } from '@/stores'; +import { getLoggedUserInfo, getAppSettings } from '@/services'; +import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores'; import { RouteAlias } from '@/router/alias'; import Storage from '@/utils/storage'; import { LOGGED_USER_STORAGE_KEY } from '@/common/constants'; +import { setupAppLanguage, setupAppTimeZone } from '@/utils/localize'; import { floppyNavigation } from './floppyNavigation'; @@ -180,3 +181,23 @@ export const tryNormalLogged = (autoLogin: boolean = false) => { return false; }; + +export const initAppSettingsStore = async () => { + const appSettings = await getAppSettings(); + if (appSettings) { + siteInfoStore.getState().update(appSettings.general); + interfaceStore.getState().update(appSettings.interface); + } +}; + +export const setupApp = async () => { + /** + * WARN: + * 1. must pre init logged user info for router guard + * 2. must pre init app settings for app render + */ + // TODO: optimize `initAppSettingsStore` by server render + await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]); + setupAppLanguage(); + setupAppTimeZone(); +}; diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts index 69f70696..de9e8fef 100644 --- a/ui/src/utils/index.ts +++ b/ui/src/utils/index.ts @@ -2,5 +2,6 @@ export { default as request } from './request'; export { default as Storage } from './storage'; export { floppyNavigation } from './floppyNavigation'; -export * as Guard from './guard'; +export * as guard from './guard'; +export * as localize from './localize'; export * from './common'; diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts new file mode 100644 index 00000000..4a2b5c05 --- /dev/null +++ b/ui/src/utils/localize.ts @@ -0,0 +1,32 @@ +import dayjs from 'dayjs'; +import i18next from 'i18next'; + +import { interfaceStore, loggedUserInfoStore } from '@/stores'; +import { DEFAULT_LANG } from '@/common/constants'; + +const localDayjs = (langName) => { + langName = langName.replace('_', '-').toLowerCase(); + dayjs.locale(langName); +}; + +export const getCurrentLang = () => { + const loggedUser = loggedUserInfoStore.getState().user; + const adminInterface = interfaceStore.getState().interface; + let currentLang = loggedUser.language; + // `default` mean use language value from admin interface + if (/default/i.test(currentLang) && adminInterface.language) { + currentLang = adminInterface.language; + } + currentLang ||= DEFAULT_LANG; + return currentLang; +}; + +export const setupAppLanguage = () => { + const lang = getCurrentLang(); + localDayjs(lang); + i18next.changeLanguage(lang); +}; + +export const setupAppTimeZone = () => { + // FIXME +}; From 78914647a88129b15efa4afc273f90a8461f837c Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 10:50:13 +0800 Subject: [PATCH 063/157] fix(siteinfostore): fix default site info value in siteInfo store --- ui/src/stores/siteInfo.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts index 626fbf04..14f3a273 100644 --- a/ui/src/stores/siteInfo.ts +++ b/ui/src/stores/siteInfo.ts @@ -12,6 +12,8 @@ const siteInfo = create((set) => ({ name: '', description: '', short_description: '', + site_url: '', + contact_email: '', }, update: (params) => set(() => { From d88aa11edf26e89fc1e56d006696e2568124c2ef Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 10:54:27 +0800 Subject: [PATCH 064/157] fix: installation init add params --- ui/src/pages/Install/index.tsx | 11 ++++++++++- ui/src/services/install/index.ts | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 287f4f11..6201f2cd 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -134,7 +134,16 @@ const Index: FC = () => { }; const checkInstall = () => { - installInit() + const params = { + lang: formData.lang.value, + db_type: formData.db_type.value, + db_username: formData.db_username.value, + db_password: formData.db_password.value, + db_host: formData.db_host.value, + db_name: formData.db_name.value, + db_file: formData.db_file.value, + }; + installInit(params) .then(() => { handleNext(); }) diff --git a/ui/src/services/install/index.ts b/ui/src/services/install/index.ts index 31d50690..a2026a7e 100644 --- a/ui/src/services/install/index.ts +++ b/ui/src/services/install/index.ts @@ -8,8 +8,8 @@ export const dbCheck = (params) => { return request.post('/installation/db/check', params); }; -export const installInit = () => { - return request.post('/installation/init'); +export const installInit = (params) => { + return request.post('/installation/init', params); }; export const installBaseInfo = (params) => { From bd53500116075a57cfe2e87a13e19f4a0b956d6d Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 11:03:27 +0800 Subject: [PATCH 065/157] refactor(admin): move admin header to admin Index --- ui/src/pages/Admin/index.tsx | 3 ++- ui/src/pages/Layout/index.tsx | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx index 27ef8565..f8b389d6 100644 --- a/ui/src/pages/Admin/index.tsx +++ b/ui/src/pages/Admin/index.tsx @@ -3,7 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { AccordionNav, PageTitle } from '@/components'; +import { AccordionNav, AdminHeader, PageTitle } from '@/components'; import { ADMIN_NAV_MENUS } from '@/common/constants'; import './index.scss'; @@ -13,6 +13,7 @@ const Dashboard: FC = () => { return ( <> +
diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index b335a3ee..7539c2fa 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -5,7 +5,7 @@ import { Helmet, HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; import { siteInfoStore, toastStore } from '@/stores'; -import { Header, AdminHeader, Footer, Toast } from '@/components'; +import { Header, Footer, Toast } from '@/components'; const Layout: FC = () => { const { msg: toastMsg, variant, clear: toastClear } = toastStore(); @@ -24,8 +24,6 @@ const Layout: FC = () => { revalidateOnFocus: false, }}>
- {/* TODO: move admin header to Admin/Index */} -
From 3f04532003f1b72ed6b70907aeab195c447a81c2 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 11:16:22 +0800 Subject: [PATCH 066/157] fix(user-interface): fix how to get current lang --- ui/src/common/interface.ts | 5 ++++- ui/src/pages/Users/Settings/Interface/index.tsx | 16 +--------------- ui/src/utils/request.ts | 10 +++------- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index a4c7dcd2..1e61d874 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -116,9 +116,12 @@ export interface UserInfoRes extends UserInfoBase { bio: string; bio_html: string; create_time?: string; - /** value = 1 active; value = 2 inactivated + /** + * value = 1 active; + * value = 2 inactivated */ mail_status: number; + language: string; e_mail?: string; [prop: string]: any; } diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index c39bf0b9..fb78f608 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -5,8 +5,6 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormDataType } from '@/common/interface'; import { useToast } from '@/hooks'; import { getLanguageOptions, updateUserInterface } from '@/services'; -import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; -import Storage from '@/utils/storage'; import { localize } from '@/utils'; import { loggedUserInfoStore } from '@/stores'; @@ -19,8 +17,7 @@ const Index = () => { const [langs, setLangs] = useState(); const [formData, setFormData] = useState({ lang: { - // FIXME: userinfo? or userInfo.language - value: loggedUserInfo, + value: loggedUserInfo.language, isInvalid: false, errorMsg: '', }, @@ -49,17 +46,6 @@ const Index = () => { useEffect(() => { getLangs(); - // TODO: get default lang by interface api - const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); - if (lang) { - setFormData({ - lang: { - value: lang, - isInvalid: false, - errorMsg: '', - }, - }); - } }, []); return (
diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index 8167448d..7859b90d 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -3,12 +3,9 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import { Modal } from '@/components'; import { loggedUserInfoStore, toastStore } from '@/stores'; -import { - LOGGED_TOKEN_STORAGE_KEY, - CURRENT_LANG_STORAGE_KEY, - DEFAULT_LANG, -} from '@/common/constants'; +import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; +import { getCurrentLang } from '@/utils/localize'; import Storage from './storage'; import { floppyNavigation } from './floppyNavigation'; @@ -35,8 +32,7 @@ class Request { this.instance.interceptors.request.use( (requestConfig: AxiosRequestConfig) => { const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || ''; - // default lang en_US - const lang = Storage.get(CURRENT_LANG_STORAGE_KEY) || DEFAULT_LANG; + const lang = getCurrentLang(); requestConfig.headers = { Authorization: token, 'Accept-Language': lang, From 23b4a44ce33bd6b555ad2f063894cc9bfb035a4d Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 4 Nov 2022 11:30:53 +0800 Subject: [PATCH 067/157] feat: rewrite config file when install config --- internal/base/conf/conf.go | 11 +++++++++ internal/cli/install.go | 25 +++------------------ internal/install/install_controller.go | 14 ++++++++---- pkg/writer/writer.go | 31 ++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 pkg/writer/writer.go diff --git a/internal/base/conf/conf.go b/internal/base/conf/conf.go index 55846825..37b091c0 100644 --- a/internal/base/conf/conf.go +++ b/internal/base/conf/conf.go @@ -9,7 +9,9 @@ import ( "github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/router" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/pkg/writer" "github.com/segmentfault/pacman/contrib/conf/viper" + "sigs.k8s.io/yaml" ) // AllConfig all config @@ -48,3 +50,12 @@ func ReadConfig(configFilePath string) (c *AllConfig, err error) { } return c, nil } + +// RewriteConfig rewrite config file path +func RewriteConfig(configFilePath string, allConfig *AllConfig) error { + content, err := yaml.Marshal(allConfig) + if err != nil { + return err + } + return writer.ReplaceFile(configFilePath, string(content)) +} diff --git a/internal/cli/install.go b/internal/cli/install.go index eee7b89a..b31abbf2 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -1,14 +1,13 @@ package cli import ( - "bufio" "fmt" - "os" "path/filepath" "github.com/answerdev/answer/configs" "github.com/answerdev/answer/i18n" "github.com/answerdev/answer/pkg/dir" + "github.com/answerdev/answer/pkg/writer" ) const ( @@ -50,7 +49,7 @@ func InstallConfigFile(configFilePath string) error { } fmt.Printf("[config-file] create directory success, config file is %s\n", configFilePath) - if err := writerFile(configFilePath, string(configs.Config)); err != nil { + if err := writer.WriteFile(configFilePath, string(configs.Config)); err != nil { fmt.Printf("[config-file] install fail %s\n", err.Error()) return fmt.Errorf("write file failed %s", err) } @@ -87,7 +86,7 @@ func installI18nBundle() { continue } fmt.Printf("[i18n] install %s bundle...\n", item.Name()) - err = writerFile(path, string(content)) + err = writer.WriteFile(path, string(content)) if err != nil { fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error()) } else { @@ -95,21 +94,3 @@ func installI18nBundle() { } } } - -func writerFile(filePath, content string) error { - file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666) - if err != nil { - return err - } - defer func() { - _ = file.Close() - }() - writer := bufio.NewWriter(file) - if _, err := writer.WriteString(content); err != nil { - return err - } - if err := writer.Flush(); err != nil { - return err - } - return nil -} diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index 42a67671..1b98db82 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -98,8 +98,7 @@ func InitEnvironment(ctx *gin.Context) { return } - err := cli.InstallConfigFile(confPath) - if err != nil { + if err := cli.InstallConfigFile(confPath); err != nil { handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{ Success: false, CreateConfigFailed: true, @@ -112,8 +111,15 @@ func InitEnvironment(ctx *gin.Context) { c, err := conf.ReadConfig(confPath) if err != nil { log.Errorf("read config failed %s", err) - err = errors.BadRequest(reason.ReadConfigFailed) - handler.HandleResponse(ctx, err, nil) + handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) + return + } + c.Data.Database.Driver = req.DbType + c.Data.Database.Connection = req.GetConnection() + + if err := conf.RewriteConfig(confPath, c); err != nil { + log.Errorf("rewrite config failed %s", err) + handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) return } diff --git a/pkg/writer/writer.go b/pkg/writer/writer.go new file mode 100644 index 00000000..d2b6c440 --- /dev/null +++ b/pkg/writer/writer.go @@ -0,0 +1,31 @@ +package writer + +import ( + "bufio" + "os" +) + +// ReplaceFile remove old file and write new file +func ReplaceFile(filePath, content string) error { + _ = os.Remove(filePath) + return WriteFile(filePath, content) +} + +// WriteFile write file to path +func WriteFile(filePath, content string) error { + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666) + if err != nil { + return err + } + defer func() { + _ = file.Close() + }() + writer := bufio.NewWriter(file) + if _, err := writer.WriteString(content); err != nil { + return err + } + if err := writer.Flush(); err != nil { + return err + } + return nil +} From 9dd41b6248c8fd35a52d5851a6a822eb7b5f16f4 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 11:45:04 +0800 Subject: [PATCH 068/157] fix: install select langs need change i18n --- ui/src/common/constants.ts | 2 +- .../pages/Install/components/FirstStep/index.tsx | 4 ++-- ui/src/pages/Install/index.tsx | 15 ++++++++++++--- ui/src/utils/localize.ts | 6 ++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index b700ad54..028e56e8 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -1,5 +1,5 @@ export const DEFAULT_LANG = 'en_US'; -export const CURRENT_LANG_STORAGE_KEY = '_a_lang__'; +export const CURRENT_LANG_STORAGE_KEY = '_a_lang_'; export const LOGGED_USER_STORAGE_KEY = '_a_lui_'; export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx index 78b4aac8..e35a0f29 100644 --- a/ui/src/pages/Install/components/FirstStep/index.tsx +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormValue, FormDataType } from '@/common/interface'; import Progress from '../Progress'; -import { getInstallLangOptions } from '@/services'; +import { getLanguageOptions } from '@/services'; interface Props { data: FormValue; @@ -18,7 +18,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const [langs, setLangs] = useState(); const getLangs = async () => { - const res: LangsType[] = await getInstallLangOptions(); + const res: LangsType[] = await getLanguageOptions(); setLangs(res); changeCallback({ lang: { diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 6201f2cd..00793fee 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -10,6 +10,8 @@ import { installBaseInfo, checkConfigFileExists, } from '@/services'; +import { Storage } from '@/utils'; +import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import { FirstStep, @@ -170,13 +172,20 @@ const Index: FC = () => { }; const handleStep = () => { + if (step === 1) { + Storage.set(CURRENT_LANG_STORAGE_KEY, formData.lang.value); + handleNext(); + } if (step === 2) { submitDatabaseForm(); - } else if (step === 3) { + } + if (step === 3) { checkInstall(); - } else if (step === 4) { + } + if (step === 4) { submitSiteConfig(); - } else { + } + if (step > 4) { handleNext(); } }; diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts index 4a2b5c05..0c6313b6 100644 --- a/ui/src/utils/localize.ts +++ b/ui/src/utils/localize.ts @@ -2,7 +2,8 @@ import dayjs from 'dayjs'; import i18next from 'i18next'; import { interfaceStore, loggedUserInfoStore } from '@/stores'; -import { DEFAULT_LANG } from '@/common/constants'; +import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; +import { Storage } from '@/utils'; const localDayjs = (langName) => { langName = langName.replace('_', '-').toLowerCase(); @@ -12,12 +13,13 @@ const localDayjs = (langName) => { export const getCurrentLang = () => { const loggedUser = loggedUserInfoStore.getState().user; const adminInterface = interfaceStore.getState().interface; + const storageLang = Storage.get(CURRENT_LANG_STORAGE_KEY); let currentLang = loggedUser.language; // `default` mean use language value from admin interface if (/default/i.test(currentLang) && adminInterface.language) { currentLang = adminInterface.language; } - currentLang ||= DEFAULT_LANG; + currentLang ||= storageLang || DEFAULT_LANG; return currentLang; }; From 25ab726cd1ec62931ea1644a390110c432a1180a Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 11:57:36 +0800 Subject: [PATCH 069/157] fix: delete console --- ui/src/pages/Install/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 00793fee..e42baaa2 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -99,7 +99,7 @@ const Index: FC = () => { }); const handleChange = (params: FormDataType) => { - console.log(params); + // console.log(params); setFormData({ ...formData, ...params }); }; From 3a235fd85b6d8f601708eb57c9bfe1649249349a Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 12:03:42 +0800 Subject: [PATCH 070/157] feat(interface-language): done admin language setting --- ui/src/pages/Admin/Interface/index.tsx | 29 ++++++++------------------ ui/src/services/admin/settings.ts | 5 +++++ ui/src/stores/interface.ts | 3 ++- ui/src/stores/userInfo.ts | 10 ++++++--- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 8a477bdb..d42b6058 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -12,18 +12,19 @@ import { interfaceStore } from '@/stores'; import { UploadImg } from '@/components'; import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; import { - getLanguageOptions, + getAdminLanguageOptions, uploadAvatar, updateInterfaceSetting, useInterfaceSetting, useThemeOptions, } from '@/services'; +import { setupAppLanguage } from '@/utils/localize'; const Interface: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.interface', }); - const { update: interfaceStoreUpdate } = interfaceStore(); + const storeInterface = interfaceStore.getState().interface; const { data: themes } = useThemeOptions(); const Toast = useToast(); const [langs, setLangs] = useState(); @@ -31,17 +32,17 @@ const Interface: FC = () => { const [formData, setFormData] = useState({ logo: { - value: setting?.logo || '', + value: setting?.logo || storeInterface.logo, isInvalid: false, errorMsg: '', }, theme: { - value: setting?.theme || '', + value: setting?.theme || storeInterface.theme, isInvalid: false, errorMsg: '', }, language: { - value: setting?.language || '', + value: setting?.language || storeInterface.language, isInvalid: false, errorMsg: '', }, @@ -52,19 +53,8 @@ const Interface: FC = () => { }, }); const getLangs = async () => { - const res: LangsType[] = await getLanguageOptions(); + const res: LangsType[] = await getAdminLanguageOptions(); setLangs(res); - if (!formData.language.value) { - // set default theme value - setFormData({ - ...formData, - language: { - value: res[0].value, - isInvalid: false, - errorMsg: '', - }, - }); - } }; // set default theme value if (!formData.theme.value && Array.isArray(themes) && themes.length) { @@ -122,7 +112,8 @@ const Interface: FC = () => { msg: t('update', { keyPrefix: 'toast' }), variant: 'success', }); - interfaceStoreUpdate(reqParams); + interfaceStore.getState().update(reqParams); + setupAppLanguage(); }) .catch((err) => { if (err.isError && err.key) { @@ -172,8 +163,6 @@ const Interface: FC = () => { useEffect(() => { getLangs(); }, []); - - console.log('formData', formData); return ( <>

{t('page_title')}

diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index 5f370260..6bae3576 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -83,3 +83,8 @@ export const useDashBoard = () => { error, }; }; + +export const getAdminLanguageOptions = () => { + const apiUrl = `/answer/admin/api/language/options`; + return request.get(apiUrl); +}; diff --git a/ui/src/stores/interface.ts b/ui/src/stores/interface.ts index eddd1db8..e92e0f8a 100644 --- a/ui/src/stores/interface.ts +++ b/ui/src/stores/interface.ts @@ -1,6 +1,7 @@ import create from 'zustand'; import { AdminSettingsInterface } from '@/common/interface'; +import { DEFAULT_LANG } from '@/common/constants'; interface InterfaceType { interface: AdminSettingsInterface; @@ -11,7 +12,7 @@ const interfaceSetting = create((set) => ({ interface: { logo: '', theme: '', - language: '', + language: DEFAULT_LANG, time_zone: '', }, update: (params) => diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index cf1aa860..e82a592b 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -25,17 +25,21 @@ const initUser: UserInfoRes = { website: '', status: '', mail_status: 1, - language: '', + language: 'Default', }; const loggedUserInfoStore = create((set) => ({ user: initUser, - update: (params) => + update: (params) => { + if (!params.language) { + params.language = 'Default'; + } set(() => { Storage.set(LOGGED_TOKEN_STORAGE_KEY, params.access_token); Storage.set(LOGGED_USER_STORAGE_KEY, params); return { user: params }; - }), + }); + }, clear: () => set(() => { Storage.remove(LOGGED_TOKEN_STORAGE_KEY); From 262b29ee28999003cf5a4188e0c3055e334d701c Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 14:54:59 +0800 Subject: [PATCH 071/157] fix: delete upgrade page --- ui/src/i18n/locales/en.json | 9 ----- ui/src/pages/Upgrade/index.tsx | 72 ---------------------------------- ui/src/router/routes.ts | 4 -- ui/src/services/common.ts | 4 -- 4 files changed, 89 deletions(-) delete mode 100644 ui/src/pages/Upgrade/index.tsx diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 4f0de90b..5b767092 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -830,15 +830,6 @@ "installed": "Already installed", "installed_description": "You appear to have already installed. To reinstall please clear your old database tables first." }, - "upgrade": { - "title": "Answer", - "update_btn": "Update data", - "update_title": "Data update required", - "update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.<1>The update process may take a little while, so please be patient.", - "done_title": "No update required", - "done_btn": "Done", - "done_desscription": "Your Answer data is already up-to-date." - }, "page_404": { "description": "Unfortunately, this page doesn't exist.", "back_home": "Back to homepage" diff --git a/ui/src/pages/Upgrade/index.tsx b/ui/src/pages/Upgrade/index.tsx deleted file mode 100644 index c264e46c..00000000 --- a/ui/src/pages/Upgrade/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useState } from 'react'; -import { Container, Row, Col, Card, Button, Spinner } from 'react-bootstrap'; -import { useTranslation, Trans } from 'react-i18next'; - -import { PageTitle } from '@/components'; -import { upgradSystem } from '@/services'; - -const Index = () => { - const { t } = useTranslation('translation', { - keyPrefix: 'upgrade', - }); - const [step] = useState(1); - const [loading, setLoading] = useState(false); - - const handleUpdate = async () => { - await upgradSystem(); - setLoading(true); - }; - return ( -
- - - -
-

{t('title')}

- - - {step === 1 && ( - <> -
{t('update_title')}
- }} - /> - {loading ? ( - - ) : ( - - )} - - )} - - {step === 2 && ( - <> -
{t('done_title')}
-

{t('done_desscription')}

- - - )} -
-
- - - - - ); -}; - -export default Index; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 5723a4e2..24016d0a 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -269,9 +269,5 @@ const routes: RouteNode[] = [ path: '/maintenance', page: 'pages/Maintenance', }, - { - path: '/upgrade', - page: 'pages/Upgrade', - }, ]; export default routes; diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 7b4db720..1ddc7f18 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -248,7 +248,3 @@ export const changeEmailVerify = (params: { code: string }) => { export const getAppSettings = () => { return request.get('/answer/api/v1/siteinfo'); }; - -export const upgradSystem = () => { - return request.post('/answer/api/v1/upgradation'); -}; From 7bea814bd410385a37634aea47dd34ce734eb122 Mon Sep 17 00:00:00 2001 From: kumfo Date: Fri, 4 Nov 2022 15:00:15 +0800 Subject: [PATCH 072/157] fix: change from xorm builder union query to manual build query --- internal/repo/search_common/search_repo.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index b2db83c3..72965c1f 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -142,13 +142,22 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, argsA = append(argsA, votes) } - b = b.Union("all", ub) - - querySQL, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() + //b = b.Union("all", ub) + ubSQL, _, err := ub.ToSQL() if err != nil { return } - countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() + bSQL, _, err := b.ToSQL() + if err != nil { + return + } + sql := fmt.Sprintf("(%s UNION ALL %s)", ubSQL, bSQL) + + querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() + if err != nil { + return + } + countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL() if err != nil { return } From baec9a93004daf270653398b4ee13353e92f4c98 Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 4 Nov 2022 15:31:53 +0800 Subject: [PATCH 073/157] feat: Add timezone setting --- ui/src/common/constants.ts | 663 ++++++++++++------ ui/src/components/FormatTime/index.tsx | 8 +- ui/src/i18n/locales/en.json | 7 +- ui/src/i18n/locales/zh_CN.json | 2 +- ui/src/pages/Admin/Interface/index.tsx | 15 +- ui/src/pages/Questions/Ask/index.tsx | 8 +- .../Detail/components/Alert/index.tsx | 4 +- ui/src/pages/Questions/EditAnswer/index.tsx | 6 +- ui/src/pages/Tags/Edit/index.tsx | 6 +- ui/src/utils/localize.ts | 9 +- 10 files changed, 505 insertions(+), 223 deletions(-) diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 028e56e8..43f92c74 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -56,229 +56,494 @@ export const ADMIN_NAV_MENUS = [ child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], }, ]; -// timezones + export const TIMEZONES = [ { - label: 'UTC-12', - value: 'UTC-12', + label: 'Africa', + options: [ + { value: 'Africa/Abidjan', label: 'Abidjan' }, + { value: 'Africa/Accra', label: 'Accra' }, + { value: 'Africa/Addis_Ababa', label: 'Addis Ababa' }, + { value: 'Africa/Algiers', label: 'Algiers' }, + { value: 'Africa/Asmara', label: 'Asmara' }, + { value: 'Africa/Bamako', label: 'Bamako' }, + { value: 'Africa/Bangui', label: 'Bangui' }, + { value: 'Africa/Banjul', label: 'Banjul' }, + { value: 'Africa/Bissau', label: 'Bissau' }, + { value: 'Africa/Blantyre', label: 'Blantyre' }, + { value: 'Africa/Brazzaville', label: 'Brazzaville' }, + { value: 'Africa/Bujumbura', label: 'Bujumbura' }, + { value: 'Africa/Cairo', label: 'Cairo' }, + { value: 'Africa/Casablanca', label: 'Casablanca' }, + { value: 'Africa/Ceuta', label: 'Ceuta' }, + { value: 'Africa/Conakry', label: 'Conakry' }, + { value: 'Africa/Dakar', label: 'Dakar' }, + { value: 'Africa/Dar_es_Salaam', label: 'Dar es Salaam' }, + { value: 'Africa/Djibouti', label: 'Djibouti' }, + { value: 'Africa/Douala', label: 'Douala' }, + { value: 'Africa/El_Aaiun', label: 'El Aaiun' }, + { value: 'Africa/Freetown', label: 'Freetown' }, + { value: 'Africa/Gaborone', label: 'Gaborone' }, + { value: 'Africa/Harare', label: 'Harare' }, + { value: 'Africa/Johannesburg', label: 'Johannesburg' }, + { value: 'Africa/Juba', label: 'Juba' }, + { value: 'Africa/Kampala', label: 'Kampala' }, + { value: 'Africa/Khartoum', label: 'Khartoum' }, + { value: 'Africa/Kigali', label: 'Kigali' }, + { value: 'Africa/Kinshasa', label: 'Kinshasa' }, + { value: 'Africa/Lagos', label: 'Lagos' }, + { value: 'Africa/Libreville', label: 'Libreville' }, + { value: 'Africa/Lome', label: 'Lome' }, + { value: 'Africa/Luanda', label: 'Luanda' }, + { value: 'Africa/Lubumbashi', label: 'Lubumbashi' }, + { value: 'Africa/Lusaka', label: 'Lusaka' }, + { value: 'Africa/Malabo', label: 'Malabo' }, + { value: 'Africa/Maputo', label: 'Maputo' }, + { value: 'Africa/Maseru', label: 'Maseru' }, + { value: 'Africa/Mbabane', label: 'Mbabane' }, + { value: 'Africa/Mogadishu', label: 'Mogadishu' }, + { value: 'Africa/Monrovia', label: 'Monrovia' }, + { value: 'Africa/Nairobi', label: 'Nairobi' }, + { value: 'Africa/Ndjamena', label: 'Ndjamena' }, + { value: 'Africa/Niamey', label: 'Niamey' }, + { value: 'Africa/Nouakchott', label: 'Nouakchott' }, + { value: 'Africa/Ouagadougou', label: 'Ouagadougou' }, + { value: 'Africa/Porto-Novo', label: 'Porto-Novo' }, + { value: 'Africa/Sao_Tome', label: 'Sao Tome' }, + { value: 'Africa/Tripoli', label: 'Tripoli' }, + { value: 'Africa/Tunis', label: 'Tunis' }, + { value: 'Africa/Windhoek', label: 'Windhoek' }, + ], }, { - label: 'UTC-11:30', - value: 'UTC-11.5', + label: 'America', + options: [ + { value: 'America/Adak', label: 'Adak' }, + { value: 'America/Anchorage', label: 'Anchorage' }, + { value: 'America/Anguilla', label: 'Anguilla' }, + { value: 'America/Antigua', label: 'Antigua' }, + { value: 'America/Araguaina', label: 'Araguaina' }, + { + value: 'America/Argentina/Buenos_Aires', + label: 'Argentina - Buenos Aires', + }, + { value: 'America/Argentina/Catamarca', label: 'Argentina - Catamarca' }, + { value: 'America/Argentina/Cordoba', label: 'Argentina - Cordoba' }, + { value: 'America/Argentina/Jujuy', label: 'Argentina - Jujuy' }, + { value: 'America/Argentina/La_Rioja', label: 'Argentina - La Rioja' }, + { value: 'America/Argentina/Mendoza', label: 'Argentina - Mendoza' }, + { + value: 'America/Argentina/Rio_Gallegos', + label: 'Argentina - Rio Gallegos', + }, + { value: 'America/Argentina/Salta', label: 'Argentina - Salta' }, + { value: 'America/Argentina/San_Juan', label: 'Argentina - San Juan' }, + { value: 'America/Argentina/San_Luis', label: 'Argentina - San Luis' }, + { value: 'America/Argentina/Tucuman', label: 'Argentina - Tucuman' }, + { value: 'America/Argentina/Ushuaia', label: 'Argentina - Ushuaia' }, + { value: 'America/Aruba', label: 'Aruba' }, + { value: 'America/Asuncion', label: 'Asuncion' }, + { value: 'America/Atikokan', label: 'Atikokan' }, + { value: 'America/Bahia', label: 'Bahia' }, + { value: 'America/Bahia_Banderas', label: 'Bahia Banderas' }, + { value: 'America/Barbados', label: 'Barbados' }, + { value: 'America/Belem', label: 'Belem' }, + { value: 'America/Belize', label: 'Belize' }, + { value: 'America/Blanc-Sablon', label: 'Blanc-Sablon' }, + { value: 'America/Boa_Vista', label: 'Boa Vista' }, + { value: 'America/Bogota', label: 'Bogota' }, + { value: 'America/Boise', label: 'Boise' }, + { value: 'America/Cambridge_Bay', label: 'Cambridge Bay' }, + { value: 'America/Campo_Grande', label: 'Campo Grande' }, + { value: 'America/Cancun', label: 'Cancun' }, + { value: 'America/Caracas', label: 'Caracas' }, + { value: 'America/Cayenne', label: 'Cayenne' }, + { value: 'America/Cayman', label: 'Cayman' }, + { value: 'America/Chicago', label: 'Chicago' }, + { value: 'America/Chihuahua', label: 'Chihuahua' }, + { value: 'America/Costa_Rica', label: 'Costa Rica' }, + { value: 'America/Creston', label: 'Creston' }, + { value: 'America/Cuiaba', label: 'Cuiaba' }, + { value: 'America/Curacao', label: 'Curacao' }, + { value: 'America/Danmarkshavn', label: 'Danmarkshavn' }, + { value: 'America/Dawson', label: 'Dawson' }, + { value: 'America/Dawson_Creek', label: 'Dawson Creek' }, + { value: 'America/Denver', label: 'Denver' }, + { value: 'America/Detroit', label: 'Detroit' }, + { value: 'America/Dominica', label: 'Dominica' }, + { value: 'America/Edmonton', label: 'Edmonton' }, + { value: 'America/Eirunepe', label: 'Eirunepe' }, + { value: 'America/El_Salvador', label: 'El Salvador' }, + { value: 'America/Fort_Nelson', label: 'Fort Nelson' }, + { value: 'America/Fortaleza', label: 'Fortaleza' }, + { value: 'America/Glace_Bay', label: 'Glace Bay' }, + { value: 'America/Godthab', label: 'Godthab' }, + { value: 'America/Goose_Bay', label: 'Goose Bay' }, + { value: 'America/Grand_Turk', label: 'Grand Turk' }, + { value: 'America/Grenada', label: 'Grenada' }, + { value: 'America/Guadeloupe', label: 'Guadeloupe' }, + { value: 'America/Guatemala', label: 'Guatemala' }, + { value: 'America/Guayaquil', label: 'Guayaquil' }, + { value: 'America/Guyana', label: 'Guyana' }, + { value: 'America/Halifax', label: 'Halifax' }, + { value: 'America/Havana', label: 'Havana' }, + { value: 'America/Hermosillo', label: 'Hermosillo' }, + { + value: 'America/Indiana/Indianapolis', + label: 'Indiana - Indianapolis', + }, + { value: 'America/Indiana/Knox', label: 'Indiana - Knox' }, + { value: 'America/Indiana/Marengo', label: 'Indiana - Marengo' }, + { value: 'America/Indiana/Petersburg', label: 'Indiana - Petersburg' }, + { value: 'America/Indiana/Tell_City', label: 'Indiana - Tell City' }, + { value: 'America/Indiana/Vevay', label: 'Indiana - Vevay' }, + { value: 'America/Indiana/Vincennes', label: 'Indiana - Vincennes' }, + { value: 'America/Indiana/Winamac', label: 'Indiana - Winamac' }, + { value: 'America/Inuvik', label: 'Inuvik' }, + { value: 'America/Iqaluit', label: 'Iqaluit' }, + { value: 'America/Jamaica', label: 'Jamaica' }, + { value: 'America/Juneau', label: 'Juneau' }, + { value: 'America/Kentucky/Louisville', label: 'Kentucky - Louisville' }, + { value: 'America/Kentucky/Monticello', label: 'Kentucky - Monticello' }, + { value: 'America/Kralendijk', label: 'Kralendijk' }, + { value: 'America/La_Paz', label: 'La Paz' }, + { value: 'America/Lima', label: 'Lima' }, + { value: 'America/Los_Angeles', label: 'Los Angeles' }, + { value: 'America/Lower_Princes', label: 'Lower Princes' }, + { value: 'America/Maceio', label: 'Maceio' }, + { value: 'America/Managua', label: 'Managua' }, + { value: 'America/Manaus', label: 'Manaus' }, + { value: 'America/Marigot', label: 'Marigot' }, + { value: 'America/Martinique', label: 'Martinique' }, + { value: 'America/Matamoros', label: 'Matamoros' }, + { value: 'America/Mazatlan', label: 'Mazatlan' }, + { value: 'America/Miquelon', label: 'Miquelon' }, + { value: 'America/Moncton', label: 'Moncton' }, + { value: 'America/Monterrey', label: 'Monterrey' }, + { value: 'America/Montevideo', label: 'Montevideo' }, + { value: 'America/Montserrat', label: 'Montserrat' }, + { value: 'America/Nassau', label: 'Nassau' }, + { value: 'America/New_York', label: 'New York' }, + { value: 'America/Nipigon', label: 'Nipigon' }, + { value: 'America/Nome', label: 'Nome' }, + { value: 'America/Noronha', label: 'Noronha' }, + { value: 'America/North_Dakota/Beulah', label: 'North Dakota - Beulah' }, + { value: 'America/North_Dakota/Center', label: 'North Dakota - Center' }, + { + value: 'America/North_Dakota/New_Salem', + label: 'North Dakota - New Salem', + }, + { value: 'America/Ojinaga', label: 'Ojinaga' }, + { value: 'America/Panama', label: 'Panama' }, + { value: 'America/Pangnirtung', label: 'Pangnirtung' }, + { value: 'America/Paramaribo', label: 'Paramaribo' }, + { value: 'America/Phoenix', label: 'Phoenix' }, + { value: 'America/Port-au-Prince', label: 'Port-au-Prince' }, + { value: 'America/Port_of_Spain', label: 'Port of Spain' }, + { value: 'America/Porto_Velho', label: 'Porto Velho' }, + { value: 'America/Puerto_Rico', label: 'Puerto Rico' }, + { value: 'America/Punta_Arenas', label: 'Punta Arenas' }, + { value: 'America/Rainy_River', label: 'Rainy River' }, + { value: 'America/Rankin_Inlet', label: 'Rankin Inlet' }, + { value: 'America/Recife', label: 'Recife' }, + { value: 'America/Regina', label: 'Regina' }, + { value: 'America/Resolute', label: 'Resolute' }, + { value: 'America/Rio_Branco', label: 'Rio Branco' }, + { value: 'America/Santarem', label: 'Santarem' }, + { value: 'America/Santiago', label: 'Santiago' }, + { value: 'America/Santo_Domingo', label: 'Santo Domingo' }, + { value: 'America/Sao_Paulo', label: 'Sao Paulo' }, + { value: 'America/Scoresbysund', label: 'Scoresbysund' }, + { value: 'America/Sitka', label: 'Sitka' }, + { value: 'America/St_Barthelemy', label: 'St Barthelemy' }, + { value: 'America/St_Johns', label: 'St Johns' }, + { value: 'America/St_Kitts', label: 'St Kitts' }, + { value: 'America/St_Lucia', label: 'St Lucia' }, + { value: 'America/St_Thomas', label: 'St Thomas' }, + { value: 'America/St_Vincent', label: 'St Vincent' }, + { value: 'America/Swift_Current', label: 'Swift Current' }, + { value: 'America/Tegucigalpa', label: 'Tegucigalpa' }, + { value: 'America/Thule', label: 'Thule' }, + { value: 'America/Thunder_Bay', label: 'Thunder Bay' }, + { value: 'America/Tijuana', label: 'Tijuana' }, + { value: 'America/Toronto', label: 'Toronto' }, + { value: 'America/Tortola', label: 'Tortola' }, + { value: 'America/Vancouver', label: 'Vancouver' }, + { value: 'America/Whitehorse', label: 'Whitehorse' }, + { value: 'America/Winnipeg', label: 'Winnipeg' }, + { value: 'America/Yakutat', label: 'Yakutat' }, + { value: 'America/Yellowknife', label: 'Yellowknife' }, + ], }, { - label: 'UTC-11', - value: 'UTC-11', + label: 'Antarctica', + options: [ + { value: 'Antarctica/Casey', label: 'Casey' }, + { value: 'Antarctica/Davis', label: 'Davis' }, + { value: 'Antarctica/DumontDUrville', label: 'DumontDUrville' }, + { value: 'Antarctica/Macquarie', label: 'Macquarie' }, + { value: 'Antarctica/Mawson', label: 'Mawson' }, + { value: 'Antarctica/McMurdo', label: 'McMurdo' }, + { value: 'Antarctica/Palmer', label: 'Palmer' }, + { value: 'Antarctica/Rothera', label: 'Rothera' }, + { value: 'Antarctica/Syowa', label: 'Syowa' }, + { value: 'Antarctica/Troll', label: 'Troll' }, + { value: 'Antarctica/Vostok', label: 'Vostok' }, + ], }, { - label: 'UTC-10:30', - value: 'UTC-10.5', + label: 'Arctic', + options: [{ value: 'Arctic/Longyearbyen', label: 'Longyearbyen' }], }, { - label: 'UTC-10', - value: 'UTC-10', + label: 'Asia', + options: [ + { value: 'Asia/Aden', label: 'Aden' }, + { value: 'Asia/Almaty', label: 'Almaty' }, + { value: 'Asia/Amman', label: 'Amman' }, + { value: 'Asia/Anadyr', label: 'Anadyr' }, + { value: 'Asia/Aqtau', label: 'Aqtau' }, + { value: 'Asia/Aqtobe', label: 'Aqtobe' }, + { value: 'Asia/Ashgabat', label: 'Ashgabat' }, + { value: 'Asia/Atyrau', label: 'Atyrau' }, + { value: 'Asia/Baghdad', label: 'Baghdad' }, + { value: 'Asia/Bahrain', label: 'Bahrain' }, + { value: 'Asia/Baku', label: 'Baku' }, + { value: 'Asia/Bangkok', label: 'Bangkok' }, + { value: 'Asia/Barnaul', label: 'Barnaul' }, + { value: 'Asia/Beirut', label: 'Beirut' }, + { value: 'Asia/Bishkek', label: 'Bishkek' }, + { value: 'Asia/Brunei', label: 'Brunei' }, + { value: 'Asia/Chita', label: 'Chita' }, + { value: 'Asia/Choibalsan', label: 'Choibalsan' }, + { value: 'Asia/Colombo', label: 'Colombo' }, + { value: 'Asia/Damascus', label: 'Damascus' }, + { value: 'Asia/Dhaka', label: 'Dhaka' }, + { value: 'Asia/Dili', label: 'Dili' }, + { value: 'Asia/Dubai', label: 'Dubai' }, + { value: 'Asia/Dushanbe', label: 'Dushanbe' }, + { value: 'Asia/Famagusta', label: 'Famagusta' }, + { value: 'Asia/Gaza', label: 'Gaza' }, + { value: 'Asia/Hebron', label: 'Hebron' }, + { value: 'Asia/Ho_Chi_Minh', label: 'Ho Chi Minh' }, + { value: 'Asia/Hong_Kong', label: 'Hong Kong' }, + { value: 'Asia/Hovd', label: 'Hovd' }, + { value: 'Asia/Irkutsk', label: 'Irkutsk' }, + { value: 'Asia/Jakarta', label: 'Jakarta' }, + { value: 'Asia/Jayapura', label: 'Jayapura' }, + { value: 'Asia/Jerusalem', label: 'Jerusalem' }, + { value: 'Asia/Kabul', label: 'Kabul' }, + { value: 'Asia/Kamchatka', label: 'Kamchatka' }, + { value: 'Asia/Karachi', label: 'Karachi' }, + { value: 'Asia/Kathmandu', label: 'Kathmandu' }, + { value: 'Asia/Khandyga', label: 'Khandyga' }, + { value: 'Asia/Kolkata', label: 'Kolkata' }, + { value: 'Asia/Krasnoyarsk', label: 'Krasnoyarsk' }, + { value: 'Asia/Kuala_Lumpur', label: 'Kuala Lumpur' }, + { value: 'Asia/Kuching', label: 'Kuching' }, + { value: 'Asia/Kuwait', label: 'Kuwait' }, + { value: 'Asia/Macau', label: 'Macau' }, + { value: 'Asia/Magadan', label: 'Magadan' }, + { value: 'Asia/Makassar', label: 'Makassar' }, + { value: 'Asia/Manila', label: 'Manila' }, + { value: 'Asia/Muscat', label: 'Muscat' }, + { value: 'Asia/Nicosia', label: 'Nicosia' }, + { value: 'Asia/Novokuznetsk', label: 'Novokuznetsk' }, + { value: 'Asia/Novosibirsk', label: 'Novosibirsk' }, + { value: 'Asia/Omsk', label: 'Omsk' }, + { value: 'Asia/Oral', label: 'Oral' }, + { value: 'Asia/Phnom_Penh', label: 'Phnom Penh' }, + { value: 'Asia/Pontianak', label: 'Pontianak' }, + { value: 'Asia/Pyongyang', label: 'Pyongyang' }, + { value: 'Asia/Qatar', label: 'Qatar' }, + { value: 'Asia/Qostanay', label: 'Qostanay' }, + { value: 'Asia/Qyzylorda', label: 'Qyzylorda' }, + { value: 'Asia/Riyadh', label: 'Riyadh' }, + { value: 'Asia/Sakhalin', label: 'Sakhalin' }, + { value: 'Asia/Samarkand', label: 'Samarkand' }, + { value: 'Asia/Seoul', label: 'Seoul' }, + { value: 'Asia/Shanghai', label: 'Shanghai' }, + { value: 'Asia/Singapore', label: 'Singapore' }, + { value: 'Asia/Srednekolymsk', label: 'Srednekolymsk' }, + { value: 'Asia/Taipei', label: 'Taipei' }, + { value: 'Asia/Tashkent', label: 'Tashkent' }, + { value: 'Asia/Tbilisi', label: 'Tbilisi' }, + { value: 'Asia/Tehran', label: 'Tehran' }, + { value: 'Asia/Thimphu', label: 'Thimphu' }, + { value: 'Asia/Tokyo', label: 'Tokyo' }, + { value: 'Asia/Tomsk', label: 'Tomsk' }, + { value: 'Asia/Ulaanbaatar', label: 'Ulaanbaatar' }, + { value: 'Asia/Urumqi', label: 'Urumqi' }, + { value: 'Asia/Ust-Nera', label: 'Ust-Nera' }, + { value: 'Asia/Vientiane', label: 'Vientiane' }, + { value: 'Asia/Vladivostok', label: 'Vladivostok' }, + { value: 'Asia/Yakutsk', label: 'Yakutsk' }, + { value: 'Asia/Yangon', label: 'Yangon' }, + { value: 'Asia/Yekaterinburg', label: 'Yekaterinburg' }, + { value: 'Asia/Yerevan', label: 'Yerevan' }, + ], }, { - label: 'UTC-9:30', - value: 'UTC-9.5', + label: 'Atlantic', + options: [ + { value: 'Atlantic/Azores', label: 'Azores' }, + { value: 'Atlantic/Bermuda', label: 'Bermuda' }, + { value: 'Atlantic/Canary', label: 'Canary' }, + { value: 'Atlantic/Cape_Verde', label: 'Cape Verde' }, + { value: 'Atlantic/Faroe', label: 'Faroe' }, + { value: 'Atlantic/Madeira', label: 'Madeira' }, + { value: 'Atlantic/Reykjavik', label: 'Reykjavik' }, + { value: 'Atlantic/South_Georgia', label: 'South Georgia' }, + { value: 'Atlantic/Stanley', label: 'Stanley' }, + { value: 'Atlantic/St_Helena', label: 'St Helena' }, + ], }, { - label: 'UTC-9', - value: 'UTC-9', + label: 'Australia', + options: [ + { value: 'Australia/Adelaide', label: 'Adelaide' }, + { value: 'Australia/Brisbane', label: 'Brisbane' }, + { value: 'Australia/Broken_Hill', label: 'Broken Hill' }, + { value: 'Australia/Currie', label: 'Currie' }, + { value: 'Australia/Darwin', label: 'Darwin' }, + { value: 'Australia/Eucla', label: 'Eucla' }, + { value: 'Australia/Hobart', label: 'Hobart' }, + { value: 'Australia/Lindeman', label: 'Lindeman' }, + { value: 'Australia/Lord_Howe', label: 'Lord Howe' }, + { value: 'Australia/Melbourne', label: 'Melbourne' }, + { value: 'Australia/Perth', label: 'Perth' }, + { value: 'Australia/Sydney', label: 'Sydney' }, + ], }, { - label: 'UTC-8:30', - value: 'UTC-8.5', + label: 'Europe', + options: [ + { value: 'Europe/Amsterdam', label: 'Amsterdam' }, + { value: 'Europe/Andorra', label: 'Andorra' }, + { value: 'Europe/Astrakhan', label: 'Astrakhan' }, + { value: 'Europe/Athens', label: 'Athens' }, + { value: 'Europe/Belgrade', label: 'Belgrade' }, + { value: 'Europe/Berlin', label: 'Berlin' }, + { value: 'Europe/Bratislava', label: 'Bratislava' }, + { value: 'Europe/Brussels', label: 'Brussels' }, + { value: 'Europe/Bucharest', label: 'Bucharest' }, + { value: 'Europe/Budapest', label: 'Budapest' }, + { value: 'Europe/Busingen', label: 'Busingen' }, + { value: 'Europe/Chisinau', label: 'Chisinau' }, + { value: 'Europe/Copenhagen', label: 'Copenhagen' }, + { value: 'Europe/Dublin', label: 'Dublin' }, + { value: 'Europe/Gibraltar', label: 'Gibraltar' }, + { value: 'Europe/Guernsey', label: 'Guernsey' }, + { value: 'Europe/Helsinki', label: 'Helsinki' }, + { value: 'Europe/Isle_of_Man', label: 'Isle of Man' }, + { value: 'Europe/Istanbul', label: 'Istanbul' }, + { value: 'Europe/Jersey', label: 'Jersey' }, + { value: 'Europe/Kaliningrad', label: 'Kaliningrad' }, + { value: 'Europe/Kiev', label: 'Kiev' }, + { value: 'Europe/Kirov', label: 'Kirov' }, + { value: 'Europe/Lisbon', label: 'Lisbon' }, + { value: 'Europe/Ljubljana', label: 'Ljubljana' }, + { value: 'Europe/London', label: 'London' }, + { value: 'Europe/Luxembourg', label: 'Luxembourg' }, + { value: 'Europe/Madrid', label: 'Madrid' }, + { value: 'Europe/Malta', label: 'Malta' }, + { value: 'Europe/Mariehamn', label: 'Mariehamn' }, + { value: 'Europe/Minsk', label: 'Minsk' }, + { value: 'Europe/Monaco', label: 'Monaco' }, + { value: 'Europe/Moscow', label: 'Moscow' }, + { value: 'Europe/Oslo', label: 'Oslo' }, + { value: 'Europe/Paris', label: 'Paris' }, + { value: 'Europe/Podgorica', label: 'Podgorica' }, + { value: 'Europe/Prague', label: 'Prague' }, + { value: 'Europe/Riga', label: 'Riga' }, + { value: 'Europe/Rome', label: 'Rome' }, + { value: 'Europe/Samara', label: 'Samara' }, + { value: 'Europe/San_Marino', label: 'San Marino' }, + { value: 'Europe/Sarajevo', label: 'Sarajevo' }, + { value: 'Europe/Saratov', label: 'Saratov' }, + { value: 'Europe/Simferopol', label: 'Simferopol' }, + { value: 'Europe/Skopje', label: 'Skopje' }, + { value: 'Europe/Sofia', label: 'Sofia' }, + { value: 'Europe/Stockholm', label: 'Stockholm' }, + { value: 'Europe/Tallinn', label: 'Tallinn' }, + { value: 'Europe/Tirane', label: 'Tirane' }, + { value: 'Europe/Ulyanovsk', label: 'Ulyanovsk' }, + { value: 'Europe/Uzhgorod', label: 'Uzhgorod' }, + { value: 'Europe/Vaduz', label: 'Vaduz' }, + { value: 'Europe/Vatican', label: 'Vatican' }, + { value: 'Europe/Vienna', label: 'Vienna' }, + { value: 'Europe/Vilnius', label: 'Vilnius' }, + { value: 'Europe/Volgograd', label: 'Volgograd' }, + { value: 'Europe/Warsaw', label: 'Warsaw' }, + { value: 'Europe/Zagreb', label: 'Zagreb' }, + { value: 'Europe/Zaporozhye', label: 'Zaporozhye' }, + { value: 'Europe/Zurich', label: 'Zurich' }, + ], }, { - label: 'UTC-8', - value: 'UTC-8', + label: 'Indian', + options: [ + { value: 'Indian/Antananarivo', label: 'Antananarivo' }, + { value: 'Indian/Chagos', label: 'Chagos' }, + { value: 'Indian/Christmas', label: 'Christmas' }, + { value: 'Indian/Cocos', label: 'Cocos' }, + { value: 'Indian/Comoro', label: 'Comoro' }, + { value: 'Indian/Kerguelen', label: 'Kerguelen' }, + { value: 'Indian/Mahe', label: 'Mahe' }, + { value: 'Indian/Maldives', label: 'Maldives' }, + { value: 'Indian/Mauritius', label: 'Mauritius' }, + { value: 'Indian/Mayotte', label: 'Mayotte' }, + { value: 'Indian/Reunion', label: 'Reunion' }, + ], }, { - label: 'UTC-7:30', - value: 'UTC-7.5', - }, - { - label: 'UTC-7', - value: 'UTC-7', - }, - { - label: 'UTC-6:30', - value: 'UTC-6.5', - }, - { - label: 'UTC-6', - value: 'UTC-6', - }, - { - label: 'UTC-5:30', - value: 'UTC-5.5', - }, - { - label: 'UTC-5', - value: 'UTC-5', - }, - { - label: 'UTC-4:30', - value: 'UTC-4.5', - }, - { - label: 'UTC-4', - value: 'UTC-4', - }, - { - label: 'UTC-3:30', - value: 'UTC-3.5', - }, - { - label: 'UTC-3', - value: 'UTC-3', - }, - { - label: 'UTC-2:30', - value: 'UTC-2.5', - }, - { - label: 'UTC-2', - value: 'UTC-2', - }, - { - label: 'UTC-1:30', - value: 'UTC-1.5', - }, - { - label: 'UTC-1', - value: 'UTC-1', - }, - { - label: 'UTC-0:30', - value: 'UTC-0.5', - }, - { - label: 'UTC+0', - value: 'UTC+0', - }, - { - label: 'UTC+0:30', - value: 'UTC+0.5', - }, - { - label: 'UTC+1', - value: 'UTC+1', - }, - { - label: 'UTC+1:30', - value: 'UTC+1.5', - }, - { - label: 'UTC+2', - value: 'UTC+2', - }, - { - label: 'UTC+2:30', - value: 'UTC+2.5', - }, - { - label: 'UTC+3', - value: 'UTC+3', - }, - { - label: 'UTC+3:30', + label: 'Pacific', + options: [ + { value: 'Pacific/Apia', label: 'Apia' }, + { value: 'Pacific/Auckland', label: 'Auckland' }, + { value: 'Pacific/Bougainville', label: 'Bougainville' }, + { value: 'Pacific/Chatham', label: 'Chatham' }, + { value: 'Pacific/Chuuk', label: 'Chuuk' }, + { value: 'Pacific/Easter', label: 'Easter' }, + { value: 'Pacific/Efate', label: 'Efate' }, + { value: 'Pacific/Enderbury', label: 'Enderbury' }, + { value: 'Pacific/Fakaofo', label: 'Fakaofo' }, + { value: 'Pacific/Fiji', label: 'Fiji' }, + { value: 'Pacific/Funafuti', label: 'Funafuti' }, - value: 'UTC+3.5', + { value: 'Pacific/Galapagos', label: 'Galapagos' }, + { value: 'Pacific/Gambier', label: 'Gambier' }, + { value: 'Pacific/Guadalcanal', label: 'Guadalcanal' }, + { value: 'Pacific/Guam', label: 'Guam' }, + { value: 'Pacific/Honolulu', label: 'Honolulu' }, + { value: 'Pacific/Kiritimati', label: 'Kiritimati' }, + { value: 'Pacific/Kosrae', label: 'Kosrae' }, + { value: 'Pacific/Kwajalein', label: 'Kwajalein' }, + { value: 'Pacific/Majuro', label: 'Majuro' }, + { value: 'Pacific/Marquesas', label: 'Marquesas' }, + { value: 'Pacific/Midway', label: 'Midway' }, + { value: 'Pacific/Nauru', label: 'Nauru' }, + { value: 'Pacific/Niue', label: 'Niue' }, + { value: 'Pacific/Norfolk', label: 'Norfolk' }, + { value: 'Pacific/Noumea', label: 'Noumea' }, + { value: 'Pacific/Pago_Pago', label: 'Pago Pago' }, + { value: 'Pacific/Palau', label: 'Palau' }, + { value: 'Pacific/Pitcairn', label: 'Pitcairn' }, + { value: 'Pacific/Pohnpei', label: 'Pohnpei' }, + { value: 'Pacific/Port_Moresby', label: 'Port Moresby' }, + { value: 'Pacific/Rarotonga', label: 'Rarotonga' }, + { value: 'Pacific/Saipan', label: 'Saipan' }, + { value: 'Pacific/Tahiti', label: 'Tahiti' }, + { value: 'Pacific/Tarawa', label: 'Tarawa' }, + { value: 'Pacific/Tongatapu', label: 'Tongatapu' }, + { value: 'Pacific/Wake', label: 'Wake' }, + { value: 'Pacific/Wallis', label: 'Wallis' }, + ], }, - { - label: 'UTC+4', - value: 'UTC+4', - }, - { - label: 'UTC+4:30', - value: 'UTC+4.5', - }, - { - label: 'UTC+5', - value: 'UTC+5', - }, - { - label: 'UTC+5:30', - value: 'UTC+5.5', - }, - { - label: 'UTC+5:45', - value: 'UTC+5.75', - }, - { - label: 'UTC+6', - value: 'UTC+6', - }, - { - label: 'UTC+6:30', - value: 'UTC+6.5', - }, { - label: 'UTC+7', - value: 'UTC+7', - }, - { - label: 'UTC+7:30', - value: 'UTC+7.5', - }, - { - label: 'UTC+8', - value: 'UTC+8', - }, - { - label: 'UTC+8:30', - value: 'UTC+8.5', - }, - { - label: 'UTC+8:45', - value: 'UTC+8.75', - }, - { - label: 'UTC+9', - value: 'UTC+9', - }, - { - label: 'UTC+9:30', - value: 'UTC+9.5', - }, - { - label: 'UTC+10', - value: 'UTC+10', - }, - { - label: 'UTC+10:30', - value: 'UTC+10.5', - }, - { - label: 'UTC+11', - value: 'UTC+11', - }, - { - label: 'UTC+11:30', - value: 'UTC+11.5', - }, - { - label: 'UTC+12', - value: 'UTC+12', - }, - { - label: 'UTC+12:45', - value: 'UTC+12.75', - }, - { - label: 'UTC+13', - value: 'UTC+13', - }, - { - label: 'UTC+13:45', - value: 'UTC+13.75', - }, - { - label: 'UTC+14', - value: 'UTC+14', + label: 'UTC', + options: [{ value: 'UTC', label: 'UTC' }], }, ]; export const DEFAULT_TIMEZONE = 'UTC+0'; diff --git a/ui/src/components/FormatTime/index.tsx b/ui/src/components/FormatTime/index.tsx index 45649087..d3a4d024 100644 --- a/ui/src/components/FormatTime/index.tsx +++ b/ui/src/components/FormatTime/index.tsx @@ -37,10 +37,10 @@ const Index: FC = ({ time, preFix, className }) => { between < 3600 * 24 * 366 && dayjs.unix(from).format('YYYY') === dayjs.unix(now).format('YYYY') ) { - return dayjs.unix(from).format(t('dates.long_date')); + return dayjs.unix(from).tz().format(t('dates.long_date')); } - return dayjs.unix(from).format(t('dates.long_date_with_year')); + return dayjs.unix(from).tz().format(t('dates.long_date_with_year')); }; if (!time) { @@ -50,8 +50,8 @@ const Index: FC = ({ time, preFix, className }) => { return ( diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 5b767092..76c30c3a 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -800,10 +800,9 @@ "label": "Contact Email", "text": "Email address of key contact responsible for this site.", "msg": { - "empty": "Contact Email cannot be empty.", + "empty": "Contact Email cannot be empty.", "incorrect": "Contact Email incorrect format." } - }, "admin_name": { "label": "Name", @@ -818,7 +817,7 @@ "label": "Email", "text": "You will need this email to log in.", "msg": { - "empty": "Email cannot be empty.", + "empty": "Email cannot be empty.", "incorrect": "Email incorrect format." } }, @@ -1029,7 +1028,7 @@ "time_zone": { "label": "Timezone", "msg": "Timezone cannot be empty.", - "text": "Choose a UTC (Coordinated Universal Time) time offset." + "text": "Choose a city in the same timezone as you." } }, "smtp": { diff --git a/ui/src/i18n/locales/zh_CN.json b/ui/src/i18n/locales/zh_CN.json index a5e25ea3..72069e9e 100644 --- a/ui/src/i18n/locales/zh_CN.json +++ b/ui/src/i18n/locales/zh_CN.json @@ -283,7 +283,7 @@ "btn_cancel": "取消" }, "dates": { - "long_date": "YYYY年MM月", + "long_date": "MM月DD日", "long_date_with_year": "YYYY年MM月DD日", "long_date_with_time": "YYYY年MM月DD日 HH:mm", "now": "刚刚", diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index d42b6058..e4fab665 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -18,7 +18,7 @@ import { useInterfaceSetting, useThemeOptions, } from '@/services'; -import { setupAppLanguage } from '@/utils/localize'; +import { setupAppLanguage, setupAppTimeZone } from '@/utils/localize'; const Interface: FC = () => { const { t } = useTranslation('translation', { @@ -114,6 +114,7 @@ const Interface: FC = () => { }); interfaceStore.getState().update(reqParams); setupAppLanguage(); + setupAppTimeZone(); }) .catch((err) => { if (err.isError && err.key) { @@ -258,9 +259,15 @@ const Interface: FC = () => { }}> {TIMEZONES?.map((item) => { return ( - + + {item.options.map((option) => { + return ( + + ); + })} + ); })} diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index fe4d3319..3ed72f84 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -281,9 +281,11 @@ const Ask = () => { {revisions.map( ({ reason, create_at, user_info }, index) => { - const date = dayjs(create_at * 1000).format( - t('long_date_with_time', { keyPrefix: 'dates' }), - ); + const date = dayjs(create_at * 1000) + .tz() + .format( + t('long_date_with_time', { keyPrefix: 'dates' }), + ); return ( + From 8ddc4cf44d4287c799f1e6461cba49bdcaef2762 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 12:03:39 +0800 Subject: [PATCH 084/157] update: site info service reference --- cmd/answer/wire_gen.go | 11 +++++------ internal/controller/lang_controller.go | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 57f5a5f6..49e980eb 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -93,18 +93,18 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, return nil, nil, err } siteInfoRepo := site_info.NewSiteInfo(dataData) - configRepo := config.NewConfigRepo(dataData) - emailRepo := export.NewEmailRepo(dataData) - emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) - langController := controller.NewLangController(i18nTranslator, siteInfoService) + siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) + langController := controller.NewLangController(i18nTranslator, siteInfoCommonService) authRepo := auth.NewAuthRepo(dataData) authService := auth2.NewAuthService(authRepo) + configRepo := config.NewConfigRepo(dataData) userRepo := user.NewUserRepo(dataData, configRepo) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo) userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) + emailRepo := export.NewEmailRepo(dataData) + emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) @@ -152,7 +152,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 384d711c..176d1d0a 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -5,18 +5,18 @@ import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { translator i18n.Translator - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController { +func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController { return &LangController{translator: tr, siteInfoService: siteInfoService} } From d6b65c3da512f24d617d4260ca5b8d17a97d2c81 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 7 Nov 2022 12:34:08 +0800 Subject: [PATCH 085/157] refactor(admin): Form field validity verification --- ui/src/pages/Admin/General/index.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index cdce67bb..2ea2f485 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -62,7 +62,15 @@ const General: FC = () => { isInvalid: true, errorMsg: t('site_url.msg'), }; + } else if (!/^(https?):\/\/([\w.]+\/?)\S*$/.test(site_url.value)) { + ret = false; + formData.site_url = { + value: formData.site_url.value, + isInvalid: true, + errorMsg: t('site_url.validate'), + }; } + if (!contact_email.value) { ret = false; formData.contact_email = { @@ -70,6 +78,17 @@ const General: FC = () => { isInvalid: true, errorMsg: t('contact_email.msg'), }; + } else if ( + !/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test( + contact_email.value, + ) + ) { + ret = false; + formData.contact_email = { + value: formData.contact_email.value, + isInvalid: true, + errorMsg: t('contact_email.validate'), + }; } setFormData({ ...formData, From f4a2b6b614fcf97416db0070a9c00cd434b775d1 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 7 Nov 2022 12:34:22 +0800 Subject: [PATCH 086/157] refactor: update en.json --- ui/src/i18n/locales/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 76c30c3a..67a91cfa 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -990,6 +990,7 @@ "site_url": { "label": "Site URL", "msg": "Site url cannot be empty.", + "validate": "Please enter a valid URL.", "text": "The address of your site." }, "short_description": { @@ -1005,6 +1006,7 @@ "contact_email": { "label": "Contact Email", "msg": "Contact email cannot be empty.", + "validate": "Contact email is not valid.", "text": "Email address of key contact responsible for this site." } }, From 088d3876fcddf3014caaf6e84808740d3d86499c Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 12:43:44 +0800 Subject: [PATCH 087/157] doc: update install document --- INSTALL_CN.md | 108 ++++----------------------------- docs/img/install-database.png | Bin 0 -> 13214 bytes docs/img/install-site-info.png | Bin 0 -> 43093 bytes 3 files changed, 13 insertions(+), 95 deletions(-) create mode 100644 docs/img/install-database.png create mode 100644 docs/img/install-site-info.png diff --git a/INSTALL_CN.md b/INSTALL_CN.md index 300851a2..848c9ce1 100644 --- a/INSTALL_CN.md +++ b/INSTALL_CN.md @@ -1,106 +1,24 @@ # Answer 安装指引 -安装 Answer 之前,您需要先安装基本环境。 - - 数据库 - - [MySQL](http://dev.mysql.com):版本 >= 5.7 - -然后,您可以通过以下几种方式来安装 Answer: - - - 采用 Docker 部署 - - 二进制安装 - - 源码安装 - -## 使用 Docker-compose 安装 Answer +## 使用 docker 安装 +### 步骤 1: 使用 docker 命令启动项目 ```bash -$ mkdir answer && cd answer -$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml -$ docker-compose up +docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest ``` +### 步骤 2: 访问安装路径进行项目安装 +[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) -启动完成后使用浏览器访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/). +选择语言后点击下一步选择合适的数据库,如果当前只是想体验,建议直接选择 sqlite 作为数据库,如下图所示 -你可以使用默认的用户名:( **`admin@admin.com`** ) 和密码:( **`admin`** ) 进行登录. +![install-database](docs/img/install-database.png) -## 使用Docker 安装 Answer -可以从 Docker Hub 或者 GitHub Container registry 下载最新的 tags 镜像 +然后点击下一步会进行配置文件创建等操作,点击下一步输入网站基本信息和管理员信息,如下图所示 -### 用法 -将配置和存储目录挂在到镜像之外 volume (/var/data -> /data),你可以修改外部挂载地址 +![install-site-info](docs/img/install-site-info.png) -``` -# 将镜像从 docker hub 拉到本地 -$ docker pull answerdev/answer:latest +点击下一步即可安装完成 -# 创建一个挂载目录 -$ mkdir -p /var/data +### 步骤 3:安装完成后访问项目路径开始使用 +[http://127.0.0.1:9080/](http://127.0.0.1:9080/) -# 先运行一遍镜像 -$ docker run --name=answer -p 9080:80 -v /var/data:/data answerdev/answer - -# 第一次启动后会在/var/data 目录下生成配置文件 -# /var/data/conf/config.yaml -# 需要修改配置文件中的Mysql 数据库地址 -vim /var/data/conf/config.yaml - -# 修改数据库连接 connection: [username]:[password]@tcp([host]:[port])/[DbName] -... - -# 配置好配置文件后可以再次启动镜像即可启动服务 -$ docker start answer -``` - -## 使用二进制 安装 Answer -可以使用编译完成的各个平台的二进制文件运行 Answer 项目 -### 用法 -从 GitHub 最新版本的tag中下载对应平台的二进制文件压缩包 - - 1. 解压压缩包 - 2. 使用命令 cd 进入到刚刚创建的目录 - 3. 执行命令 ./answer init - 4. Answer 会在当前目录生成 ./data 目录 - 5. 进入 data 目录修改 config.yaml 文件 - 6. 将数据库连接地址修改为你的数据库连接地址 - - connection: [username]:[password]@tcp([host]:[port])/[DbName] - 7. 退出 data 目录,执行 ./answer run -c ./data/conf/config.yaml - -## 当前支持的命令 -用法: answer [command] - -- help: 帮助 -- init: 初始化环境 -- run: 启动 -- check: 环境依赖检查 -- dump: 备份数据 - -## 配置文件 config.yaml 参数说明 - -``` -server: - http: - addr: 0.0.0.0:80 #项目访问端口号 -data: - database: - connection: root:root@tcp(127.0.0.1:3306)/answer #mysql数据库连接地址 - cache: - file_path: "/tmp/cache/cache.db" #缓存文件存放路径 -i18n: - bundle_dir: "/data/i18n" #国际化文件存放目录 -swaggerui: - show: true #是否显示swaggerapi文档,地址 /swagger/index.html - protocol: http #swagger 协议头 - host: 127.0.0.1 #可被访问的ip地址或域名 - address: ':80' #可被访问的端口号 -service_config: - secret_key: "answer" #加密key - web_host: "http://127.0.0.1" #页面访问使用域名地址 - upload_path: "./upfiles" #上传目录 -``` - -## 编译镜像 -如果修改了源文件并且要重新打包镜像可以使用以下语句重新打包镜像 -``` -docker build -t answer:v1.0.0 . -``` -## 常见问题 - 1. 项目无法启动,answer 主程序启动依赖配置文件 config.yaml 、国际化翻译目录 /i18n 、上传文件存放目录 /upfiles,需要确保项目启动时加载了配置文件 answer run -c config.yaml 以及在 config.yaml 正确的指定 i18n 和 upfiles 目录的配置项 +使用刚才创建的管理员用户名密码即可登录。 diff --git a/docs/img/install-database.png b/docs/img/install-database.png new file mode 100644 index 0000000000000000000000000000000000000000..09fbf36a18bb039cfaec997dd76fc6e4b9c12599 GIT binary patch literal 13214 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 literal 0 HcmV?d00001 diff --git a/docs/img/install-site-info.png b/docs/img/install-site-info.png new file mode 100644 index 0000000000000000000000000000000000000000..b8166caf426bca8f51a475b8c23da10617761ed1 GIT binary patch literal 43093 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 DY$>gM literal 0 HcmV?d00001 From bf730b954abf3dfafde2487af9b9cd678705738d Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 7 Nov 2022 12:45:01 +0800 Subject: [PATCH 088/157] add version --- cmd/answer/main.go | 4 +- docs/docs.go | 4 +- docs/swagger.json | 4 +- docs/swagger.yaml | 4 +- internal/base/constant/constant.go | 2 + internal/schema/dashboard_schema.go | 37 +++++++++++++------ .../service/dashboard/dashboard_service.go | 32 ++++++++++++++++ 7 files changed, 68 insertions(+), 19 deletions(-) diff --git a/cmd/answer/main.go b/cmd/answer/main.go index 08214244..e9308442 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/answerdev/answer/internal/base/conf" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/cli" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman" @@ -40,7 +41,6 @@ func main() { func runApp() { log.SetLogger(zap.NewLogger( log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) - c, err := readConfig() if err != nil { panic(err) @@ -50,6 +50,8 @@ func runApp() { if err != nil { panic(err) } + constant.Version = Version + defer cleanup() if err := app.Run(); err != nil { panic(err) diff --git a/docs/docs.go b/docs/docs.go index 5a209686..5c31c2fb 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2717,14 +2717,14 @@ const docTemplate = `{ }, "/answer/api/v1/siteinfo": { "get": { - "description": "Get siteinfo", + "description": "get site info", "produces": [ "application/json" ], "tags": [ "site" ], - "summary": "Get siteinfo", + "summary": "get site info", "responses": { "200": { "description": "OK", diff --git a/docs/swagger.json b/docs/swagger.json index 362039a0..f7545616 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2705,14 +2705,14 @@ }, "/answer/api/v1/siteinfo": { "get": { - "description": "Get siteinfo", + "description": "get site info", "produces": [ "application/json" ], "tags": [ "site" ], - "summary": "Get siteinfo", + "summary": "get site info", "responses": { "200": { "description": "OK", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 76c5217d..8fab9053 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3043,7 +3043,7 @@ paths: - Search /answer/api/v1/siteinfo: get: - description: Get siteinfo + description: get site info produces: - application/json responses: @@ -3056,7 +3056,7 @@ paths: data: $ref: '#/definitions/schema.SiteGeneralResp' type: object - summary: Get siteinfo + summary: get site info tags: - site /answer/api/v1/tag: diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 5a182716..e41d50ea 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -27,6 +27,8 @@ const ( // object TagID AnswerList // key equal database's table name var ( + Version string = "" + ObjectTypeStrMapping = map[string]int{ QuestionObjectType: 1, AnswerObjectType: 2, diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index 74eb8ca9..bba29da1 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -1,16 +1,29 @@ package schema type DashboardInfo struct { - QuestionCount int64 `json:"question_count"` - AnswerCount int64 `json:"answer_count"` - CommentCount int64 `json:"comment_count"` - VoteCount int64 `json:"vote_count"` - UserCount int64 `json:"user_count"` - ReportCount int64 `json:"report_count"` - UploadingFiles bool `json:"uploading_files"` - SMTP bool `json:"smtp"` - HTTPS bool `json:"https"` - TimeZone string `json:"time_zone"` - OccupyingStorageSpace string `json:"occupying_storage_space"` - AppStartTime string `json:"app_start_time"` + QuestionCount int64 `json:"question_count"` + AnswerCount int64 `json:"answer_count"` + CommentCount int64 `json:"comment_count"` + VoteCount int64 `json:"vote_count"` + UserCount int64 `json:"user_count"` + ReportCount int64 `json:"report_count"` + UploadingFiles bool `json:"uploading_files"` + SMTP bool `json:"smtp"` + HTTPS bool `json:"https"` + TimeZone string `json:"time_zone"` + OccupyingStorageSpace string `json:"occupying_storage_space"` + AppStartTime string `json:"app_start_time"` + VersionInfo DashboardInfoVersion `json:"version_info"` +} + +type DashboardInfoVersion struct { + Version string `json:"version"` + RemoteVersion string `json:"remote_version"` +} + +type RemoteVersion struct { + Release struct { + Version string `json:"version"` + URL string `json:"url"` + } `json:"release"` } diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index fcea03dd..e642a85c 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -2,7 +2,11 @@ package dashboard import ( "context" + "encoding/json" + "io/ioutil" + "net/http" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity_common" answercommon "github.com/answerdev/answer/internal/service/answer_common" @@ -12,6 +16,7 @@ import ( "github.com/answerdev/answer/internal/service/report_common" "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/segmentfault/pacman/log" ) type DashboardService struct { @@ -112,5 +117,32 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.OccupyingStorageSpace = "1MB" dashboardInfo.AppStartTime = "102" dashboardInfo.TimeZone = siteInfoInterface.TimeZone + dashboardInfo.VersionInfo.Version = constant.Version + dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx) return dashboardInfo, nil } + +func (ds *DashboardService) RemoteVersion(ctx context.Context) string { + url := "https://answer.dev/getlatest" + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("User-Agent", "Answer/"+constant.Version) + resp, err := (&http.Client{}).Do(req) + if err != nil { + log.Error("http.Client error", err) + return "" + } + defer resp.Body.Close() + + respByte, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Error("http.Client error", err) + return "" + } + remoteVersion := &schema.RemoteVersion{} + err = json.Unmarshal(respByte, remoteVersion) + if err != nil { + log.Error("json.Unmarshal error", err) + return "" + } + return remoteVersion.Release.Version +} From 8f567e0abd22b0f86245595b81d93ab2a11a818f Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 7 Nov 2022 14:27:54 +0800 Subject: [PATCH 089/157] feat: get text excerpt --- go.mod | 1 + go.sum | 2 ++ pkg/htmltext/htmltext.go | 44 ++++++++++++++++++++++++++++++ pkg/htmltext/htmltext_test.go | 51 +++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 pkg/htmltext/htmltext.go create mode 100644 pkg/htmltext/htmltext_test.go diff --git a/go.mod b/go.mod index d6d85117..a3a2470c 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/goccy/go-json v0.9.11 github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 + github.com/grokify/html-strip-tags-go v0.0.1 github.com/jinzhu/copier v0.3.5 github.com/jinzhu/now v1.1.5 github.com/lib/pq v1.10.7 diff --git a/go.sum b/go.sum index be85073b..2e07c8ff 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go new file mode 100644 index 00000000..0694d990 --- /dev/null +++ b/pkg/htmltext/htmltext.go @@ -0,0 +1,44 @@ +package htmltext + +import ( + "github.com/grokify/html-strip-tags-go" + "regexp" + "strings" +) + +// ClearText clear HTML, get the clear text +func ClearText(html string) (text string) { + var ( + re *regexp.Regexp + codeReg = `(?ism)<(pre)>.*<\/pre>` + codeRepl = "{code...}" + linkReg = `(?ism).*?<\/a>` + linkRepl = "[link]" + spaceReg = ` +` + spaceRepl = " " + ) + re = regexp.MustCompile(codeReg) + html = re.ReplaceAllString(html, codeRepl) + + re = regexp.MustCompile(linkReg) + html = re.ReplaceAllString(html, linkRepl) + + text = strings.NewReplacer( + "\n", " ", + "\r", " ", + "\t", " ", + ).Replace(strip.StripTags(html)) + + // replace multiple spaces to one space + re = regexp.MustCompile(spaceReg) + text = strings.TrimSpace(re.ReplaceAllString(text, spaceRepl)) + return +} + +// FetchExcerpt return the excerpt from the HTML string +func FetchExcerpt(html, trimMarker string, limit int) (text string) { + text = ClearText(html) + runeText := []rune(text) + text = string(runeText[0:limit]) + return +} diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go new file mode 100644 index 00000000..353ca20b --- /dev/null +++ b/pkg/htmltext/htmltext_test.go @@ -0,0 +1,51 @@ +package htmltext + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestClearText(t *testing.T) { + var ( + expected, + clearedText string + ) + + // test code clear text + expected = "hello{code...}" + clearedText = ClearText("

hello

var a = \"good\"

") + assert.Equal(t, expected, clearedText) + + // test link clear text + expected = "hello[link]" + clearedText = ClearText("

helloexample.com

") + assert.Equal(t, expected, clearedText) + clearedText = ClearText("

helloexample.com

") + assert.Equal(t, expected, clearedText) + + expected = "hello world" + clearedText = ClearText("
hello
\n
world
") + assert.Equal(t, expected, clearedText) +} + +func TestFetchExcerpt(t *testing.T) { + var ( + expected, + text string + ) + + // test english string + expected = "hello" + text = FetchExcerpt("

hello world

", "...", 5) + assert.Equal(t, expected, text) + + // test mixed string + expected = "hello你好" + text = FetchExcerpt("

hello你好world

", "...", 7) + assert.Equal(t, expected, text) + + // test mixed string with emoticon + expected = "hello你好😂" + text = FetchExcerpt("

hello你好😂world

", "...", 8) + assert.Equal(t, expected, text) +} From ad8cb70d49773210e33968cb7e227fac2df04f6f Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 7 Nov 2022 14:29:49 +0800 Subject: [PATCH 090/157] feat: get text excerpt --- pkg/htmltext/htmltext.go | 2 +- pkg/htmltext/htmltext_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go index 0694d990..fa93ff2d 100644 --- a/pkg/htmltext/htmltext.go +++ b/pkg/htmltext/htmltext.go @@ -39,6 +39,6 @@ func ClearText(html string) (text string) { func FetchExcerpt(html, trimMarker string, limit int) (text string) { text = ClearText(html) runeText := []rune(text) - text = string(runeText[0:limit]) + text = string(runeText[0:limit]) + trimMarker return } diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go index 353ca20b..71aafbb6 100644 --- a/pkg/htmltext/htmltext_test.go +++ b/pkg/htmltext/htmltext_test.go @@ -35,17 +35,17 @@ func TestFetchExcerpt(t *testing.T) { ) // test english string - expected = "hello" + expected = "hello..." text = FetchExcerpt("

hello world

", "...", 5) assert.Equal(t, expected, text) // test mixed string - expected = "hello你好" + expected = "hello你好..." text = FetchExcerpt("

hello你好world

", "...", 7) assert.Equal(t, expected, text) // test mixed string with emoticon - expected = "hello你好😂" + expected = "hello你好😂..." text = FetchExcerpt("

hello你好😂world

", "...", 8) assert.Equal(t, expected, text) } From c7ee44d33c718bc3ed83c7b7726eb35429857e4c Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 7 Nov 2022 14:32:17 +0800 Subject: [PATCH 091/157] feat: get text excerpt --- pkg/htmltext/htmltext.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go index fa93ff2d..31ae1a22 100644 --- a/pkg/htmltext/htmltext.go +++ b/pkg/htmltext/htmltext.go @@ -8,6 +8,11 @@ import ( // ClearText clear HTML, get the clear text func ClearText(html string) (text string) { + if len(html) == 0 { + text = html + return + } + var ( re *regexp.Regexp codeReg = `(?ism)<(pre)>.*<\/pre>` @@ -37,8 +42,19 @@ func ClearText(html string) (text string) { // FetchExcerpt return the excerpt from the HTML string func FetchExcerpt(html, trimMarker string, limit int) (text string) { + if len(html) == 0 { + text = html + return + } + text = ClearText(html) runeText := []rune(text) - text = string(runeText[0:limit]) + trimMarker + if len(runeText) <= limit { + text = string(runeText) + } else { + text = string(runeText[0:limit]) + } + + text += trimMarker return } From dfaa926e252bc85d79de8485da8002a2f1a6a71e Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 7 Nov 2022 15:49:43 +0800 Subject: [PATCH 092/157] refactor(admin): Display version updates --- ui/src/common/interface.ts | 5 +++- .../components/HealthStatus/index.tsx | 23 +++++++++++++++---- ui/src/pages/Admin/Dashboard/index.tsx | 1 + 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 1e61d874..9ea391c7 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -342,7 +342,10 @@ export interface AdminDashboard { time_zone: string; occupying_storage_space: string; app_start_time: number; - app_version: string; https: boolean; + version_info: { + remote_version: string; + version: string; + }; }; } diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx index 1b1c7ced..f36e02a5 100644 --- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -11,7 +11,8 @@ interface IProps { const HealthStatus: FC = ({ data }) => { const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); - + const { version, remote_version } = data.version_info || {}; + const isLatest = version === remote_version; return ( @@ -19,10 +20,22 @@ const HealthStatus: FC = ({ data }) => {
{t('version')} - 90 - - {t('update_to')} {data.app_version} - + {version} + {isLatest && ( + + {t('latest')} + + )} + {!isLatest && remote_version && ( + + {t('update_to')} {remote_version} + + )} + {!isLatest && !remote_version && ( + + {t('check_failed')} + + )} {t('https')} diff --git a/ui/src/pages/Admin/Dashboard/index.tsx b/ui/src/pages/Admin/Dashboard/index.tsx index 2037016e..c20f250c 100644 --- a/ui/src/pages/Admin/Dashboard/index.tsx +++ b/ui/src/pages/Admin/Dashboard/index.tsx @@ -18,6 +18,7 @@ const Dashboard: FC = () => { if (!data) { return null; } + return ( <>

{t('title')}

From 8b312cde7e62deb8261a3c420afd31ebe1bf4e37 Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 7 Nov 2022 16:05:01 +0800 Subject: [PATCH 093/157] feat: unified processing excerpt --- internal/repo/search_common/search_repo.go | 16 ++++------------ .../service/report_backyard/report_backyard.go | 18 ++++-------------- internal/service/tag/tag_service.go | 17 ++++------------- 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index b2db83c3..22a4cbb5 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -3,6 +3,7 @@ package search_common import ( "context" "fmt" + "github.com/answerdev/answer/pkg/htmltext" "strings" "time" @@ -25,7 +26,7 @@ var ( "`question`.`id`", "`question`.`id` as `question_id`", "`title`", - "`original_text`", + "`parsed_text`", "`question`.`created_at`", "`user_id`", "`vote_count`", @@ -38,7 +39,7 @@ var ( "`answer`.`id` as `id`", "`question_id`", "`question`.`title` as `title`", - "`answer`.`original_text` as `original_text`", + "`answer`.`parsed_text` as `parsed_text`", "`answer`.`created_at`", "`answer`.`user_id` as `user_id`", "`answer`.`vote_count` as `vote_count`", @@ -412,7 +413,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) object = schema.SearchObject{ ID: string(r["id"]), Title: string(r["title"]), - Excerpt: cutOutParsedText(string(r["original_text"])), + Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240), CreatedAtParsed: tp.Unix(), UserInfo: userInfo, Tags: tags, @@ -443,15 +444,6 @@ func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.Us } } -func cutOutParsedText(parsedText string) string { - parsedText = strings.TrimSpace(parsedText) - idx := strings.Index(parsedText, "\n") - if idx >= 0 { - parsedText = parsedText[0:idx] - } - return parsedText -} - func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) { relevanceRes := []string{} args = []interface{}{} diff --git a/internal/service/report_backyard/report_backyard.go b/internal/service/report_backyard/report_backyard.go index 947e7197..69b87c85 100644 --- a/internal/service/report_backyard/report_backyard.go +++ b/internal/service/report_backyard/report_backyard.go @@ -2,9 +2,8 @@ package report_backyard import ( "context" - "strings" - "github.com/answerdev/answer/internal/service/config" + "github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" @@ -180,20 +179,20 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem case "question": r.QuestionID = questionId r.Title = question.Title - r.Excerpt = rs.cutOutTagParsedText(question.OriginalText) + r.Excerpt = htmltext.FetchExcerpt(question.ParsedText, "...", 240) case "answer": r.QuestionID = questionId r.AnswerID = answerId r.Title = question.Title - r.Excerpt = rs.cutOutTagParsedText(answer.OriginalText) + r.Excerpt = htmltext.FetchExcerpt(answer.ParsedText, "...", 240) case "comment": r.QuestionID = questionId r.AnswerID = answerId r.CommentID = commentId r.Title = question.Title - r.Excerpt = rs.cutOutTagParsedText(cmt.OriginalText) + r.Excerpt = htmltext.FetchExcerpt(cmt.ParsedText, "...", 240) } // parse reason @@ -214,12 +213,3 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem } resp = &res } - -func (rs *ReportBackyardService) cutOutTagParsedText(parsedText string) string { - parsedText = strings.TrimSpace(parsedText) - idx := strings.Index(parsedText, "\n") - if idx >= 0 { - parsedText = parsedText[0:idx] - } - return parsedText -} diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index cf6fc02e..6384eb82 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -3,9 +3,8 @@ package tag import ( "context" "encoding/json" - "strings" - "github.com/answerdev/answer/internal/service/revision_common" + "github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" @@ -344,12 +343,13 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith resp := make([]*schema.GetTagPageResp, 0) for _, tag := range tags { + excerpt := htmltext.FetchExcerpt(tag.ParsedText, "...", 240) resp = append(resp, &schema.GetTagPageResp{ TagID: tag.ID, SlugName: tag.SlugName, DisplayName: tag.DisplayName, - OriginalText: cutOutTagParsedText(tag.OriginalText), - ParsedText: cutOutTagParsedText(tag.ParsedText), + OriginalText: excerpt, + ParsedText: excerpt, FollowCount: tag.FollowCount, QuestionCount: tag.QuestionCount, IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID), @@ -371,12 +371,3 @@ func (ts *TagService) checkTagIsFollow(ctx context.Context, userID, tagID string } return followed } - -func cutOutTagParsedText(parsedText string) string { - parsedText = strings.TrimSpace(parsedText) - idx := strings.Index(parsedText, "\n") - if idx >= 0 { - parsedText = parsedText[0:idx] - } - return parsedText -} From 82ac07ae675a79be9ecf7b7b34a36a942c3e23dc Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 7 Nov 2022 16:27:35 +0800 Subject: [PATCH 094/157] refactor(admin): remove jsx --- ui/src/pages/Admin/Dashboard/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ui/src/pages/Admin/Dashboard/index.tsx b/ui/src/pages/Admin/Dashboard/index.tsx index c20f250c..0b6834f9 100644 --- a/ui/src/pages/Admin/Dashboard/index.tsx +++ b/ui/src/pages/Admin/Dashboard/index.tsx @@ -37,12 +37,6 @@ const Dashboard: FC = () => { - {process.env.REACT_APP_VERSION && ( -

- {`${t('version')} `} - {process.env.REACT_APP_VERSION} -

- )} ); }; From aa1d434b28d2f7602c4c3dcc526db19b6a0c0b20 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 16:38:06 +0800 Subject: [PATCH 095/157] doc: update install document, add docker-compose and binary installation --- INSTALL_CN.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/INSTALL_CN.md b/INSTALL_CN.md index 848c9ce1..4a93964a 100644 --- a/INSTALL_CN.md +++ b/INSTALL_CN.md @@ -22,3 +22,44 @@ docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:lat [http://127.0.0.1:9080/](http://127.0.0.1:9080/) 使用刚才创建的管理员用户名密码即可登录。 + +## 使用 docker-compose 安装 +### 步骤 1: 使用 docker-compose 命令启动项目 +```bash +mkdir answer && cd answer +wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml +docker-compose up +``` + +### 步骤 2: 访问安装路径进行项目安装 +[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) + +具体配置与 docker 使用时相同 + +### 步骤 3:安装完成后访问项目路径开始使用 +[http://127.0.0.1:9080/](http://127.0.0.1:9080/) + +## 使用 二进制 安装 +### 步骤 1: 下载二进制文件 +[https://github.com/answerdev/answer/releases](https://github.com/answerdev/answer/releases) +请下载您当下系统所需要的对应版本 + +### 步骤 2: 使用命令行安装 +> 以下命令中 -C 指定的是 answer 所需的数据目录,您可以根据实际需要进行修改 + +```bash +./answer init -C ./answer-data/ +``` + +然后访问:[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) 进行安装,具体配置与使用 docker 安装相同 + +### 步骤 3: 使用命令行启动 +安装完成之后程序会退出,请使用命令正式启动项目 +```bash +./answer run -C ./answer-data/ +``` + +正常启动后可以访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/) 使用安装时指定的管理员用户名密码进行登录 + +## 安装常见问题 +- 使用 docker 重新安装遇到问题?默认我们给出的命令是使用 `answer-data` 命名卷,所以如果重新不需要原来的数据,请主动进行删除 `docker volume rm answer-data` From 530532ab1c84e006571ed977b8f09859bce14c50 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 16:44:48 +0800 Subject: [PATCH 096/157] feat: change the default smtp configuration to empty --- internal/migrations/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 509bf516..cebdc1c7 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -189,7 +189,7 @@ func initConfigTable(engine *xorm.Engine) error { {ID: 30, Key: "answer.vote_up", Value: `0`}, {ID: 31, Key: "answer.vote_up_cancel", Value: `0`}, {ID: 32, Key: "question.follow", Value: `0`}, - {ID: 33, Key: "email.config", Value: `{"from_name":"answer","from_email":"answer@answer.com","smtp_host":"smtp.answer.org","smtp_port":465,"smtp_password":"answer","smtp_username":"answer@answer.com","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`}, + {ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`}, {ID: 35, Key: "tag.follow", Value: `0`}, {ID: 36, Key: "rank.question.add", Value: `0`}, {ID: 37, Key: "rank.question.edit", Value: `0`}, From f43e2e93a125b4bde1ac5fae0c13c7d836903a4b Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 7 Nov 2022 16:45:23 +0800 Subject: [PATCH 097/157] add dashboard smtp verification --- .../service/dashboard/dashboard_service.go | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index e642a85c..fda3d646 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -7,15 +7,18 @@ import ( "net/http" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity_common" answercommon "github.com/answerdev/answer/internal/service/answer_common" "github.com/answerdev/answer/internal/service/comment_common" "github.com/answerdev/answer/internal/service/config" + "github.com/answerdev/answer/internal/service/export" questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/report_common" "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -112,7 +115,13 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.ReportCount = reportCount dashboardInfo.UploadingFiles = true - dashboardInfo.SMTP = true + emailconfig, err := ds.GetEmailConfig() + if err != nil { + return dashboardInfo, err + } + if emailconfig.SMTPHost != "" { + dashboardInfo.SMTP = true + } dashboardInfo.HTTPS = true dashboardInfo.OccupyingStorageSpace = "1MB" dashboardInfo.AppStartTime = "102" @@ -146,3 +155,16 @@ func (ds *DashboardService) RemoteVersion(ctx context.Context) string { } return remoteVersion.Release.Version } + +func (ds *DashboardService) GetEmailConfig() (ec *export.EmailConfig, err error) { + emailConf, err := ds.configRepo.GetString("email.config") + if err != nil { + return nil, err + } + ec = &export.EmailConfig{} + err = json.Unmarshal([]byte(emailConf), ec) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return ec, nil +} From a29eca4a4de902961151c44708de3fb89ed3a66a Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 17:45:36 +0800 Subject: [PATCH 098/157] feat: add timezone validation --- internal/schema/siteinfo_schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 446b986d..70be00ba 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -12,7 +12,7 @@ type SiteInterfaceReq struct { Logo string `validate:"omitempty,gt=0,lte=256" form:"logo" json:"logo"` Theme string `validate:"required,gt=1,lte=128" form:"theme" json:"theme"` Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"` - TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"` + TimeZone string `validate:"required,gt=1,lte=128,timezone" form:"time_zone" json:"time_zone"` } // SiteGeneralResp site general response From d78df49928ebc22b05f118cb9a55e75c78741680 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 7 Nov 2022 17:59:07 +0800 Subject: [PATCH 099/157] fix: install translation error --- ui/src/i18n/locales/en.json | 5 +++-- ui/src/pages/Install/index.tsx | 15 ++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 76c30c3a..29f7ff91 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -824,8 +824,9 @@ "ready_title": "Your Answer is Ready!", "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", "good_luck": "Have fun, and good luck!", - "warning": "Warning", - "warning_description": "The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now.", + "warn_title": "Warning", + "warn_description": "The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first.", + "install_now": "You may try <1>installing now.", "installed": "Already installed", "installed_description": "You appear to have already installed. To reinstall please clear your old database tables first." }, diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index c0737654..b15ed145 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable prettier/prettier */ import { FC, useState, useEffect } from 'react'; import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; import { useTranslation, Trans } from 'react-i18next'; @@ -265,16 +266,12 @@ const Index: FC = () => { {step === 6 && (
-
{t('warning')}
+
{t('warn_title')}

- - The file config.yaml already exists. If you - need to reset any of the configuration items in this - file, please delete it first. You may try{' '} - handleInstallNow(e)}> - installing now - - . + }} /> + {' '} + + You may try handleInstallNow(e)}>installing now.

From d5a57589e6d421308aeef8df8d889a2ce9e658ef Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 18:03:51 +0800 Subject: [PATCH 100/157] feat: add site_url and contact_email validation --- internal/schema/siteinfo_schema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index da78327e..9a0a4839 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -5,8 +5,8 @@ type SiteGeneralReq struct { Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` - SiteUrl string `validate:"required,gt=1,lte=128" form:"site_url" json:"site_url"` - ContactEmail string `validate:"required,gt=1,lte=128" form:"contact_email" json:"contact_email"` + SiteUrl string `validate:"required,gt=1,lte=512,url" form:"site_url" json:"site_url"` + ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` } // SiteInterfaceReq site interface request From ae38b2d1fc8491d43afe304ed6a131ee18dbcc5d Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 7 Nov 2022 18:10:10 +0800 Subject: [PATCH 101/157] fix: install page cannot fetch init config data --- ui/src/router/routes.ts | 2 +- ui/src/utils/guard.ts | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 24016d0a..c6b8d359 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -262,7 +262,7 @@ const routes: RouteNode[] = [ ], }, { - path: 'install', + path: '/install', page: 'pages/Install', }, { diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index c9517d7a..2e123bf7 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -190,6 +190,15 @@ export const initAppSettingsStore = async () => { } }; +export const shouldInitAppFetchData = () => { + const { pathname } = window.location; + if (pathname === '/install') { + return false; + } + + return true; +}; + export const setupApp = async () => { /** * WARN: @@ -197,7 +206,10 @@ export const setupApp = async () => { * 2. must pre init app settings for app render */ // TODO: optimize `initAppSettingsStore` by server render - await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]); - setupAppLanguage(); - setupAppTimeZone(); + + if (shouldInitAppFetchData()) { + await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]); + setupAppLanguage(); + setupAppTimeZone(); + } }; From 79ae9636abf3d39c220de6f3208eaf0d446da1f6 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 10:03:39 +0800 Subject: [PATCH 102/157] fix: install page input add type password --- ui/src/pages/Install/components/FourthStep/index.tsx | 2 ++ ui/src/pages/Install/components/SecondStep/index.tsx | 1 + ui/src/pages/Install/index.tsx | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index fae0c53c..3807ac69 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -171,6 +171,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {t('contact_email.label')} { @@ -215,6 +216,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {t('admin_password.label')} { diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx index 3e67913a..30e48930 100644 --- a/ui/src/pages/Install/components/SecondStep/index.tsx +++ b/ui/src/pages/Install/components/SecondStep/index.tsx @@ -148,6 +148,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {t('db_password.label')} { if (tableExist) { setStep(7); } else { - setStep(4); + setStep(2); } }; From 607573d3860d1385fae8443e92e8cef5c25decd9 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Tue, 8 Nov 2022 11:33:48 +0800 Subject: [PATCH 103/157] fix startTime --- cmd/answer/main.go | 3 +++ internal/schema/dashboard_schema.go | 4 ++++ internal/service/dashboard/dashboard_service.go | 5 ++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/answer/main.go b/cmd/answer/main.go index e9308442..da836a5d 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -3,10 +3,12 @@ package main import ( "os" "path/filepath" + "time" "github.com/answerdev/answer/internal/base/conf" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/cli" + "github.com/answerdev/answer/internal/schema" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman" "github.com/segmentfault/pacman/contrib/conf/viper" @@ -51,6 +53,7 @@ func runApp() { panic(err) } constant.Version = Version + schema.AppStartTime = time.Now() defer cleanup() if err := app.Run(); err != nil { diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index bba29da1..2f5b3622 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -1,5 +1,9 @@ package schema +import "time" + +var AppStartTime time.Time + type DashboardInfo struct { QuestionCount int64 `json:"question_count"` AnswerCount int64 `json:"answer_count"` diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index fda3d646..aa876507 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -3,8 +3,10 @@ package dashboard import ( "context" "encoding/json" + "fmt" "io/ioutil" "net/http" + "time" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" @@ -124,7 +126,8 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI } dashboardInfo.HTTPS = true dashboardInfo.OccupyingStorageSpace = "1MB" - dashboardInfo.AppStartTime = "102" + startTime := time.Now().Unix() - schema.AppStartTime.Unix() + dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) dashboardInfo.TimeZone = siteInfoInterface.TimeZone dashboardInfo.VersionInfo.Version = constant.Version dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx) From 41f83ab1751a223d7d9e6579684d7ad007268d66 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 15:08:21 +0800 Subject: [PATCH 104/157] fix: change avatar size --- ui/src/components/UserCard/index.tsx | 21 +++++++++++++++------ ui/src/i18n/locales/en.json | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/ui/src/components/UserCard/index.tsx b/ui/src/components/UserCard/index.tsx index 16be5b0d..169354ec 100644 --- a/ui/src/components/UserCard/index.tsx +++ b/ui/src/components/UserCard/index.tsx @@ -33,12 +33,21 @@ const Index: FC = ({ data, time, preFix, className = '' }) => { /> ) : ( - + <> + + + + )}
diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 93b6a8ac..e2aaa72f 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -859,7 +859,7 @@ }, "dashboard": { "title": "Dashboard", - "welcome": "Welcome to Answer Admin !", + "welcome": "Welcome to Answer Admin!", "site_statistics": "Site Statistics", "questions": "Questions:", "answers": "Answers:", From 4a123b933d42587966f4338c3db2d950ec00f6f9 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 17:11:19 +0800 Subject: [PATCH 105/157] fix: remove redundant code --- ui/src/components/FollowingTags/index.tsx | 4 ++-- ui/src/pages/Users/AccountForgot/index.tsx | 7 +------ ui/src/pages/Users/Login/index.tsx | 2 -- ui/src/pages/Users/PasswordReset/index.tsx | 6 +----- ui/src/pages/Users/Register/index.tsx | 7 +------ ui/src/services/client/notification.ts | 4 ++-- ui/src/services/client/tag.ts | 4 ++-- ui/src/utils/common.ts | 1 + ui/src/utils/guard.ts | 16 +++++++++++++--- 9 files changed, 23 insertions(+), 28 deletions(-) diff --git a/ui/src/components/FollowingTags/index.tsx b/ui/src/components/FollowingTags/index.tsx index 06fec6ec..9e3156b0 100644 --- a/ui/src/components/FollowingTags/index.tsx +++ b/ui/src/components/FollowingTags/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; import { TagSelector, Tag } from '@/components'; -import { tryNormalLogged } from '@/utils/guard'; +import { tryLoggedAndActicevated } from '@/utils/guard'; import { useFollowingTags, followTags } from '@/services'; const Index: FC = () => { @@ -32,7 +32,7 @@ const Index: FC = () => { }); }; - if (!tryNormalLogged()) { + if (!tryLoggedAndActicevated().ok) { return null; } diff --git a/ui/src/pages/Users/AccountForgot/index.tsx b/ui/src/pages/Users/AccountForgot/index.tsx index e7a77ea5..7af609cc 100644 --- a/ui/src/pages/Users/AccountForgot/index.tsx +++ b/ui/src/pages/Users/AccountForgot/index.tsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Container, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { tryNormalLogged } from '@/utils/guard'; import { PageTitle } from '@/components'; import SendEmail from './components/sendEmail'; @@ -17,10 +16,6 @@ const Index: React.FC = () => { setEmail(mail); }; - useEffect(() => { - tryNormalLogged(); - }, []); - return ( <> diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index b8794ff4..7a499e64 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -158,8 +158,6 @@ const Index: React.FC = () => { if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { setStep(2); - } else { - guard.tryNormalLogged(); } }, []); diff --git a/ui/src/pages/Users/PasswordReset/index.tsx b/ui/src/pages/Users/PasswordReset/index.tsx index b97bd707..ab00de30 100644 --- a/ui/src/pages/Users/PasswordReset/index.tsx +++ b/ui/src/pages/Users/PasswordReset/index.tsx @@ -1,4 +1,4 @@ -import React, { FormEvent, useState, useEffect } from 'react'; +import React, { FormEvent, useState } from 'react'; import { Container, Col, Form, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -7,7 +7,6 @@ import { loggedUserInfoStore } from '@/stores'; import { getQueryString } from '@/utils'; import type { FormDataType } from '@/common/interface'; import { replacementPassword } from '@/services'; -import { tryNormalLogged } from '@/utils/guard'; import { PageTitle } from '@/components'; const Index: React.FC = () => { @@ -115,9 +114,6 @@ const Index: React.FC = () => { }); }; - useEffect(() => { - tryNormalLogged(); - }, []); return ( <> diff --git a/ui/src/pages/Users/Register/index.tsx b/ui/src/pages/Users/Register/index.tsx index e4d94b2d..811de77d 100644 --- a/ui/src/pages/Users/Register/index.tsx +++ b/ui/src/pages/Users/Register/index.tsx @@ -1,9 +1,8 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Container } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { PageTitle, Unactivate } from '@/components'; -import { tryNormalLogged } from '@/utils/guard'; import SignUpForm from './components/SignUpForm'; @@ -15,10 +14,6 @@ const Index: React.FC = () => { setShowForm((bol) => !bol); }; - useEffect(() => { - tryNormalLogged(); - }, []); - return (

{t('page_title')}

diff --git a/ui/src/services/client/notification.ts b/ui/src/services/client/notification.ts index a849db0a..6d34022b 100644 --- a/ui/src/services/client/notification.ts +++ b/ui/src/services/client/notification.ts @@ -3,7 +3,7 @@ import qs from 'qs'; import request from '@/utils/request'; import type * as Type from '@/common/interface'; -import { tryNormalLogged } from '@/utils/guard'; +import { tryLoggedAndActicevated } from '@/utils/guard'; export const useQueryNotifications = (params) => { const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, { @@ -33,7 +33,7 @@ export const useQueryNotificationStatus = () => { const apiUrl = '/answer/api/v1/notification/status'; return useSWR<{ inbox: number; achievement: number }>( - tryNormalLogged() ? apiUrl : null, + tryLoggedAndActicevated().ok ? apiUrl : null, request.instance.get, { refreshInterval: 3000, diff --git a/ui/src/services/client/tag.ts b/ui/src/services/client/tag.ts index 42b9f1ac..b1ec3a9a 100644 --- a/ui/src/services/client/tag.ts +++ b/ui/src/services/client/tag.ts @@ -2,7 +2,7 @@ import useSWR from 'swr'; import request from '@/utils/request'; import type * as Type from '@/common/interface'; -import { tryNormalLogged } from '@/utils/guard'; +import { tryLoggedAndActicevated } from '@/utils/guard'; export const deleteTag = (id) => { return request.delete('/answer/api/v1/tag', { @@ -24,7 +24,7 @@ export const saveSynonymsTags = (params) => { export const useFollowingTags = () => { let apiUrl = ''; - if (tryNormalLogged()) { + if (tryLoggedAndActicevated().ok) { apiUrl = '/answer/api/v1/tags/following'; } const { data, error, mutate } = useSWR(apiUrl, request.instance.get); diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index dcc2d084..31bb0a40 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -85,6 +85,7 @@ function formatUptime(value) { return `< 1 ${t('dates.hour')}`; } + export { getQueryString, thousandthDivision, diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index 2e123bf7..7f926fba 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -154,9 +154,9 @@ export const admin = () => { /** * try user was logged and all state ok - * @param autoLogin + * @param canNavigate // if true, will navigate to login page if not logged */ -export const tryNormalLogged = (autoLogin: boolean = false) => { +export const tryNormalLogged = (canNavigate: boolean = false) => { const us = deriveLoginState(); if (us.isNormal) { @@ -164,7 +164,7 @@ export const tryNormalLogged = (autoLogin: boolean = false) => { } // must assert logged state first and return if (!us.isLogged) { - if (autoLogin) { + if (canNavigate) { floppyNavigation.navigateToLogin(); } return false; @@ -182,6 +182,16 @@ export const tryNormalLogged = (autoLogin: boolean = false) => { return false; }; +export const tryLoggedAndActicevated = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + console.log('tryLogged', us); + if (!us.isLogged || !us.isActivated) { + gr.ok = false; + } + return gr; +}; + export const initAppSettingsStore = async () => { const appSettings = await getAppSettings(); if (appSettings) { From 7abbff7278924cd34e179ad71248dbf0bbf9f6b2 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 17:16:50 +0800 Subject: [PATCH 106/157] fix: change email page need login --- ui/src/router/routes.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index c6b8d359..c745ba1d 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -154,7 +154,9 @@ const routes: RouteNode[] = [ { path: 'users/change-email', page: 'pages/Users/ChangeEmail', - // TODO: guard this (change email when user not activated) ? + guard: async () => { + return guard.notLogged(); + }, }, { path: 'users/password-reset', From 64ef0140305284855a8998b8c6c639dfa71980c0 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 8 Nov 2022 17:42:48 +0800 Subject: [PATCH 107/157] feat(i18n): language resource migrate done! --- ui/.eslintrc.js | 4 +- ui/config-overrides.js | 15 + ui/package.json | 5 +- ui/pnpm-lock.yaml | 17 + ui/scripts/i18n-locale-tool.js | 59 + ui/src/common/constants.ts | 1 + ui/src/i18n/DO_NOT_EDIT_dir_locales | 0 ui/src/i18n/init.ts | 9 +- ui/src/i18n/locales/en.json | 1094 ---------------- ui/src/i18n/locales/en_US.yaml | 1119 +++++++++++++++++ ui/src/i18n/locales/i18n.yaml | 6 + ui/src/i18n/locales/it_IT.yaml | 170 +++ ui/src/i18n/locales/zh_CN.json | 914 -------------- ui/src/i18n/locales/zh_CN.yaml | 919 ++++++++++++++ ui/src/pages/Admin/Interface/index.tsx | 5 +- .../pages/Users/Settings/Interface/index.tsx | 4 +- ui/src/react-app-env.d.ts | 1 + ui/src/services/client/settings.ts | 4 +- ui/src/utils/common.ts | 1 + ui/src/utils/localize.ts | 75 +- ui/tsconfig.json | 2 +- 21 files changed, 2392 insertions(+), 2032 deletions(-) create mode 100644 ui/scripts/i18n-locale-tool.js create mode 100644 ui/src/i18n/DO_NOT_EDIT_dir_locales delete mode 100644 ui/src/i18n/locales/en.json create mode 100644 ui/src/i18n/locales/en_US.yaml create mode 100644 ui/src/i18n/locales/i18n.yaml create mode 100644 ui/src/i18n/locales/it_IT.yaml delete mode 100644 ui/src/i18n/locales/zh_CN.json create mode 100644 ui/src/i18n/locales/zh_CN.yaml diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 98edc408..6ecb4b49 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + root: true, env: { browser: true, es2021: true, @@ -19,7 +20,8 @@ module.exports = { }, ecmaVersion: 'latest', sourceType: 'module', - project: './tsconfig.json', + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], }, plugins: ['react', '@typescript-eslint'], rules: { diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 35510f12..e8d2dceb 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -1,10 +1,23 @@ const path = require('path'); +const i18nLocaleTool = require('./scripts/i18n-locale-tool'); module.exports = { webpack: function (config, env) { if (env === 'production') { config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH; + i18nLocaleTool.resolvePresetLocales(); } + + for (let _rule of config.module.rules) { + if (_rule.oneOf) { + _rule.oneOf.unshift({ + test: /\.ya?ml$/, + use: 'yaml-loader' + }); + break; + } + } + config.resolve.alias = { ...config.resolve.alias, '@': path.resolve(__dirname, 'src'), @@ -14,6 +27,8 @@ module.exports = { }, devServer: function (configFunction) { + i18nLocaleTool.autoSync(); + return function (proxy, allowedHost) { const config = configFunction(proxy, allowedHost); config.proxy = { diff --git a/ui/package.json b/ui/package.json index 069456ef..fd2a3470 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,7 +10,6 @@ "build:prod": "env-cmd -f .env.production react-app-rewired build", "build": "env-cmd -f .env react-app-rewired build", "test": "react-app-rewired test", - "eject": "react-scripts eject", "lint": "eslint . --cache --fix --ext .ts,.tsx", "prepare": "cd .. && husky install", "cz": "cz", @@ -74,6 +73,7 @@ "@types/react-helmet": "^6.1.5", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.33.0", + "chokidar": "^3.5.3", "commitizen": "^4.2.5", "conventional-changelog-cli": "^2.2.2", "customize-cra": "^1.0.0", @@ -101,7 +101,8 @@ "sass": "^1.54.4", "tsconfig-paths-webpack-plugin": "^4.0.0", "typescript": "*", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "yaml-loader": "^0.8.0" }, "packageManager": "pnpm@7.9.5", "engines": { diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 505ff738..a4408b96 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -26,6 +26,7 @@ specifiers: axios: ^0.27.2 bootstrap: ^5.2.0 bootstrap-icons: ^1.9.1 + chokidar: ^3.5.3 classnames: ^2.3.1 codemirror: 5.65.0 commitizen: ^4.2.5 @@ -77,6 +78,7 @@ specifiers: tsconfig-paths-webpack-plugin: ^4.0.0 typescript: '*' web-vitals: ^2.1.4 + yaml-loader: ^0.8.0 zustand: ^4.1.1 dependencies: @@ -131,6 +133,7 @@ devDependencies: '@types/react-helmet': 6.1.5 '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha + chokidar: 3.5.3 commitizen: 4.2.5 conventional-changelog-cli: 2.2.2 customize-cra: 1.0.0 @@ -159,6 +162,7 @@ devDependencies: tsconfig-paths-webpack-plugin: 4.0.0 typescript: 4.8.3 web-vitals: 2.1.4 + yaml-loader: 0.8.0 packages: @@ -7040,6 +7044,10 @@ packages: filelist: 1.0.4 minimatch: 3.1.2 + /javascript-stringify/2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + dev: true + /jest-changed-files/27.5.1: resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11682,6 +11690,15 @@ packages: /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yaml-loader/0.8.0: + resolution: {integrity: sha512-LjeKnTzVBKWiQBeE2L9ssl6WprqaUIxCSNs5tle8PaDydgu3wVFXTbMfsvF2MSErpy9TDVa092n4q6adYwJaWg==} + engines: {node: '>= 12.13'} + dependencies: + javascript-stringify: 2.1.0 + loader-utils: 2.0.2 + yaml: 2.1.1 + dev: true + /yaml/1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} diff --git a/ui/scripts/i18n-locale-tool.js b/ui/scripts/i18n-locale-tool.js new file mode 100644 index 00000000..456b117b --- /dev/null +++ b/ui/scripts/i18n-locale-tool.js @@ -0,0 +1,59 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const path = require('node:path'); +const fs = require('node:fs'); + +const chokidar = require('chokidar'); + +const SRC_PATH = path.resolve(__dirname, '../../i18n'); +const DEST_PATH = path.resolve(__dirname, '../src/i18n/locales'); +const PRESET_LANG = ['en_US', 'zh_CN']; + +const cleanLocales = () => { + fs.readdirSync(DEST_PATH).forEach((fp) => { + fs.rmSync(path.resolve(DEST_PATH, fp), { force: true, recursive: true }); + }); +}; + +const copyLocaleFile = (filePath) => { + const targetFilePath = path.resolve(DEST_PATH, path.basename(filePath)); + fs.copyFile( + filePath, + targetFilePath, + fs.constants.COPYFILE_FICLONE, + (err) => { + if (err) { + throw err; + } + }, + ); +}; + +const watchAndSync = () => { + chokidar + .watch(path.resolve(SRC_PATH, '*.yaml'), { + awaitWriteFinish: true, + }) + .on('all', (evt, filePath) => { + copyLocaleFile(filePath); + }); +}; + +const autoSync = () => { + cleanLocales(); + watchAndSync(); +}; + +const resolvePresetLocales = () => { + PRESET_LANG.forEach((lng) => { + const sp = path.resolve(SRC_PATH, `${lng}.yaml`); + const tp = path.resolve(DEST_PATH, `${lng}.yaml`); + if (fs.existsSync(tp) === false) { + copyLocaleFile(sp); + } + }); +}; + +module.exports = { + autoSync, + resolvePresetLocales, +}; diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 028e56e8..a4db79ad 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -1,5 +1,6 @@ export const DEFAULT_LANG = 'en_US'; export const CURRENT_LANG_STORAGE_KEY = '_a_lang_'; +export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_'; export const LOGGED_USER_STORAGE_KEY = '_a_lui_'; export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; diff --git a/ui/src/i18n/DO_NOT_EDIT_dir_locales b/ui/src/i18n/DO_NOT_EDIT_dir_locales new file mode 100644 index 00000000..e69de29b diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index d60413bb..6d75ddc0 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -4,9 +4,8 @@ import i18next from 'i18next'; import Backend from 'i18next-http-backend'; import { DEFAULT_LANG } from '@/common/constants'; - -import en from './locales/en.json'; -import zh from './locales/zh_CN.json'; +import en_US from '@/i18n/locales/en_US.yaml'; +import zh_CN from '@/i18n/locales/zh_CN.yaml'; i18next // load translation using http @@ -16,10 +15,10 @@ i18next .init({ resources: { en_US: { - translation: en, + translation: en_US.ui, }, zh_CN: { - translation: zh, + translation: zh_CN.ui, }, }, // debug: process.env.NODE_ENV === 'development', diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json deleted file mode 100644 index 4f0de90b..00000000 --- a/ui/src/i18n/locales/en.json +++ /dev/null @@ -1,1094 +0,0 @@ -{ - "how_to_format": { - "title": "How to Format", - "description": "
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
" - }, - "pagination": { - "prev": "Prev", - "next": "Next" - }, - "page_title": { - "question": "Question", - "questions": "Questions", - "tag": "Tag", - "tags": "Tags", - "tag_wiki": "tag wiki", - "edit_tag": "Edit Tag", - "ask_a_question": "Add Question", - "edit_question": "Edit Question", - "edit_answer": "Edit Answer", - "search": "Search", - "posts_containing": "Posts containing", - "settings": "Settings", - "notifications": "Notifications", - "login": "Log In", - "sign_up": "Sign Up", - "account_recovery": "Account Recovery", - "account_activation": "Account Activation", - "confirm_email": "Confirm Email", - "account_suspended": "Account Suspended", - "admin": "Admin", - "change_email": "Modify Email", - "install": "Answer Installation", - "upgrade": "Answer Upgrade", - "maintenance": "Webite Maintenance" - }, - "notifications": { - "title": "Notifications", - "inbox": "Inbox", - "achievement": "Achievements", - "all_read": "Mark all as read", - "show_more": "Show more" - }, - "suspended": { - "title": "Your Account has been Suspended", - "until_time": "Your account was suspended until {{ time }}.", - "forever": "This user was suspended forever.", - "end": "You don't meet a community guideline." - }, - "editor": { - "blockquote": { - "text": "Blockquote" - }, - "bold": { - "text": "Strong" - }, - "chart": { - "text": "Chart", - "flow_chart": "Flow chart", - "sequence_diagram": "Sequence diagram", - "class_diagram": "Class diagram", - "state_diagram": "State diagram", - "entity_relationship_diagram": "Entity relationship diagram", - "user_defined_diagram": "User defined diagram", - "gantt_chart": "Gantt chart", - "pie_chart": "Pie chart" - }, - "code": { - "text": "Code Sample", - "add_code": "Add code sample", - "form": { - "fields": { - "code": { - "label": "Code", - "msg": { - "empty": "Code cannot be empty." - } - }, - "language": { - "label": "Language (optional)", - "placeholder": "Automatic detection" - } - } - }, - "btn_cancel": "Cancel", - "btn_confirm": "Add" - }, - "formula": { - "text": "Formula", - "options": { - "inline": "Inline formula", - "block": "Block formula" - } - }, - "heading": { - "text": "Heading", - "options": { - "h1": "Heading 1", - "h2": "Heading 2", - "h3": "Heading 3", - "h4": "Heading 4", - "h5": "Heading 5", - "h6": "Heading 6" - } - }, - "help": { - "text": "Help" - }, - "hr": { - "text": "Horizontal Rule" - }, - "image": { - "text": "Image", - "add_image": "Add image", - "tab_image": "Upload image", - "form_image": { - "fields": { - "file": { - "label": "Image File", - "btn": "Select image", - "msg": { - "empty": "File cannot be empty.", - "only_image": "Only image files are allowed.", - "max_size": "File size cannot exceed 4MB." - } - }, - "description": { - "label": "Description (optional)" - } - } - }, - "tab_url": "Image URL", - "form_url": { - "fields": { - "url": { - "label": "Image URL", - "msg": { - "empty": "Image URL cannot be empty." - } - }, - "name": { - "label": "Description (optional)" - } - } - }, - "btn_cancel": "Cancel", - "btn_confirm": "Add", - "uploading": "Uploading" - }, - "indent": { - "text": "Indent" - }, - "outdent": { - "text": "Outdent" - }, - "italic": { - "text": "Emphasis" - }, - "link": { - "text": "Hyperlink", - "add_link": "Add hyperlink", - "form": { - "fields": { - "url": { - "label": "URL", - "msg": { - "empty": "URL cannot be empty." - } - }, - "name": { - "label": "Description (optional)" - } - } - }, - "btn_cancel": "Cancel", - "btn_confirm": "Add" - }, - "ordered_list": { - "text": "Numbered List" - }, - "unordered_list": { - "text": "Bulleted List" - }, - "table": { - "text": "Table", - "heading": "Heading", - "cell": "Cell" - } - }, - "close_modal": { - "title": "I am closing this post as...", - "btn_cancel": "Cancel", - "btn_submit": "Submit", - "remark": { - "empty": "Cannot be empty." - }, - "msg": { - "empty": "Please select a reason." - } - }, - "report_modal": { - "flag_title": "I am flagging to report this post as...", - "close_title": "I am closing this post as...", - "review_question_title": "Review question", - "review_answer_title": "Review answer", - "review_comment_title": "Review comment", - "btn_cancel": "Cancel", - "btn_submit": "Submit", - "remark": { - "empty": "Cannot be empty." - }, - "msg": { - "empty": "Please select a reason." - } - }, - "tag_modal": { - "title": "Create new tag", - "form": { - "fields": { - "display_name": { - "label": "Display Name", - "msg": { - "empty": "Display name cannot be empty.", - "range": "Display name up to 35 characters." - } - }, - "slug_name": { - "label": "URL Slug", - "description": "Must use the character set \"a-z\", \"0-9\", \"+ # - .\"", - "msg": { - "empty": "URL slug cannot be empty.", - "range": "URL slug up to 35 characters.", - "character": "URL slug contains unallowed character set." - } - }, - "description": { - "label": "Description (optional)" - } - } - }, - "btn_cancel": "Cancel", - "btn_submit": "Submit" - }, - "tag_info": { - "created_at": "Created", - "edited_at": "Edited", - "synonyms": { - "title": "Synonyms", - "text": "The following tags will be remapped to", - "empty": "No synonyms found.", - "btn_add": "Add a synonym", - "btn_edit": "Edit", - "btn_save": "Save" - }, - "synonyms_text": "The following tags will be remapped to", - "delete": { - "title": "Delete this tag", - "content": "

We do not allowed deleting tag with posts.

Please remove this tag from the posts first.

", - "content2": "Are you sure you wish to delete?", - "close": "Close" - } - }, - "edit_tag": { - "title": "Edit Tag", - "default_reason": "Edit tag", - "form": { - "fields": { - "revision": { - "label": "Revision" - }, - "display_name": { - "label": "Display Name" - }, - "slug_name": { - "label": "URL Slug", - "info": "Must use the character set \"a-z\", \"0-9\", \"+ # - .\"" - }, - "description": { - "label": "Description" - }, - "edit_summary": { - "label": "Edit Summary", - "placeholder": "Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)" - } - } - }, - "btn_save_edits": "Save edits", - "btn_cancel": "Cancel" - }, - "dates": { - "long_date": "MMM D", - "long_date_with_year": "MMM D, YYYY", - "long_date_with_time": "MMM D, YYYY [at] HH:mm", - "now": "now", - "x_seconds_ago": "{{count}}s ago", - "x_minutes_ago": "{{count}}m ago", - "x_hours_ago": "{{count}}h ago", - "hour": "hour", - "day": "day" - }, - "comment": { - "btn_add_comment": "Add comment", - "reply_to": "Reply to", - "btn_reply": "Reply", - "btn_edit": "Edit", - "btn_delete": "Delete", - "btn_flag": "Flag", - "btn_save_edits": "Save edits", - "btn_cancel": "Cancel", - "show_more": "Show more comment", - "tip_question": "Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.", - "tip_answer": "Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting." - }, - "edit_answer": { - "title": "Edit Answer", - "default_reason": "Edit answer", - "form": { - "fields": { - "revision": { - "label": "Revision" - }, - "answer": { - "label": "Answer" - }, - "edit_summary": { - "label": "Edit Summary", - "placeholder": "Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)" - } - } - }, - "btn_save_edits": "Save edits", - "btn_cancel": "Cancel" - }, - "tags": { - "title": "Tags", - "sort_buttons": { - "popular": "Popular", - "name": "Name", - "newest": "newest" - }, - "button_follow": "Follow", - "button_following": "Following", - "tag_label": "questions", - "search_placeholder": "Filter by tag name", - "no_description": "The tag has no description.", - "more": "More" - }, - "ask": { - "title": "Add Question", - "edit_title": "Edit Question", - "default_reason": "Edit question", - "similar_questions": "Similar questions", - "form": { - "fields": { - "revision": { - "label": "Revision" - }, - "title": { - "label": "Title", - "placeholder": "Be specific and imagine you're asking a question to another person", - "msg": { - "empty": "Title cannot be empty.", - "range": "Title up to 150 characters" - } - }, - "body": { - "label": "Body", - "msg": { - "empty": "Body cannot be empty." - } - }, - "tags": { - "label": "Tags", - "msg": { - "empty": "Tags cannot be empty." - } - }, - "answer": { - "label": "Answer", - "msg": { - "empty": "Answer cannot be empty." - } - } - } - }, - "btn_post_question": "Post your question", - "btn_save_edits": "Save edits", - "answer_question": "Answer your own question", - "post_question&answer": "Post your question and answer" - }, - "tag_selector": { - "add_btn": "Add tag", - "create_btn": "Create new tag", - "search_tag": "Search tag", - "hint": "Describe what your question is about, at least one tag is required.", - "no_result": "No tags matched" - }, - "header": { - "nav": { - "question": "Questions", - "tag": "Tags", - "user": "Users", - "profile": "Profile", - "setting": "Settings", - "logout": "Log out", - "admin": "Admin" - }, - "search": { - "placeholder": "Search" - } - }, - "footer": { - "build_on": "Built on <1> Answer - the open-source software that power Q&A communities
Made with love © 2022 Answer" - }, - "upload_img": { - "name": "Change", - "loading": "loading..." - }, - "pic_auth_code": { - "title": "Captcha", - "placeholder": "Type the text above", - "msg": { - "empty": "Captcha cannot be empty." - } - }, - "inactive": { - "first": "You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account.", - "info": "If it doesn't arrive, check your spam folder.", - "another": "We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder.", - "btn_name": "Resend activation email", - "change_btn_name": "Change email", - "msg": { - "empty": "Cannot be empty." - } - }, - "login": { - "page_title": "Welcome to Answer", - "info_sign": "Don't have an account? <1>Sign up", - "info_login": "Already have an account? <1>Log in", - "forgot_pass": "Forgot password?", - "name": { - "label": "Name", - "msg": { - "empty": "Name cannot be empty.", - "range": "Name up to 30 characters." - } - }, - "email": { - "label": "Email", - "msg": { - "empty": "Email cannot be empty." - } - }, - "password": { - "label": "Password", - "msg": { - "empty": "Password cannot be empty.", - "different": "The passwords entered on both sides are inconsistent" - } - } - }, - "account_forgot": { - "page_title": "Forgot Your Password", - "btn_name": "Send me recovery email", - "send_success": "If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly.", - "email": { - "label": "Email", - "msg": { - "empty": "Email cannot be empty." - } - } - }, - "change_email": { - "page_title": "Welcome to Answer", - "btn_cancel": "Cancel", - "btn_update": "Update email address", - "send_success": "If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly.", - "email": { - "label": "New Email", - "msg": { - "empty": "Email cannot be empty." - } - } - }, - "password_reset": { - "page_title": "Password Reset", - "btn_name": "Reset my password", - "reset_success": "You successfully changed your password; you will be redirected to the log in page.", - "link_invalid": "Sorry, this password reset link is no longer valid. Perhaps your password is already reset?", - "to_login": "Continue to log in page", - "password": { - "label": "Password", - "msg": { - "empty": "Password cannot be empty.", - "length": "The length needs to be between 8 and 32", - "different": "The passwords entered on both sides are inconsistent" - } - }, - "password_confirm": { - "label": "Confirm New Password" - } - }, - "settings": { - "page_title": "Settings", - "nav": { - "profile": "Profile", - "notification": "Notifications", - "account": "Account", - "interface": "Interface" - }, - "profile": { - "btn_name": "Update profile", - "display_name": { - "label": "Display Name", - "msg": "Display name cannot be empty.", - "msg_range": "Display name up to 30 characters" - }, - "username": { - "label": "Username", - "caption": "People can mention you as \"@username\".", - "msg": "Username cannot be empty.", - "msg_range": "Username up to 30 characters", - "character": "Must use the character set \"a-z\", \"0-9\", \" - . _\"" - }, - "avatar": { - "label": "Profile Image", - "gravatar": "Gravatar", - "gravatar_text": "You can change image on <1>gravatar.com", - "custom": "Custom", - "btn_refresh": "Refresh", - "custom_text": "You can upload your image.", - "default": "Default", - "msg": "Please upload an avatar" - }, - "bio": { - "label": "About Me (optional)" - }, - "website": { - "label": "Website (optional)", - "placeholder": "https://example.com", - "msg": "Website incorrect format" - }, - "location": { - "label": "Location (optional)", - "placeholder": "City, Country" - } - }, - "notification": { - "email": { - "label": "Email Notifications", - "radio": "Answers to your questions, comments, and more" - } - }, - "account": { - "change_email_btn": "Change email", - "change_pass_btn": "Change password", - "change_email_info": "We've sent an email to that address. Please follow the confirmation instructions.", - "email": { - "label": "Email", - "msg": "Email cannot be empty." - }, - "password_title": "Password", - "current_pass": { - "label": "Current Password", - "msg": { - "empty": "Current Password cannot be empty.", - "length": "The length needs to be between 8 and 32.", - "different": "The two entered passwords do not match." - } - }, - "new_pass": { - "label": "New Password" - }, - "pass_confirm": { - "label": "Confirm New Password" - } - }, - "interface": { - "lang": { - "label": "Interface Language", - "text": "User interface language. It will change when you refresh the page." - } - } - }, - "toast": { - "update": "update success", - "update_password": "Password changed successfully.", - "flag_success": "Thanks for flagging." - }, - "related_question": { - "title": "Related Questions", - "btn": "Add question", - "answers": "answers" - }, - "question_detail": { - "Asked": "Asked", - "asked": "asked", - "update": "Modified", - "edit": "edited", - "Views": "Viewed", - "Follow": "Follow", - "Following": "Following", - "answered": "answered", - "closed_in": "Closed in", - "show_exist": "Show existing question.", - "answers": { - "title": "Answers", - "score": "Score", - "newest": "Newest", - "btn_accept": "Accept", - "btn_accepted": "Accepted" - }, - "write_answer": { - "title": "Your Answer", - "btn_name": "Post your answer", - "confirm_title": "Continue to answer", - "continue": "Continue", - "confirm_info": "

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

", - "empty": "Answer cannot be empty." - } - }, - "delete": { - "title": "Delete this post", - "question": "We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete?", - "answer_accepted": "

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?", - "other": "Are you sure you wish to delete?", - "tip_question_deleted": "This post has been deleted", - "tip_answer_deleted": "This answer has been deleted" - }, - "btns": { - "confirm": "Confirm", - "cancel": "Cancel", - "save": "Save", - "delete": "Delete", - "login": "Log in", - "signup": "Sign up", - "logout": "Log out", - "verify": "Verify", - "add_question": "Add question" - }, - "search": { - "title": "Search Results", - "keywords": "Keywords", - "options": "Options", - "follow": "Follow", - "following": "Following", - "counts": "{{count}} Results", - "more": "More", - "sort_btns": { - "relevance": "Relevance", - "newest": "Newest", - "active": "Active", - "score": "Score" - }, - "tips": { - "title": "Advanced Search Tips", - "tag": "<1>[tag] search withing a tag", - "user": "<1>user:username search by author", - "answer": "<1>answers:0 unanswered questions", - "score": "<1>score:3 posts with a 3+ score", - "question": "<1>is:question search questions", - "is_answer": "<1>is:answer search answers" - }, - "empty": "We couldn't find anything.
Try different or less specific keywords." - }, - "share": { - "name": "Share", - "copy": "Copy link", - "via": "Share post via...", - "copied": "Copied", - "facebook": "Share to Facebook", - "twitter": "Share to Twitter" - }, - "cannot_vote_for_self": "You can't vote for your own post", - "modal_confirm": { - "title": "Error..." - }, - "account_result": { - "page_title": "Welcome to Answer", - "success": "Your new account is confirmed; you will be redirected to the home page.", - "link": "Continue to homepage", - "invalid": "Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?", - "confirm_new_email": "Your email has been updated.", - "confirm_new_email_invalid": "Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?" - }, - "question": { - "following_tags": "Following Tags", - "edit": "Edit", - "save": "Save", - "follow_tag_tip": "Follow tags to curate your list of questions.", - "hot_questions": "Hot Questions", - "all_questions": "All Questions", - "x_questions": "{{ count }} Questions", - "x_answers": "{{ count }} answers", - "questions": "Questions", - "answers": "Answers", - "newest": "Newest", - "active": "Active", - "frequent": "Frequent", - "score": "Score", - "unanswered": "Unanswered", - "modified": "modified", - "answered": "answered", - "asked": "asked", - "closed": "closed", - "follow_a_tag": "Follow a tag", - "more": "More" - }, - "personal": { - "overview": "Overview", - "answers": "Answers", - "answer": "answer", - "questions": "Questions", - "question": "question", - "bookmarks": "Bookmarks", - "reputation": "Reputation", - "comments": "Comments", - "votes": "Votes", - "newest": "Newest", - "score": "Score", - "edit_profile": "Edit Profile", - "visited_x_days": "Visited {{ count }} days", - "viewed": "Viewed", - "joined": "Joined", - "last_login": "Seen", - "about_me": "About Me", - "about_me_empty": "// Hello, World !", - "top_answers": "Top Answers", - "top_questions": "Top Questions", - "stats": "Stats", - "list_empty": "No posts found.
Perhaps you'd like to select a different tab?", - "accepted": "Accepted", - "answered": "answered", - "asked": "asked", - "upvote": "upvote", - "downvote": "downvote", - "mod_short": "Mod", - "mod_long": "Moderators", - "x_reputation": "reputation", - "x_votes": "votes received", - "x_answers": "answers", - "x_questions": "questions" - }, - "install": { - "title": "Answer", - "next": "Next", - "done": "Done", - "config_yaml_error": "Can’t create the config.yaml file.", - "lang": { - "label": "Please choose a language" - }, - "db_type": { - "label": "Database Engine" - }, - "db_username": { - "label": "Username", - "placeholder": "root", - "msg": "Username cannot be empty." - }, - "db_password": { - "label": "Password", - "placeholder": "root", - "msg": "Password cannot be empty." - }, - "db_host": { - "label": "Database Host", - "placeholder": "db:3306", - "msg": "Database Host cannot be empty." - }, - "db_name": { - "label": "Database Name", - "placeholder": "answer", - "msg": "Database Name cannot be empty." - }, - "db_file": { - "label": "Database File", - "placeholder": "/data/answer.db", - "msg": "Database File cannot be empty." - }, - "config_yaml": { - "title": "Create config.yaml", - "label": "The config.yaml file created.", - "description": "You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it.", - "info": "After you’ve done that, click “Next” button." - }, - "site_information": "Site Information", - "admin_account": "Admin Account", - "site_name": { - "label": "Site Name", - "msg": "Site Name cannot be empty." - }, - "site_url": { - "label": "Site URL", - "text": "The address of your site.", - "msg": { - "empty": "Site URL cannot be empty.", - "incorrect": "Site URL incorrect format." - } - }, - "contact_email": { - "label": "Contact Email", - "text": "Email address of key contact responsible for this site.", - "msg": { - "empty": "Contact Email cannot be empty.", - "incorrect": "Contact Email incorrect format." - } - - }, - "admin_name": { - "label": "Name", - "msg": "Name cannot be empty." - }, - "admin_password": { - "label": "Password", - "text": "You will need this password to log in. Please store it in a secure location.", - "msg": "Password cannot be empty." - }, - "admin_email": { - "label": "Email", - "text": "You will need this email to log in.", - "msg": { - "empty": "Email cannot be empty.", - "incorrect": "Email incorrect format." - } - }, - "ready_title": "Your Answer is Ready!", - "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", - "good_luck": "Have fun, and good luck!", - "warning": "Warning", - "warning_description": "The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now.", - "installed": "Already installed", - "installed_description": "You appear to have already installed. To reinstall please clear your old database tables first." - }, - "upgrade": { - "title": "Answer", - "update_btn": "Update data", - "update_title": "Data update required", - "update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.<1>The update process may take a little while, so please be patient.", - "done_title": "No update required", - "done_btn": "Done", - "done_desscription": "Your Answer data is already up-to-date." - }, - "page_404": { - "description": "Unfortunately, this page doesn't exist.", - "back_home": "Back to homepage" - }, - "page_50X": { - "description": "The server encountered an error and could not complete your request.", - "back_home": "Back to homepage" - }, - "page_maintenance": { - "description": "We are under maintenance, we’ll be back soon." - }, - "admin": { - "admin_header": { - "title": "Admin" - }, - "nav_menus": { - "dashboard": "Dashboard", - "contents": "Contents", - "questions": "Questions", - "answers": "Answers", - "users": "Users", - "flags": "Flags", - "settings": "Settings", - "general": "General", - "interface": "Interface", - "smtp": "SMTP" - }, - "dashboard": { - "title": "Dashboard", - "welcome": "Welcome to Answer Admin !", - "site_statistics": "Site Statistics", - "questions": "Questions:", - "answers": "Answers:", - "comments": "Comments:", - "votes": "Votes:", - "active_users": "Active users:", - "flags": "Flags:", - "site_health_status": "Site Health Status", - "version": "Version:", - "https": "HTTPS:", - "uploading_files": "Uploading files:", - "smtp": "SMTP:", - "timezone": "Timezone:", - "system_info": "System Info", - "storage_used": "Storage used:", - "uptime": "Uptime:", - "answer_links": "Answer Links", - "documents": "Documents", - "feedback": "Feedback", - "review": "Review", - "config": "Config", - "update_to": "Update to", - "latest": "Latest", - "check_failed": "Check failed", - "yes": "Yes", - "no": "No", - "not_allowed": "Not allowed", - "allowed": "Allowed", - "enabled": "Enabled", - "disabled": "Disabled" - }, - "flags": { - "title": "Flags", - "pending": "Pending", - "completed": "Completed", - "flagged": "Flagged", - "created": "Created", - "action": "Action", - "review": "Review" - }, - "change_modal": { - "title": "Change user status to...", - "btn_cancel": "Cancel", - "btn_submit": "Submit", - "normal_name": "normal", - "normal_description": "A normal user can ask and answer questions.", - "suspended_name": "suspended", - "suspended_description": "A suspended user can't log in.", - "deleted_name": "deleted", - "deleted_description": "Delete profile, authentication associations.", - "inactive_name": "inactive", - "inactive_description": "An inactive user must re-validate their email.", - "confirm_title": "Delete this user", - "confirm_content": "Are you sure you want to delete this user? This is permanent!", - "confirm_btn": "Delete", - "msg": { - "empty": "Please select a reason." - } - }, - "status_modal": { - "title": "Change {{ type }} status to...", - "normal_name": "normal", - "normal_description": "A normal post available to everyone.", - "closed_name": "closed", - "closed_description": "A closed question can't answer, but still can edit, vote and comment.", - "deleted_name": "deleted", - "deleted_description": "All reputation gained and lost will be restored.", - "btn_cancel": "Cancel", - "btn_submit": "Submit", - "btn_next": "Next" - }, - "users": { - "title": "Users", - "name": "Name", - "email": "Email", - "reputation": "Reputation", - "created_at": "Created Time", - "delete_at": "Deleted Time", - "suspend_at": "Suspended Time", - "status": "Status", - "action": "Action", - "change": "Change", - "all": "All", - "inactive": "Inactive", - "suspended": "Suspended", - "deleted": "Deleted", - "normal": "Normal", - "filter": { - "placeholder": "Filter by name, user:id" - } - }, - "questions": { - "page_title": "Questions", - "normal": "Normal", - "closed": "Closed", - "deleted": "Deleted", - "post": "Post", - "votes": "Votes", - "answers": "Answers", - "created": "Created", - "status": "Status", - "action": "Action", - "change": "Change", - "filter": { - "placeholder": "Filter by title, question:id" - } - }, - "answers": { - "page_title": "Answers", - "normal": "Normal", - "deleted": "Deleted", - "post": "Post", - "votes": "Votes", - "created": "Created", - "status": "Status", - "action": "Action", - "change": "Change", - "filter": { - "placeholder": "Filter by title, answer:id" - } - }, - "general": { - "page_title": "General", - "name": { - "label": "Site Name", - "msg": "Site name cannot be empty.", - "text": "The name of this site, as used in the title tag." - }, - "site_url": { - "label": "Site URL", - "msg": "Site url cannot be empty.", - "text": "The address of your site." - }, - "short_description": { - "label": "Short Site Description (optional)", - "msg": "Short site description cannot be empty.", - "text": "Short description, as used in the title tag on homepage." - }, - "description": { - "label": "Site Description (optional)", - "msg": "Site description cannot be empty.", - "text": "Describe this site in one sentence, as used in the meta description tag." - }, - "contact_email": { - "label": "Contact Email", - "msg": "Contact email cannot be empty.", - "text": "Email address of key contact responsible for this site." - } - }, - "interface": { - "page_title": "Interface", - "logo": { - "label": "Logo (optional)", - "msg": "Site logo cannot be empty.", - "text": "You can upload your image or <1>reset it to the site title text." - }, - "theme": { - "label": "Theme", - "msg": "Theme cannot be empty.", - "text": "Select an existing theme." - }, - "language": { - "label": "Interface Language", - "msg": "Interface language cannot be empty.", - "text": "User interface language. It will change when you refresh the page." - }, - "time_zone": { - "label": "Timezone", - "msg": "Timezone cannot be empty.", - "text": "Choose a UTC (Coordinated Universal Time) time offset." - } - }, - "smtp": { - "page_title": "SMTP", - "from_email": { - "label": "From Email", - "msg": "From email cannot be empty.", - "text": "The email address which emails are sent from." - }, - "from_name": { - "label": "From Name", - "msg": "From name cannot be empty.", - "text": "The name which emails are sent from." - }, - "smtp_host": { - "label": "SMTP Host", - "msg": "SMTP host cannot be empty.", - "text": "Your mail server." - }, - "encryption": { - "label": "Encryption", - "msg": "Encryption cannot be empty.", - "text": "For most servers SSL is the recommended option.", - "ssl": "SSL", - "none": "None" - }, - "smtp_port": { - "label": "SMTP Port", - "msg": "SMTP port must be number 1 ~ 65535.", - "text": "The port to your mail server." - }, - "smtp_username": { - "label": "SMTP Username", - "msg": "SMTP username cannot be empty." - }, - "smtp_password": { - "label": "SMTP Password", - "msg": "SMTP password cannot be empty." - }, - "test_email_recipient": { - "label": "Test Email Recipients", - "text": "Provide email address that will receive test sends.", - "msg": "Test email recipients is invalid" - }, - "smtp_authentication": { - "label": "SMTP Authentication", - "msg": "SMTP authentication cannot be empty.", - "yes": "Yes", - "no": "No" - } - } - } -} diff --git a/ui/src/i18n/locales/en_US.yaml b/ui/src/i18n/locales/en_US.yaml new file mode 100644 index 00000000..27300e92 --- /dev/null +++ b/ui/src/i18n/locales/en_US.yaml @@ -0,0 +1,1119 @@ +base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." + +email: + other: "Email" +password: + other: "Password" + +email_or_password_wrong_error: &email_or_password_wrong + other: "Email and password do not match." + +error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Answer do not found." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + theme: + not_found: + other: "Theme not found." + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + +report: + spam: + name: + other: "spam" + description: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + description: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" + description: + other: "This question has been asked before and already has an answer." + not_answer: + name: + other: "not an answer" + description: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: + name: + other: "no longer needed" + description: + other: "This comment is outdated, conversational or not relevant to this post." + other: + name: + other: "something else" + description: + other: "This post requires staff attention for another reason not listed above." + +question: + close: + duplicate: + name: + other: "spam" + description: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + description: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + description: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + description: + other: "This post requires another reason not listed above." + +notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + adopt_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + description: >- +
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by + placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Webite Maintenance + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: 'Your account was suspended until {{ time }}.' + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4MB. + description: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + description: 'Must use the character set "a-z", "0-9", "+ # - ."' + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + description: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allowed deleting tag with posts.

Please remove this tag + from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: 'Must use the character set "a-z", "0-9", "+ # - ."' + description: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: 'MMM D, YYYY' + long_date_with_time: 'MMM D, YYYY [at] HH:mm' + now: now + x_seconds_ago: '{{count}}s ago' + x_minutes_ago: '{{count}}m ago' + x_hours_ago: '{{count}}h ago' + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid + answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are + adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_description: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: 'Describe what your question is about, at least one tag is required.' + no_result: No tags matched + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. + Please follow the instructions in the mail to activate your account. + info: 'If it doesn''t arrive, check your spam folder.' + another: >- + We sent another activation email to you at {{mail}}. It might + take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to Answer + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name up to 30 characters. + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in + page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is + already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + btn_name: Update profile + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: Default + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: 'https://example.com' + msg: Website incorrect format + location: + label: Location (optional) + placeholder: 'City, Country' + notification: + email: + label: Email Notifications + radio: 'Answers to your questions, comments, and more' + account: + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation + instructions. + email: + label: Email + msg: Email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the + edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because + doing so deprives future readers of this knowledge.

Repeated deletion + of answered questions can result in your account being blocked from asking. + Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because + doing so deprives future readers of this knowledge.

Repeated deletion + of accepted answers can result in your account being blocked from answering. + Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: '{{count}} Results' + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + tips: + title: Advanced Search Tips + tag: '<1>[tag] search withing a tag' + user: '<1>user:username search by author' + answer: '<1>answers:0 unanswered questions' + score: '<1>score:3 posts with a 3+ score' + question: '<1>is:question search questions' + is_answer: '<1>is:answer search answers' + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to Twitter + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your + account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was + already changed? + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: '{{ count }} Questions' + x_answers: '{{ count }} answers' + questions: Questions + answers: Answers + newest: Newest + active: Active + frequent: Frequent + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: '// Hello, World !' + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Answer + next: Next + done: Done + config_yaml_error: Can’t create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: 'db:3306' + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + description: >- + You can create the <1>config.yaml file manually in the + <1>/var/wwww/xxx/ directory and paste the following text into it. + info: 'After you’ve done that, click “Next” button.' + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure + location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your Answer is Ready! + ready_description: >- + If you ever feel like changing more settings, visit <1>admin section; + find it in the site menu. + good_luck: 'Have fun, and good luck!' + warning: Warning + warning_description: >- + The file <1>config.yaml already exists. If you need to reset any of the + configuration items in this file, please delete it first. You may try + <2>installing now. + installed: Already installed + installed_description: >- + You appear to have already installed. To reinstall please clear your old + database tables first. + upgrade: + title: Answer + update_btn: Update data + update_title: Data update required + update_description: >- + <1>Answer has been updated! Before you continue, we have to update your data + to the newest version.<1>The update process may take a little while, so + please be patient. + done_title: No update required + done_btn: Done + done_desscription: Your Answer data is already up-to-date. + page_404: + description: 'Unfortunately, this page doesn''t exist.' + back_home: Back to homepage + page_50X: + description: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + description: 'We are under maintenance, we’ll be back soon.' + admin: + admin_header: + title: Admin + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + dashboard: + title: Dashboard + welcome: Welcome to Answer Admin ! + site_statistics: Site Statistics + questions: 'Questions:' + answers: 'Answers:' + comments: 'Comments:' + votes: 'Votes:' + active_users: 'Active users:' + flags: 'Flags:' + site_health_status: Site Health Status + version: 'Version:' + https: 'HTTPS:' + uploading_files: 'Uploading files:' + smtp: 'SMTP:' + timezone: 'Timezone:' + system_info: System Info + storage_used: 'Storage used:' + uptime: 'Uptime:' + answer_links: Answer Links + documents: Documents + feedback: Feedback + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + 'yes': 'Yes' + 'no': 'No' + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_description: A normal user can ask and answer questions. + suspended_name: suspended + suspended_description: A suspended user can't log in. + deleted_name: deleted + deleted_description: 'Delete profile, authentication associations.' + inactive_name: inactive + inactive_description: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: 'Change {{ type }} status to...' + normal_name: normal + normal_description: A normal post available to everyone. + closed_name: closed + closed_description: 'A closed question can''t answer, but still can edit, vote and comment.' + deleted_name: deleted + deleted_description: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + action: Action + change: Change + all: All + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + filter: + placeholder: 'Filter by name, user:id' + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, question:id' + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, answer:id' + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: 'The name of this site, as used in the title tag.' + site_url: + label: Site URL + msg: Site url cannot be empty. + text: The address of your site. + short_description: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: 'Short description, as used in the title tag on homepage.' + description: + label: Site Description (optional) + msg: Site description cannot be empty. + text: 'Describe this site in one sentence, as used in the meta description tag.' + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a UTC (Coordinated Universal Time) time offset. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: SMTP Authentication + msg: SMTP authentication cannot be empty. + 'yes': 'Yes' + 'no': 'No' diff --git a/ui/src/i18n/locales/i18n.yaml b/ui/src/i18n/locales/i18n.yaml new file mode 100644 index 00000000..13a6c522 --- /dev/null +++ b/ui/src/i18n/locales/i18n.yaml @@ -0,0 +1,6 @@ +# all support language +language_options: + - label: "简体中文(CN)" + value: "zh_CN" + - label: "English(US)" + value: "en_US" diff --git a/ui/src/i18n/locales/it_IT.yaml b/ui/src/i18n/locales/it_IT.yaml new file mode 100644 index 00000000..0b943941 --- /dev/null +++ b/ui/src/i18n/locales/it_IT.yaml @@ -0,0 +1,170 @@ +base: + success: + other: "Successo" + unknown: + other: "Errore sconosciuto" + request_format_error: + other: "Il formato della richiesta non è valido" + unauthorized_error: + other: "Non autorizzato" + database_error: + other: "Errore server dati" + +email: + other: "email" +password: + other: "password" + +email_or_password_wrong_error: &email_or_password_wrong + other: "Email o password errati" + +error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Risposta non trovata" + comment: + edit_without_permission: + other: "Non si hanno di privilegi sufficienti per modificare il commento" + not_found: + other: "Commento non trovato" + email: + duplicate: + other: "email già esistente" + need_to_be_verified: + other: "email deve essere verificata" + verify_url_expired: + other: "l'url di verifica email è scaduto, si prega di reinviare la email" + lang: + not_found: + other: "lingua non trovata" + object: + captcha_verification_failed: + other: "captcha errato" + disallow_follow: + other: "Non sei autorizzato a seguire" + disallow_vote: + other: "non sei autorizzato a votare" + disallow_vote_your_self: + other: "Non puoi votare un tuo post!" + not_found: + other: "oggetto non trovato" + verification_failed: + other: "verifica fallita" + email_or_password_incorrect: + other: "email o password incorretti" + old_password_verification_failed: + other: "la verifica della vecchia password è fallita" + new_password_same_as_previous_setting: + other: "La nuova password è identica alla precedente" + question: + not_found: + other: "domanda non trovata" + rank: + fail_to_meet_the_condition: + other: "Condizioni non valide per il grado" + report: + handle_failed: + other: "Gestione del report fallita" + not_found: + other: "Report non trovato" + tag: + not_found: + other: "Etichetta non trovata" + theme: + not_found: + other: "tema non trovato" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "utente non trovato" + suspended: + other: "utente sospeso" + username_invalid: + other: "utente non valido" + username_duplicate: + other: "utente già in uso" + +report: + spam: + name: + other: "spam" + description: + other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" + rude: + name: + other: "scortese o violento" + description: + other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" + duplicate: + name: + other: "duplicato" + description: + other: "Questa domanda è già stata posta e ha già una risposta." + not_answer: + name: + other: "non è una risposta" + description: + other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." + not_need: + name: + other: "non più necessario" + description: + other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." + other: + name: + other: "altro" + description: + other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." + +question: + close: + duplicate: + name: + other: "spam" + description: + other: "Questa domanda è già stata chiesta o ha già una risposta." + guideline: + name: + other: "motivo legato alla community" + description: + other: "Questa domanda non soddisfa le linee guida della comunità." + multiple: + name: + other: "richiede maggiori dettagli o chiarezza" + description: + other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: + name: + other: "altro" + description: + other: "Questo articolo richiede un'altro motivo non listato sopra." + +notification: + action: + update_question: + other: "domanda aggiornata" + answer_the_question: + other: "domanda risposta" + update_answer: + other: "risposta aggiornata" + adopt_answer: + other: "risposta accettata" + comment_question: + other: "domanda commentata" + comment_answer: + other: "risposta commentata" + reply_to_you: + other: "hai ricevuto risposta" + mention_you: + other: "sei stato menzionato" + your_question_is_closed: + other: "la tua domanda è stata chiusa" + your_question_was_deleted: + other: "la tua domanda è stata rimossa" + your_answer_was_deleted: + other: "la tua risposta è stata rimossa" + your_comment_was_deleted: + other: "il tuo commento è stato rimosso" diff --git a/ui/src/i18n/locales/zh_CN.json b/ui/src/i18n/locales/zh_CN.json deleted file mode 100644 index a5e25ea3..00000000 --- a/ui/src/i18n/locales/zh_CN.json +++ /dev/null @@ -1,914 +0,0 @@ -{ - "how_to_format": { - "title": "如何设定文本格式", - "description": "
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 **粗体**

  • 使用 4 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 `像 _这样_`

  • 使用```创建代码块

    ```
    // 这是代码
    ```
" - }, - "pagination": { - "prev": "上一页", - "next": "下一页" - }, - "page_title": { - "question": "问题", - "questions": "问题", - "tag": "标签", - "tags": "标签", - "tag_wiki": "标签 wiki", - "edit_tag": "编辑标签", - "ask_a_question": "提问题", - "edit_question": "编辑问题", - "edit_answer": "编辑回答", - "search": "搜索", - "posts_containing": "包含", - "settings": "设定", - "notifications": "通知", - "login": "登录", - "sign_up": "注册", - "account_recovery": "账号恢复", - "account_activation": "账号激活", - "confirm_email": "确认电子邮件", - "account_suspended": "账号已封禁", - "admin": "后台管理" - }, - "notifications": { - "title": "通知", - "inbox": "收件箱", - "achievement": "成就", - "all_read": "全部标记为已读", - "show_more": "显示更多" - }, - "suspended": { - "title": "账号已封禁", - "until_time": "你的账号被封禁至{{ time }}。", - "forever": "你的账号已被永久封禁。", - "end": "违反了我们的社区准则。" - }, - "editor": { - "blockquote": { - "text": "引用" - }, - "bold": { - "text": "粗体" - }, - "chart": { - "text": "图表", - "flow_chart": "流程图", - "sequence_diagram": "时序图", - "class_diagram": "类图", - "state_diagram": "状态图", - "entity_relationship_diagram": "ER 图", - "user_defined_diagram": "User defined diagram", - "gantt_chart": "甘特图", - "pie_chart": "饼图" - }, - "code": { - "text": "代码块", - "add_code": "添加代码块", - "form": { - "fields": { - "code": { - "label": "代码块", - "msg": { - "empty": "代码块不能为空" - } - }, - "language": { - "label": "语言 (可选)", - "placeholder": "自动识别" - } - } - }, - "btn_cancel": "取消", - "btn_confirm": "添加" - }, - "formula": { - "text": "公式", - "options": { - "inline": "行内公式", - "block": "公式块" - } - }, - "heading": { - "text": "标题", - "options": { - "h1": "标题 1", - "h2": "标题 2", - "h3": "标题 3", - "h4": "标题 4", - "h5": "标题 5", - "h6": "标题 6" - } - }, - "help": { - "text": "帮助" - }, - "hr": { - "text": "水平分割线" - }, - "image": { - "text": "图片", - "add_image": "添加图片", - "tab_image": "上传图片", - "form_image": { - "fields": { - "file": { - "label": "图片文件", - "btn": "选择图片", - "msg": { - "empty": "请选择图片文件。", - "only_image": "只能上传图片文件。", - "max_size": "图片文件大小不能超过 4 MB。" - } - }, - "description": { - "label": "图片描述(可选)" - } - } - }, - "tab_url": "网络图片", - "form_url": { - "fields": { - "url": { - "label": "图片地址", - "msg": { - "empty": "图片地址不能为空" - } - }, - "name": { - "label": "图片描述(可选)" - } - } - }, - "btn_cancel": "取消", - "btn_confirm": "添加", - "uploading": "上传中..." - }, - "indent": { - "text": "添加缩进" - }, - "outdent": { - "text": "减少缩进" - }, - "italic": { - "text": "斜体" - }, - "link": { - "text": "超链接", - "add_link": "添加超链接", - "form": { - "fields": { - "url": { - "label": "链接", - "msg": { - "empty": "链接不能为空。" - } - }, - "name": { - "label": "链接描述(可选)" - } - } - }, - "btn_cancel": "取消", - "btn_confirm": "添加" - }, - "ordered_list": { - "text": "有编号列表" - }, - "unordered_list": { - "text": "无编号列表" - }, - "table": { - "text": "表格", - "heading": "表头", - "cell": "单元格" - } - }, - "close_modal": { - "title": "关闭原因是...", - "btn_cancel": "取消", - "btn_submit": "提交", - "remark": { - "empty": "不能为空。" - }, - "msg": { - "empty": "请选择一个原因。" - } - }, - "report_modal": { - "flag_title": "举报原因是...", - "close_title": "关闭原因是...", - "review_question_title": "审查问题", - "review_answer_title": "审查回答", - "review_comment_title": "审查评论", - "btn_cancel": "取消", - "btn_submit": "提交", - "remark": { - "empty": "不能为空" - }, - "msg": { - "empty": "请选择一个原因。" - } - }, - "tag_modal": { - "title": "创建新标签", - "form": { - "fields": { - "display_name": { - "label": "显示名称(别名)", - "msg": { - "empty": "不能为空", - "range": "不能超过 35 个字符" - } - }, - "slug_name": { - "label": "URL 固定链接", - "description": "必须由 \"a-z\", \"0-9\", \"+ # - .\" 组成", - "msg": { - "empty": "不能为空", - "range": "不能超过 35 个字符", - "character": "包含非法字符" - } - }, - "description": { - "label": "标签描述(可选)" - } - } - }, - "btn_cancel": "取消", - "btn_submit": "提交" - }, - "tag_info": { - "created_at": "创建于", - "edited_at": "编辑于", - "synonyms": { - "title": "同义词", - "text": "以下标签等同于", - "empty": "此标签目前没有同义词。", - "btn_add": "添加同义词", - "btn_edit": "编辑", - "btn_save": "保存" - }, - "synonyms_text": "以下标签等同于", - "delete": { - "title": "删除标签", - "content": "

不允许删除有关联问题的标签。

请先从关联的问题中删除此标签的引用。

", - "content2": "确定要删除吗?", - "close": "关闭" - } - }, - "edit_tag": { - "title": "编辑标签", - "default_reason": "编辑标签", - "form": { - "fields": { - "revision": { - "label": "编辑历史" - }, - "display_name": { - "label": "名称" - }, - "slug_name": { - "label": "URL 固定链接", - "info": "必须由 \"a-z\", \"0-9\", \"+ # - .\" 组成" - }, - "description": { - "label": "描述" - }, - "edit_summary": { - "label": "编辑概要", - "placeholder": "简单描述更改原因 (错别字、文字表达、格式等等)" - } - } - }, - "btn_save_edits": "保存更改", - "btn_cancel": "取消" - }, - "dates": { - "long_date": "YYYY年MM月", - "long_date_with_year": "YYYY年MM月DD日", - "long_date_with_time": "YYYY年MM月DD日 HH:mm", - "now": "刚刚", - "x_seconds_ago": "{{count}} 秒前", - "x_minutes_ago": "{{count}} 分钟前", - "x_hours_ago": "{{count}} 小时前" - }, - "comment": { - "btn_add_comment": "添加评论", - "reply_to": "回复", - "btn_reply": "回复", - "btn_edit": "编辑", - "btn_delete": "删除", - "btn_flag": "举报", - "btn_save_edits": "保存", - "btn_cancel": "取消", - "show_more": "显示更多评论", - "tip_question": "使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。", - "tip_answer": "使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。" - }, - "edit_answer": { - "title": "编辑回答", - "default_reason": "编辑回答", - "form": { - "fields": { - "revision": { - "label": "编辑历史" - }, - "answer": { - "label": "回答内容" - }, - "edit_summary": { - "label": "编辑概要", - "placeholder": "简单描述更改原因 (错别字、文字表达、格式等等)" - } - } - }, - "btn_save_edits": "保存更改", - "btn_cancel": "取消" - }, - "tags": { - "title": "标签", - "sort_buttons": { - "popular": "热门", - "name": "名称", - "newest": "最新" - }, - "button_follow": "关注", - "button_following": "已关注", - "tag_label": "个问题", - "search_placeholder": "通过标签名过滤", - "no_description": "此标签无描述。", - "more": "更多" - }, - "ask": { - "title": "提交新的问题", - "edit_title": "编辑问题", - "default_reason": "编辑问题", - "similar_questions": "相似的问题", - "form": { - "fields": { - "revision": { - "label": "编辑历史" - }, - "title": { - "label": "标题", - "placeholder": "请详细描述你的问题", - "msg": { - "empty": "标题不能为空", - "range": "标题最多 150 个字符" - } - }, - "body": { - "label": "内容", - "msg": { - "empty": "内容不能为空" - } - }, - "tags": { - "label": "标签", - "msg": { - "empty": "必须选择一个标签" - } - }, - "answer": { - "label": "回答内容", - "msg": { - "empty": "回答内容不能为空" - } - } - } - }, - "btn_post_question": "提交问题", - "btn_save_edits": "保存更改", - "answer_question": "直接发表回答", - "post_question&answer": "提交问题和回答" - }, - "tag_selector": { - "add_btn": "添加标签", - "create_btn": "创建新标签", - "search_tag": "搜索标签", - "hint": "选择至少一个与问题相关的标签。", - "no_result": "没有匹配的标签" - }, - "header": { - "nav": { - "question": "问题", - "tag": "标签", - "user": "用户", - "profile": "用户主页", - "setting": "账号设置", - "logout": "退出登录", - "admin": "后台管理" - }, - "search": { - "placeholder": "搜索" - } - }, - "footer": { - "build_on": "Built on <1> Answer - the open-source software that power Q&A communities
Made with love © 2022 Answer" - }, - "upload_img": { - "name": "更改图片", - "loading": "加载中..." - }, - "pic_auth_code": { - "title": "验证码", - "placeholder": "输入图片中的文字", - "msg": { - "empty": "不能为空" - } - }, - "inactive": { - "first": "马上就好了!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活您的帐户。", - "info": "如果没有收到,请检查您的垃圾邮件文件夹。", - "another": "我们向您发送了另一封激活电子邮件,地址为 {{mail}}。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。", - "btn_name": "重新发送激活邮件", - "msg": { - "empty": "不能为空" - } - }, - "login": { - "page_title": "欢迎来到 Answer", - "info_sign": "没有帐户?<1>注册", - "info_login": "已经有一个帐户?<1>登录", - "forgot_pass": "忘记密码?", - "name": { - "label": "昵称", - "msg": { - "empty": "昵称不能为空", - "range": "昵称最多 30 个字符" - } - }, - "email": { - "label": "邮箱", - "msg": { - "empty": "邮箱不能为空" - } - }, - "password": { - "label": "密码", - "msg": { - "empty": "密码不能为空", - "different": "两次输入密码不一致" - } - } - }, - "account_forgot": { - "page_title": "忘记密码", - "btn_name": "发送恢复邮件", - "send_success": "如无意外,你的邮箱 {{mail}} 将会收到一封重置密码的邮件,请根据指引重置你的密码。", - "email": { - "label": "邮箱", - "msg": { - "empty": "邮箱不能为空" - } - } - }, - "password_reset": { - "page_title": "密码重置", - "btn_name": "重置我的密码", - "reset_success": "你已经成功更改密码,将返回登录页面", - "link_invalid": "抱歉,此密码重置链接已失效。也许是你已经重置过密码了?", - "to_login": "前往登录页面", - "password": { - "label": "密码", - "msg": { - "empty": "密码不能为空", - "length": "密码长度在8-32个字符之间", - "different": "两次输入密码不一致" - } - }, - "password_confirm": { - "label": "确认新密码" - } - }, - "settings": { - "page_title": "设置", - "nav": { - "profile": "我的资料", - "notification": "通知", - "account": "账号", - "interface": "界面" - }, - "profile": { - "btn_name": "保存更改", - "display_name": { - "label": "昵称", - "msg": "昵称不能为空", - "msg_range": "昵称不能超过 30 个字符" - }, - "username": { - "label": "用户名", - "caption": "用户之间可以通过 \"@用户名\" 进行交互。", - "msg": "用户名不能为空", - "msg_range": "用户名不能超过 30 个字符", - "character": "用户名只能由 \"a-z\", \"0-9\", \" - . _\" 组成" - }, - "avatar": { - "label": "头像", - "text": "您可以上传图片作为头像,也可以 <1>重置 为" - }, - "bio": { - "label": "关于我 (可选)" - }, - "website": { - "label": "网站 (可选)", - "placeholder": "https://example.com", - "msg": "格式不正确" - }, - "location": { - "label": "位置 (可选)", - "placeholder": "城市, 国家" - } - }, - "notification": { - "email": { - "label": "邮件通知", - "radio": "你的提问有新的回答,评论,和其他" - } - }, - "account": { - "change_email_btn": "更改邮箱", - "change_pass_btn": "更改密码", - "change_email_info": "邮件已发送。请根据指引完成验证。", - "email": { - "label": "邮箱", - "msg": "邮箱不能为空" - }, - "password_title": "密码", - "current_pass": { - "label": "当前密码", - "msg": { - "empty": "当前密码不能为空", - "length": "密码长度必须在 8 至 32 之间", - "different": "两次输入的密码不匹配" - } - }, - "new_pass": { - "label": "新密码" - }, - "pass_confirm": { - "label": "确认新密码" - } - }, - "interface": { - "lang": { - "label": "界面语言", - "text": "设置用户界面语言,在刷新页面后生效。" - } - } - }, - "toast": { - "update": "更新成功", - "update_password": "更改密码成功。", - "flag_success": "感谢您的标记,我们会尽快处理。" - }, - "related_question": { - "title": "相关问题", - "btn": "我要提问", - "answers": "个回答" - }, - "question_detail": { - "Asked": "提问于", - "asked": "提问于", - "update": "修改于", - "edit": "最后编辑于", - "Views": "阅读次数", - "Follow": "关注此问题", - "Following": "已关注", - "answered": "回答于", - "closed_in": "关闭于", - "show_exist": "查看相关问题。", - "answers": { - "title": "个回答", - "score": "评分", - "newest": "最新", - "btn_accept": "采纳", - "btn_accepted": "已被采纳" - }, - "write_answer": { - "title": "你的回答", - "btn_name": "提交你的回答", - "confirm_title": "继续回答", - "continue": "继续", - "confirm_info": "

您确定要提交一个新的回答吗?

您可以直接编辑和改善您之前的回答的。

", - "empty": "回答内容不能为空。" - } - }, - "delete": { - "title": "删除", - "question": "我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗?", - "answer_accepted": "

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗?", - "other": "你确定要删除?", - "tip_question_deleted": "此问题已被删除", - "tip_answer_deleted": "此回答已被删除" - }, - "btns": { - "confirm": "确认", - "cancel": "取消", - "save": "保存", - "delete": "删除", - "login": "登录", - "signup": "注册", - "logout": "退出登录", - "verify": "验证", - "add_question": "我要提问" - }, - "search": { - "title": "搜索结果", - "keywords": "关键词", - "options": "选项", - "follow": "关注", - "following": "已关注", - "counts": "{{count}} 个结果", - "more": "更多", - "sort_btns": { - "relevance": "相关性", - "newest": "最新的", - "active": "活跃的", - "score": "评分" - }, - "tips": { - "title": "高级搜索提示", - "tag": "<1>[tag] 在指定标签中搜索", - "user": "<1>user:username 根据作者搜索", - "answer": "<1>answers:0 搜索未回答的问题", - "score": "<1>score:3 评分 3 分或以上", - "question": "<1>is:question 只搜索问题", - "is_answer": "<1>is:answer 只搜索回答" - }, - "empty": "找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。" - }, - "share": { - "name": "分享", - "copy": "复制链接", - "via": "分享在...", - "copied": "已复制", - "facebook": "分享到 Facebook", - "twitter": "分享到 Twitter" - }, - "cannot_vote_for_self": "不能给自己投票", - "modal_confirm": { - "title": "发生错误..." - }, - "account_result": { - "page_title": "欢迎来到 Answer", - "success": "你的账号已通过验证,即将返回首页。", - "link": "返回首页", - "invalid": "抱歉,此验证链接已失效。也许是你的账号已经通过验证了?", - "confirm_new_email": "你的电子邮箱已更新", - "confirm_new_email_invalid": "抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了?" - }, - "question": { - "following_tags": "已关注的标签", - "edit": "编辑", - "save": "保存", - "follow_tag_tip": "按照标签整理您的问题列表。", - "hot_questions": "热点问题", - "all_questions": "全部问题", - "x_questions": "{{ count }} 个问题", - "x_answers": "{{ count }} 个回答", - "questions": "个问题", - "answers": "回答", - "newest": "最新", - "active": "活跃", - "frequent": "浏览量", - "score": "评分", - "unanswered": "未回答", - "modified": "修改于", - "answered": "回答于", - "asked": "提问于", - "closed": "已关闭", - "follow_a_tag": "关注一个标签", - "more": "更多" - }, - "personal": { - "overview": "概览", - "answers": "回答", - "answer": "回答", - "questions": "问题", - "question": "问题", - "bookmarks": "收藏", - "reputation": "声望", - "comments": "评论", - "votes": "得票", - "newest": "最新", - "score": "评分", - "edit_profile": "编辑我的资料", - "visited_x_days": "Visited {{ count }} days", - "viewed": "Viewed", - "joined": "加入于", - "last_login": "上次登录", - "about_me": "关于我", - "about_me_empty": "// Hello, World !", - "top_answers": "热门回答", - "top_questions": "热门问题", - "stats": "状态", - "list_empty": "没有找到相关的内容。
试试看其他标签?", - "accepted": "已采纳", - "answered": "回答于", - "asked": "提问于", - "upvote": "赞同", - "downvote": "反对", - "mod_short": "管理员", - "mod_long": "管理员", - "x_reputation": "声望", - "x_votes": "得票", - "x_answers": "个回答", - "x_questions": "个问题" - }, - "page_404": { - "description": "页面不存在", - "back_home": "回到主页" - }, - "page_50X": { - "description": "服务器遇到了一个错误,无法完成你的请求。", - "back_home": "回到主页" - }, - "admin": { - "admin_header": { - "title": "后台管理" - }, - "nav_menus": { - "dashboard": "后台管理", - "contents": "内容管理", - "questions": "问题", - "answers": "回答", - "users": "用户管理", - "flags": "举报管理", - "settings": "站点设置", - "general": "一般", - "interface": "界面", - "smtp": "SMTP" - }, - "dashboard": { - "title": "后台管理", - "welcome": "欢迎来到 Answer 后台管理!", - "version": "版本" - }, - "flags": { - "title": "举报", - "pending": "等待处理", - "completed": "已完成", - "flagged": "被举报内容", - "created": "创建于", - "action": "操作", - "review": "审查" - }, - "change_modal": { - "title": "更改用户状态为...", - "btn_cancel": "取消", - "btn_submit": "提交", - "normal_name": "正常", - "normal_description": "正常状态的用户可以提问和回答。", - "suspended_name": "封禁", - "suspended_description": "被封禁的用户将无法登录。", - "deleted_name": "删除", - "deleted_description": "删除用户的个人信息,认证等等。", - "inactive_name": "不活跃", - "inactive_description": "不活跃的用户必须重新验证邮箱。", - "confirm_title": "删除此用户", - "confirm_content": "确定要删除此用户?此操作无法撤销!", - "confirm_btn": "删除", - "msg": { - "empty": "请选择一个原因" - } - }, - "status_modal": { - "title": "更改 {{ type }} 状态为...", - "normal_name": "正常", - "normal_description": "所有用户都可以访问", - "closed_name": "关闭", - "closed_description": "不能回答,但仍然可以编辑、投票和评论。", - "deleted_name": "删除", - "deleted_description": "所有获得/损失的声望将会恢复。", - "btn_cancel": "取消", - "btn_submit": "提交", - "btn_next": "下一步" - }, - "users": { - "title": "用户", - "name": "名称", - "email": "邮箱", - "reputation": "声望", - "created_at": "创建时间", - "delete_at": "删除时间", - "suspend_at": "封禁时间", - "status": "状态", - "action": "操作", - "change": "更改", - "all": "全部", - "inactive": "不活跃", - "suspended": "已封禁", - "deleted": "已删除", - "normal": "正常" - }, - "questions": { - "page_title": "问题", - "normal": "正常", - "closed": "已关闭", - "deleted": "已删除", - "post": "标题", - "votes": "得票数", - "answers": "回答数", - "created": "创建于", - "status": "状态", - "action": "操作", - "change": "更改" - }, - "answers": { - "page_title": "回答", - "normal": "正常", - "deleted": "已删除", - "post": "标题", - "votes": "得票数", - "created": "创建于", - "status": "状态", - "action": "操作", - "change": "更改" - }, - "general": { - "page_title": "一般", - "name": { - "label": "站点名称", - "msg": "不能为空", - "text": "站点的名称,作为站点的标题(HTML 的 title 标签)。" - }, - "short_description": { - "label": "简短的站点标语 (可选)", - "msg": "不能为空", - "text": "简短的标语,作为网站主页的标题(HTML 的 title 标签)。" - }, - "description": { - "label": "网站描述 (可选)", - "msg": "不能为空", - "text": "使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。" - } - }, - "interface": { - "page_title": "界面", - "logo": { - "label": "Logo (可选)", - "msg": "不能为空", - "text": "可以上传图片,或者<1>重置为站点标题。" - }, - "theme": { - "label": "主题", - "msg": "不能为空", - "text": "选择一个主题" - }, - "language": { - "label": "界面语言", - "msg": "不能为空", - "text": "设置用户界面语言,在刷新页面后生效。" - } - }, - "smtp": { - "page_title": "SMTP", - "from_email": { - "label": "发件人地址", - "msg": "不能为空", - "text": "用于发送邮件的地址。" - }, - "from_name": { - "label": "发件人名称", - "msg": "不能为空", - "text": "发件人的名称" - }, - "smtp_host": { - "label": "SMTP 主机", - "msg": "不能为空", - "text": "邮件服务器" - }, - "encryption": { - "label": "加密", - "msg": "不能为空", - "text": "对于大多数服务器而言,SSL 是推荐开启的。", - "ssl": "SSL", - "none": "无加密" - }, - "smtp_port": { - "label": "SMTP 端口", - "msg": "SMTP 端口必须在 1 ~ 65535 之间。", - "text": "邮件服务器的端口号。" - }, - "smtp_username": { - "label": "SMTP 用户名", - "msg": "不能为空" - }, - "smtp_password": { - "label": "SMTP 密码", - "msg": "不能为空" - }, - "test_email_recipient": { - "label": "测试邮件收件人", - "text": "提供用于接收测试邮件的邮箱地址。", - "msg": "地址无效" - }, - "smtp_authentication": { - "label": "SMTP 认证", - "msg": "不能为空", - "yes": "是", - "no": "否" - } - } - } -} diff --git a/ui/src/i18n/locales/zh_CN.yaml b/ui/src/i18n/locales/zh_CN.yaml new file mode 100644 index 00000000..34fd9dc9 --- /dev/null +++ b/ui/src/i18n/locales/zh_CN.yaml @@ -0,0 +1,919 @@ +base: + success: + other: "成功" + unknown: + other: "未知错误" + request_format_error: + other: "请求格式错误" + unauthorized_error: + other: "未登录" + database_error: + other: "数据服务异常" + +email: + other: "邮箱" +password: + other: "密码" + +email_or_password_wrong_error: &email_or_password_wrong + other: "邮箱或密码错误" + +error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "答案未找到" + comment: + edit_without_permission: + other: "不允许编辑评论" + not_found: + other: "评论未找到" + email: + duplicate: + other: "邮箱已经存在" + need_to_be_verified: + other: "邮箱需要验证" + verify_url_expired: + other: "邮箱验证的网址已过期,请重新发送邮件" + lang: + not_found: + other: "语言未找到" + object: + captcha_verification_failed: + other: "验证码错误" + disallow_follow: + other: "你不能关注" + disallow_vote: + other: "你不能投票" + disallow_vote_your_self: + other: "你不能为自己的帖子投票!" + not_found: + other: "对象未找到" + verification_failed: + other: "验证失败" + email_or_password_incorrect: + other: "邮箱或密码不正确" + old_password_verification_failed: + other: "旧密码验证失败" + new_password_same_as_previous_setting: + other: "新密码与之前的设置相同" + question: + not_found: + other: "问题未找到" + rank: + fail_to_meet_the_condition: + other: "级别不符合条件" + report: + handle_failed: + other: "报告处理失败" + not_found: + other: "报告未找到" + tag: + not_found: + other: "标签未找到" + theme: + not_found: + other: "主题未找到" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "用户未找到" + suspended: + other: "用户已被暂停" + username_invalid: + other: "用户名无效" + username_duplicate: + other: "用户名已被使用" + set_avatar: + other: "头像设置错误" + +report: + spam: + name: + other: "垃圾信息" + description: + other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" + rude: + name: + other: "粗鲁或辱骂的" + description: + other: "有理智的人都会发现此内容不适合进行尊重的讨论。" + duplicate: + name: + other: "重复信息" + description: + other: "此问题以前就有人问过,而且已经有了答案。" + not_answer: + name: + other: "不是答案" + description: + other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" + not_need: + name: + other: "不再需要" + description: + other: "此条评论是过时的,对话性的或与本帖无关。" + other: + name: + other: "其他原因" + description: + other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" + +question: + close: + duplicate: + name: + other: "垃圾信息" + description: + other: "此问题以前就有人问过,而且已经有了答案。" + guideline: + name: + other: "社区特定原因" + description: + other: "此问题不符合社区准则。" + multiple: + name: + other: "需要细节或澄清" + description: + other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: + name: + other: "其他原因" + description: + other: "此帖子需要上述所列以外的其他理由。" + +notification: + action: + update_question: + other: "更新了问题" + answer_the_question: + other: "回答了问题" + update_answer: + other: "更新了答案" + adopt_answer: + other: "接受了答案" + comment_question: + other: "评论了问题" + comment_answer: + other: "评论了答案" + reply_to_you: + other: "回复了你" + mention_you: + other: "提到了你" + your_question_is_closed: + other: "你的问题已被关闭" + your_question_was_deleted: + other: "你的问题已被删除" + your_answer_was_deleted: + other: "你的答案已被删除" + your_comment_was_deleted: + other: "你的评论已被删除" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何设定文本格式 + description: >- +
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 + **粗体**

  • 使用 4 + 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 + `像 _这样_`

  • 使用```创建代码块

    ```
    // + 这是代码
    ```
+ pagination: + prev: 上一页 + next: 下一页 + page_title: + question: 问题 + questions: 问题 + tag: 标签 + tags: 标签 + tag_wiki: 标签 wiki + edit_tag: 编辑标签 + ask_a_question: 提问题 + edit_question: 编辑问题 + edit_answer: 编辑回答 + search: 搜索 + posts_containing: 包含 + settings: 设定 + notifications: 通知 + login: 登录 + sign_up: 注册 + account_recovery: 账号恢复 + account_activation: 账号激活 + confirm_email: 确认电子邮件 + account_suspended: 账号已封禁 + admin: 后台管理 + notifications: + title: 通知 + inbox: 收件箱 + achievement: 成就 + all_read: 全部标记为已读 + show_more: 显示更多 + suspended: + title: 账号已封禁 + until_time: '你的账号被封禁至{{ time }}。' + forever: 你的账号已被永久封禁。 + end: 违反了我们的社区准则。 + editor: + blockquote: + text: 引用 + bold: + text: 粗体 + chart: + text: 图表 + flow_chart: 流程图 + sequence_diagram: 时序图 + class_diagram: 类图 + state_diagram: 状态图 + entity_relationship_diagram: ER 图 + user_defined_diagram: User defined diagram + gantt_chart: 甘特图 + pie_chart: 饼图 + code: + text: 代码块 + add_code: 添加代码块 + form: + fields: + code: + label: 代码块 + msg: + empty: 代码块不能为空 + language: + label: 语言 (可选) + placeholder: 自动识别 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 行内公式 + block: 公式块 + heading: + text: 标题 + options: + h1: 标题 1 + h2: 标题 2 + h3: 标题 3 + h4: 标题 4 + h5: 标题 5 + h6: 标题 6 + help: + text: 帮助 + hr: + text: 水平分割线 + image: + text: 图片 + add_image: 添加图片 + tab_image: 上传图片 + form_image: + fields: + file: + label: 图片文件 + btn: 选择图片 + msg: + empty: 请选择图片文件。 + only_image: 只能上传图片文件。 + max_size: 图片文件大小不能超过 4 MB。 + description: + label: 图片描述(可选) + tab_url: 网络图片 + form_url: + fields: + url: + label: 图片地址 + msg: + empty: 图片地址不能为空 + name: + label: 图片描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上传中... + indent: + text: 添加缩进 + outdent: + text: 减少缩进 + italic: + text: 斜体 + link: + text: 超链接 + add_link: 添加超链接 + form: + fields: + url: + label: 链接 + msg: + empty: 链接不能为空。 + name: + label: 链接描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: 有编号列表 + unordered_list: + text: 无编号列表 + table: + text: 表格 + heading: 表头 + cell: 单元格 + close_modal: + title: 关闭原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空。 + msg: + empty: 请选择一个原因。 + report_modal: + flag_title: 举报原因是... + close_title: 关闭原因是... + review_question_title: 审查问题 + review_answer_title: 审查回答 + review_comment_title: 审查评论 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空 + msg: + empty: 请选择一个原因。 + tag_modal: + title: 创建新标签 + form: + fields: + display_name: + label: 显示名称(别名) + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + slug_name: + label: URL 固定链接 + description: '必须由 "a-z", "0-9", "+ # - ." 组成' + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + character: 包含非法字符 + description: + label: 标签描述(可选) + btn_cancel: 取消 + btn_submit: 提交 + tag_info: + created_at: 创建于 + edited_at: 编辑于 + synonyms: + title: 同义词 + text: 以下标签等同于 + empty: 此标签目前没有同义词。 + btn_add: 添加同义词 + btn_edit: 编辑 + btn_save: 保存 + synonyms_text: 以下标签等同于 + delete: + title: 删除标签 + content:

不允许删除有关联问题的标签。

请先从关联的问题中删除此标签的引用。

+ content2: 确定要删除吗? + close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + form: + fields: + revision: + label: 编辑历史 + display_name: + label: 名称 + slug_name: + label: URL 固定链接 + info: '必须由 "a-z", "0-9", "+ # - ." 组成' + description: + label: 描述 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: YYYY年MM月 + long_date_with_year: YYYY年MM月DD日 + long_date_with_time: 'YYYY年MM月DD日 HH:mm' + now: 刚刚 + x_seconds_ago: '{{count}} 秒前' + x_minutes_ago: '{{count}} 分钟前' + x_hours_ago: '{{count}} 小时前' + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存 + btn_cancel: 取消 + show_more: 显示更多评论 + tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。 + tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名过滤 + no_description: 此标签无描述。 + more: 更多 + ask: + title: 提交新的问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + similar_questions: 相似的问题 + form: + fields: + revision: + label: 编辑历史 + title: + label: 标题 + placeholder: 请详细描述你的问题 + msg: + empty: 标题不能为空 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 直接发表回答 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: 选择至少一个与问题相关的标签。 + no_result: 没有匹配的标签 + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + profile: 用户主页 + setting: 账号设置 + logout: 退出登录 + admin: 后台管理 + search: + placeholder: 搜索 + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: 更改图片 + loading: 加载中... + pic_auth_code: + title: 验证码 + placeholder: 输入图片中的文字 + msg: + empty: 不能为空 + inactive: + first: '马上就好了!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活您的帐户。' + info: 如果没有收到,请检查您的垃圾邮件文件夹。 + another: '我们向您发送了另一封激活电子邮件,地址为 {{mail}}。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。' + btn_name: 重新发送激活邮件 + msg: + empty: 不能为空 + login: + page_title: 欢迎来到 Answer + info_sign: 没有帐户?<1>注册 + info_login: 已经有一个帐户?<1>登录 + forgot_pass: 忘记密码? + name: + label: 昵称 + msg: + empty: 昵称不能为空 + range: 昵称最多 30 个字符 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password: + label: 密码 + msg: + empty: 密码不能为空 + different: 两次输入密码不一致 + account_forgot: + page_title: 忘记密码 + btn_name: 发送恢复邮件 + send_success: '如无意外,你的邮箱 {{mail}} 将会收到一封重置密码的邮件,请根据指引重置你的密码。' + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password_reset: + page_title: 密码重置 + btn_name: 重置我的密码 + reset_success: 你已经成功更改密码,将返回登录页面 + link_invalid: 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? + to_login: 前往登录页面 + password: + label: 密码 + msg: + empty: 密码不能为空 + length: 密码长度在8-32个字符之间 + different: 两次输入密码不一致 + password_confirm: + label: 确认新密码 + settings: + page_title: 设置 + nav: + profile: 我的资料 + notification: 通知 + account: 账号 + interface: 界面 + profile: + btn_name: 保存更改 + display_name: + label: 昵称 + msg: 昵称不能为空 + msg_range: 昵称不能超过 30 个字符 + username: + label: 用户名 + caption: 用户之间可以通过 "@用户名" 进行交互。 + msg: 用户名不能为空 + msg_range: 用户名不能超过 30 个字符 + character: '用户名只能由 "a-z", "0-9", " - . _" 组成' + avatar: + label: 头像 + text: 您可以上传图片作为头像,也可以 <1>重置 为 + bio: + label: 关于我 (可选) + website: + label: 网站 (可选) + placeholder: 'https://example.com' + msg: 格式不正确 + location: + label: 位置 (可选) + placeholder: '城市, 国家' + notification: + email: + label: 邮件通知 + radio: 你的提问有新的回答,评论,和其他 + account: + change_email_btn: 更改邮箱 + change_pass_btn: 更改密码 + change_email_info: 邮件已发送。请根据指引完成验证。 + email: + label: 邮箱 + msg: 邮箱不能为空 + password_title: 密码 + current_pass: + label: 当前密码 + msg: + empty: 当前密码不能为空 + length: 密码长度必须在 8 至 32 之间 + different: 两次输入的密码不匹配 + new_pass: + label: 新密码 + pass_confirm: + label: 确认新密码 + interface: + lang: + label: 界面语言 + text: 设置用户界面语言,在刷新页面后生效。 + toast: + update: 更新成功 + update_password: 更改密码成功。 + flag_success: 感谢您的标记,我们会尽快处理。 + related_question: + title: 相关问题 + btn: 我要提问 + answers: 个回答 + question_detail: + Asked: 提问于 + asked: 提问于 + update: 修改于 + edit: 最后编辑于 + Views: 阅读次数 + Follow: 关注此问题 + Following: 已关注 + answered: 回答于 + closed_in: 关闭于 + show_exist: 查看相关问题。 + answers: + title: 个回答 + score: 评分 + newest: 最新 + btn_accept: 采纳 + btn_accepted: 已被采纳 + write_answer: + title: 你的回答 + btn_name: 提交你的回答 + confirm_title: 继续回答 + continue: 继续 + confirm_info:

您确定要提交一个新的回答吗?

您可以直接编辑和改善您之前的回答的。

+ empty: 回答内容不能为空。 + delete: + title: 删除 + question: >- + 我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_question_deleted: 此问题已被删除 + tip_answer_deleted: 此回答已被删除 + btns: + confirm: 确认 + cancel: 取消 + save: 保存 + delete: 删除 + login: 登录 + signup: 注册 + logout: 退出登录 + verify: 验证 + add_question: 我要提问 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: '{{count}} 个结果' + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + tips: + title: 高级搜索提示 + tag: '<1>[tag] 在指定标签中搜索' + user: '<1>user:username 根据作者搜索' + answer: '<1>answers:0 搜索未回答的问题' + score: '<1>score:3 评分 3 分或以上' + question: '<1>is:question 只搜索问题' + is_answer: '<1>is:answer 只搜索回答' + empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 + share: + name: 分享 + copy: 复制链接 + via: 分享在... + copied: 已复制 + facebook: 分享到 Facebook + twitter: 分享到 Twitter + cannot_vote_for_self: 不能给自己投票 + modal_confirm: + title: 发生错误... + account_result: + page_title: 欢迎来到 Answer + success: 你的账号已通过验证,即将返回首页。 + link: 返回首页 + invalid: 抱歉,此验证链接已失效。也许是你的账号已经通过验证了? + confirm_new_email: 你的电子邮箱已更新 + confirm_new_email_invalid: 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? + question: + following_tags: 已关注的标签 + edit: 编辑 + save: 保存 + follow_tag_tip: 按照标签整理您的问题列表。 + hot_questions: 热点问题 + all_questions: 全部问题 + x_questions: '{{ count }} 个问题' + x_answers: '{{ count }} 个回答' + questions: 个问题 + answers: 回答 + newest: 最新 + active: 活跃 + frequent: 浏览量 + score: 评分 + unanswered: 未回答 + modified: 修改于 + answered: 回答于 + asked: 提问于 + closed: 已关闭 + follow_a_tag: 关注一个标签 + more: 更多 + personal: + overview: 概览 + answers: 回答 + answer: 回答 + questions: 问题 + question: 问题 + bookmarks: 收藏 + reputation: 声望 + comments: 评论 + votes: 得票 + newest: 最新 + score: 评分 + edit_profile: 编辑我的资料 + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: 加入于 + last_login: 上次登录 + about_me: 关于我 + about_me_empty: '// Hello, World !' + top_answers: 热门回答 + top_questions: 热门问题 + stats: 状态 + list_empty: 没有找到相关的内容。
试试看其他标签? + accepted: 已采纳 + answered: 回答于 + asked: 提问于 + upvote: 赞同 + downvote: 反对 + mod_short: 管理员 + mod_long: 管理员 + x_reputation: 声望 + x_votes: 得票 + x_answers: 个回答 + x_questions: 个问题 + page_404: + description: 页面不存在 + back_home: 回到主页 + page_50X: + description: 服务器遇到了一个错误,无法完成你的请求。 + back_home: 回到主页 + admin: + admin_header: + title: 后台管理 + nav_menus: + dashboard: 后台管理 + contents: 内容管理 + questions: 问题 + answers: 回答 + users: 用户管理 + flags: 举报管理 + settings: 站点设置 + general: 一般 + interface: 界面 + smtp: SMTP + dashboard: + title: 后台管理 + welcome: 欢迎来到 Answer 后台管理! + version: 版本 + flags: + title: 举报 + pending: 等待处理 + completed: 已完成 + flagged: 被举报内容 + created: 创建于 + action: 操作 + review: 审查 + change_modal: + title: 更改用户状态为... + btn_cancel: 取消 + btn_submit: 提交 + normal_name: 正常 + normal_description: 正常状态的用户可以提问和回答。 + suspended_name: 封禁 + suspended_description: 被封禁的用户将无法登录。 + deleted_name: 删除 + deleted_description: 删除用户的个人信息,认证等等。 + inactive_name: 不活跃 + inactive_description: 不活跃的用户必须重新验证邮箱。 + confirm_title: 删除此用户 + confirm_content: 确定要删除此用户?此操作无法撤销! + confirm_btn: 删除 + msg: + empty: 请选择一个原因 + status_modal: + title: '更改 {{ type }} 状态为...' + normal_name: 正常 + normal_description: 所有用户都可以访问 + closed_name: 关闭 + closed_description: 不能回答,但仍然可以编辑、投票和评论。 + deleted_name: 删除 + deleted_description: 所有获得/损失的声望将会恢复。 + btn_cancel: 取消 + btn_submit: 提交 + btn_next: 下一步 + users: + title: 用户 + name: 名称 + email: 邮箱 + reputation: 声望 + created_at: 创建时间 + delete_at: 删除时间 + suspend_at: 封禁时间 + status: 状态 + action: 操作 + change: 更改 + all: 全部 + inactive: 不活跃 + suspended: 已封禁 + deleted: 已删除 + normal: 正常 + questions: + page_title: 问题 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + post: 标题 + votes: 得票数 + answers: 回答数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + answers: + page_title: 回答 + normal: 正常 + deleted: 已删除 + post: 标题 + votes: 得票数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + general: + page_title: 一般 + name: + label: 站点名称 + msg: 不能为空 + text: 站点的名称,作为站点的标题(HTML 的 title 标签)。 + short_description: + label: 简短的站点标语 (可选) + msg: 不能为空 + text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。 + description: + label: 网站描述 (可选) + msg: 不能为空 + text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。 + interface: + page_title: 界面 + logo: + label: Logo (可选) + msg: 不能为空 + text: 可以上传图片,或者<1>重置为站点标题。 + theme: + label: 主题 + msg: 不能为空 + text: 选择一个主题 + language: + label: 界面语言 + msg: 不能为空 + text: 设置用户界面语言,在刷新页面后生效。 + smtp: + page_title: SMTP + from_email: + label: 发件人地址 + msg: 不能为空 + text: 用于发送邮件的地址。 + from_name: + label: 发件人名称 + msg: 不能为空 + text: 发件人的名称 + smtp_host: + label: SMTP 主机 + msg: 不能为空 + text: 邮件服务器 + encryption: + label: 加密 + msg: 不能为空 + text: 对于大多数服务器而言,SSL 是推荐开启的。 + ssl: SSL + none: 无加密 + smtp_port: + label: SMTP 端口 + msg: SMTP 端口必须在 1 ~ 65535 之间。 + text: 邮件服务器的端口号。 + smtp_username: + label: SMTP 用户名 + msg: 不能为空 + smtp_password: + label: SMTP 密码 + msg: 不能为空 + test_email_recipient: + label: 测试邮件收件人 + text: 提供用于接收测试邮件的邮箱地址。 + msg: 地址无效 + smtp_authentication: + label: SMTP 认证 + msg: 不能为空 + 'yes': 是 + 'no': 否 diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index d42b6058..2d305bb2 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -12,13 +12,12 @@ import { interfaceStore } from '@/stores'; import { UploadImg } from '@/components'; import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; import { - getAdminLanguageOptions, uploadAvatar, updateInterfaceSetting, useInterfaceSetting, useThemeOptions, } from '@/services'; -import { setupAppLanguage } from '@/utils/localize'; +import { setupAppLanguage, loadLanguageOptions } from '@/utils/localize'; const Interface: FC = () => { const { t } = useTranslation('translation', { @@ -53,7 +52,7 @@ const Interface: FC = () => { }, }); const getLangs = async () => { - const res: LangsType[] = await getAdminLanguageOptions(); + const res: LangsType[] = await loadLanguageOptions(true); setLangs(res); }; // set default theme value diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index fb78f608..7d60dea8 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormDataType } from '@/common/interface'; import { useToast } from '@/hooks'; -import { getLanguageOptions, updateUserInterface } from '@/services'; +import { updateUserInterface } from '@/services'; import { localize } from '@/utils'; import { loggedUserInfoStore } from '@/stores'; @@ -24,7 +24,7 @@ const Index = () => { }); const getLangs = async () => { - const res: LangsType[] = await getLanguageOptions(); + const res: LangsType[] = await localize.loadLanguageOptions(); setLangs(res); }; diff --git a/ui/src/react-app-env.d.ts b/ui/src/react-app-env.d.ts index 6431bc5f..c40b86b3 100644 --- a/ui/src/react-app-env.d.ts +++ b/ui/src/react-app-env.d.ts @@ -1 +1,2 @@ /// +declare module '*.yaml'; diff --git a/ui/src/services/client/settings.ts b/ui/src/services/client/settings.ts index a47262eb..27fbe1a9 100644 --- a/ui/src/services/client/settings.ts +++ b/ui/src/services/client/settings.ts @@ -1,9 +1,7 @@ -// import useSWR from 'swr'; - import request from '@/utils/request'; import type * as Type from '@/common/interface'; -export const loadLang = () => { +export const getLanguageConfig = () => { return request.get('/answer/api/v1/language/config'); }; diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index dcc2d084..31bb0a40 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -85,6 +85,7 @@ function formatUptime(value) { return `< 1 ${t('dates.hour')}`; } + export { getQueryString, thousandthDivision, diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts index 0c6313b6..a9260905 100644 --- a/ui/src/utils/localize.ts +++ b/ui/src/utils/localize.ts @@ -2,10 +2,68 @@ import dayjs from 'dayjs'; import i18next from 'i18next'; import { interfaceStore, loggedUserInfoStore } from '@/stores'; -import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; +import { + CURRENT_LANG_STORAGE_KEY, + DEFAULT_LANG, + LANG_RESOURCE_STORAGE_KEY, +} from '@/common/constants'; import { Storage } from '@/utils'; +import { + getAdminLanguageOptions, + getLanguageConfig, + getLanguageOptions, +} from '@/services'; -const localDayjs = (langName) => { +export const loadLanguageOptions = async (forAdmin = false) => { + const languageOptions = forAdmin + ? await getAdminLanguageOptions() + : await getLanguageOptions(); + if (process.env.NODE_ENV === 'development') { + const { default: optConf } = await import('@/i18n/locales/i18n.yaml'); + optConf?.language_options.forEach((opt) => { + if (!languageOptions.find((_) => opt.label === _.label)) { + languageOptions.push(opt); + } + }); + } + return languageOptions; +}; + +const addI18nResource = async (langName) => { + const res = { lng: langName, resources: undefined }; + if (process.env.NODE_ENV === 'development') { + try { + const { default: resConf } = await import( + `@/i18n/locales/${langName}.yaml` + ); + res.resources = resConf.ui; + } catch (ex) { + console.log('ex: ', ex); + } + } else { + const storageResource = Storage.get(LANG_RESOURCE_STORAGE_KEY); + if (storageResource?.lng === res.lng) { + res.resources = storageResource.resources; + } else { + const langConf = await getLanguageConfig(); + if (langConf) { + res.resources = langConf; + } + } + } + if (res.resources) { + i18next.addResourceBundle( + res.lng, + 'translation', + res.resources, + true, + true, + ); + Storage.set(LANG_RESOURCE_STORAGE_KEY, res); + } +}; + +const localeDayjs = (langName) => { langName = langName.replace('_', '-').toLowerCase(); dayjs.locale(langName); }; @@ -13,19 +71,22 @@ const localDayjs = (langName) => { export const getCurrentLang = () => { const loggedUser = loggedUserInfoStore.getState().user; const adminInterface = interfaceStore.getState().interface; - const storageLang = Storage.get(CURRENT_LANG_STORAGE_KEY); + const fallbackLang = Storage.get(CURRENT_LANG_STORAGE_KEY) || DEFAULT_LANG; let currentLang = loggedUser.language; // `default` mean use language value from admin interface - if (/default/i.test(currentLang) && adminInterface.language) { + if (/default/i.test(currentLang)) { currentLang = adminInterface.language; } - currentLang ||= storageLang || DEFAULT_LANG; + currentLang ||= fallbackLang; return currentLang; }; -export const setupAppLanguage = () => { +export const setupAppLanguage = async () => { const lang = getCurrentLang(); - localDayjs(lang); + if (!i18next.getDataByLanguage(lang)) { + await addI18nResource(lang); + } + localeDayjs(lang); i18next.changeLanguage(lang); }; diff --git a/ui/tsconfig.json b/ui/tsconfig.json index f270b720..000cc227 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -23,5 +23,5 @@ "@/*": ["src/*"] } }, - "include": ["src", "node_modules/@testing-library/jest-dom"] + "include": ["src", "node_modules/@testing-library/jest-dom", "scripts"] } From 31d6a980416534c93b8084dfe304ec77b89ad2ff Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 8 Nov 2022 17:44:32 +0800 Subject: [PATCH 108/157] feat(i18n): merge i18n files done! --- i18n/en_US.yaml | 947 ++++++++++++++++++++++++++++++++++++++++++++++++ i18n/zh_CN.yaml | 747 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 1694 insertions(+) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 824c92e6..27300e92 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -170,3 +170,950 @@ notification: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + description: >- +
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by + placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Webite Maintenance + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: 'Your account was suspended until {{ time }}.' + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4MB. + description: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + description: 'Must use the character set "a-z", "0-9", "+ # - ."' + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + description: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allowed deleting tag with posts.

Please remove this tag + from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: 'Must use the character set "a-z", "0-9", "+ # - ."' + description: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: 'MMM D, YYYY' + long_date_with_time: 'MMM D, YYYY [at] HH:mm' + now: now + x_seconds_ago: '{{count}}s ago' + x_minutes_ago: '{{count}}m ago' + x_hours_ago: '{{count}}h ago' + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid + answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are + adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_description: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: 'Describe what your question is about, at least one tag is required.' + no_result: No tags matched + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. + Please follow the instructions in the mail to activate your account. + info: 'If it doesn''t arrive, check your spam folder.' + another: >- + We sent another activation email to you at {{mail}}. It might + take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to Answer + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name up to 30 characters. + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in + page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is + already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + btn_name: Update profile + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: Default + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: 'https://example.com' + msg: Website incorrect format + location: + label: Location (optional) + placeholder: 'City, Country' + notification: + email: + label: Email Notifications + radio: 'Answers to your questions, comments, and more' + account: + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation + instructions. + email: + label: Email + msg: Email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the + edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because + doing so deprives future readers of this knowledge.

Repeated deletion + of answered questions can result in your account being blocked from asking. + Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because + doing so deprives future readers of this knowledge.

Repeated deletion + of accepted answers can result in your account being blocked from answering. + Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: '{{count}} Results' + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + tips: + title: Advanced Search Tips + tag: '<1>[tag] search withing a tag' + user: '<1>user:username search by author' + answer: '<1>answers:0 unanswered questions' + score: '<1>score:3 posts with a 3+ score' + question: '<1>is:question search questions' + is_answer: '<1>is:answer search answers' + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to Twitter + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your + account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was + already changed? + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: '{{ count }} Questions' + x_answers: '{{ count }} answers' + questions: Questions + answers: Answers + newest: Newest + active: Active + frequent: Frequent + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: '// Hello, World !' + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Answer + next: Next + done: Done + config_yaml_error: Can’t create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: 'db:3306' + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + description: >- + You can create the <1>config.yaml file manually in the + <1>/var/wwww/xxx/ directory and paste the following text into it. + info: 'After you’ve done that, click “Next” button.' + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure + location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your Answer is Ready! + ready_description: >- + If you ever feel like changing more settings, visit <1>admin section; + find it in the site menu. + good_luck: 'Have fun, and good luck!' + warning: Warning + warning_description: >- + The file <1>config.yaml already exists. If you need to reset any of the + configuration items in this file, please delete it first. You may try + <2>installing now. + installed: Already installed + installed_description: >- + You appear to have already installed. To reinstall please clear your old + database tables first. + upgrade: + title: Answer + update_btn: Update data + update_title: Data update required + update_description: >- + <1>Answer has been updated! Before you continue, we have to update your data + to the newest version.<1>The update process may take a little while, so + please be patient. + done_title: No update required + done_btn: Done + done_desscription: Your Answer data is already up-to-date. + page_404: + description: 'Unfortunately, this page doesn''t exist.' + back_home: Back to homepage + page_50X: + description: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + description: 'We are under maintenance, we’ll be back soon.' + admin: + admin_header: + title: Admin + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + dashboard: + title: Dashboard + welcome: Welcome to Answer Admin ! + site_statistics: Site Statistics + questions: 'Questions:' + answers: 'Answers:' + comments: 'Comments:' + votes: 'Votes:' + active_users: 'Active users:' + flags: 'Flags:' + site_health_status: Site Health Status + version: 'Version:' + https: 'HTTPS:' + uploading_files: 'Uploading files:' + smtp: 'SMTP:' + timezone: 'Timezone:' + system_info: System Info + storage_used: 'Storage used:' + uptime: 'Uptime:' + answer_links: Answer Links + documents: Documents + feedback: Feedback + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + 'yes': 'Yes' + 'no': 'No' + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_description: A normal user can ask and answer questions. + suspended_name: suspended + suspended_description: A suspended user can't log in. + deleted_name: deleted + deleted_description: 'Delete profile, authentication associations.' + inactive_name: inactive + inactive_description: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: 'Change {{ type }} status to...' + normal_name: normal + normal_description: A normal post available to everyone. + closed_name: closed + closed_description: 'A closed question can''t answer, but still can edit, vote and comment.' + deleted_name: deleted + deleted_description: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + action: Action + change: Change + all: All + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + filter: + placeholder: 'Filter by name, user:id' + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, question:id' + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, answer:id' + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: 'The name of this site, as used in the title tag.' + site_url: + label: Site URL + msg: Site url cannot be empty. + text: The address of your site. + short_description: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: 'Short description, as used in the title tag on homepage.' + description: + label: Site Description (optional) + msg: Site description cannot be empty. + text: 'Describe this site in one sentence, as used in the meta description tag.' + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a UTC (Coordinated Universal Time) time offset. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: SMTP Authentication + msg: SMTP authentication cannot be empty. + 'yes': 'Yes' + 'no': 'No' diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 7d76cc06..34fd9dc9 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -170,3 +170,750 @@ notification: other: "你的答案已被删除" your_comment_was_deleted: other: "你的评论已被删除" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何设定文本格式 + description: >- +
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 + **粗体**

  • 使用 4 + 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 + `像 _这样_`

  • 使用```创建代码块

    ```
    // + 这是代码
    ```
+ pagination: + prev: 上一页 + next: 下一页 + page_title: + question: 问题 + questions: 问题 + tag: 标签 + tags: 标签 + tag_wiki: 标签 wiki + edit_tag: 编辑标签 + ask_a_question: 提问题 + edit_question: 编辑问题 + edit_answer: 编辑回答 + search: 搜索 + posts_containing: 包含 + settings: 设定 + notifications: 通知 + login: 登录 + sign_up: 注册 + account_recovery: 账号恢复 + account_activation: 账号激活 + confirm_email: 确认电子邮件 + account_suspended: 账号已封禁 + admin: 后台管理 + notifications: + title: 通知 + inbox: 收件箱 + achievement: 成就 + all_read: 全部标记为已读 + show_more: 显示更多 + suspended: + title: 账号已封禁 + until_time: '你的账号被封禁至{{ time }}。' + forever: 你的账号已被永久封禁。 + end: 违反了我们的社区准则。 + editor: + blockquote: + text: 引用 + bold: + text: 粗体 + chart: + text: 图表 + flow_chart: 流程图 + sequence_diagram: 时序图 + class_diagram: 类图 + state_diagram: 状态图 + entity_relationship_diagram: ER 图 + user_defined_diagram: User defined diagram + gantt_chart: 甘特图 + pie_chart: 饼图 + code: + text: 代码块 + add_code: 添加代码块 + form: + fields: + code: + label: 代码块 + msg: + empty: 代码块不能为空 + language: + label: 语言 (可选) + placeholder: 自动识别 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 行内公式 + block: 公式块 + heading: + text: 标题 + options: + h1: 标题 1 + h2: 标题 2 + h3: 标题 3 + h4: 标题 4 + h5: 标题 5 + h6: 标题 6 + help: + text: 帮助 + hr: + text: 水平分割线 + image: + text: 图片 + add_image: 添加图片 + tab_image: 上传图片 + form_image: + fields: + file: + label: 图片文件 + btn: 选择图片 + msg: + empty: 请选择图片文件。 + only_image: 只能上传图片文件。 + max_size: 图片文件大小不能超过 4 MB。 + description: + label: 图片描述(可选) + tab_url: 网络图片 + form_url: + fields: + url: + label: 图片地址 + msg: + empty: 图片地址不能为空 + name: + label: 图片描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上传中... + indent: + text: 添加缩进 + outdent: + text: 减少缩进 + italic: + text: 斜体 + link: + text: 超链接 + add_link: 添加超链接 + form: + fields: + url: + label: 链接 + msg: + empty: 链接不能为空。 + name: + label: 链接描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: 有编号列表 + unordered_list: + text: 无编号列表 + table: + text: 表格 + heading: 表头 + cell: 单元格 + close_modal: + title: 关闭原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空。 + msg: + empty: 请选择一个原因。 + report_modal: + flag_title: 举报原因是... + close_title: 关闭原因是... + review_question_title: 审查问题 + review_answer_title: 审查回答 + review_comment_title: 审查评论 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空 + msg: + empty: 请选择一个原因。 + tag_modal: + title: 创建新标签 + form: + fields: + display_name: + label: 显示名称(别名) + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + slug_name: + label: URL 固定链接 + description: '必须由 "a-z", "0-9", "+ # - ." 组成' + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + character: 包含非法字符 + description: + label: 标签描述(可选) + btn_cancel: 取消 + btn_submit: 提交 + tag_info: + created_at: 创建于 + edited_at: 编辑于 + synonyms: + title: 同义词 + text: 以下标签等同于 + empty: 此标签目前没有同义词。 + btn_add: 添加同义词 + btn_edit: 编辑 + btn_save: 保存 + synonyms_text: 以下标签等同于 + delete: + title: 删除标签 + content:

不允许删除有关联问题的标签。

请先从关联的问题中删除此标签的引用。

+ content2: 确定要删除吗? + close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + form: + fields: + revision: + label: 编辑历史 + display_name: + label: 名称 + slug_name: + label: URL 固定链接 + info: '必须由 "a-z", "0-9", "+ # - ." 组成' + description: + label: 描述 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: YYYY年MM月 + long_date_with_year: YYYY年MM月DD日 + long_date_with_time: 'YYYY年MM月DD日 HH:mm' + now: 刚刚 + x_seconds_ago: '{{count}} 秒前' + x_minutes_ago: '{{count}} 分钟前' + x_hours_ago: '{{count}} 小时前' + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存 + btn_cancel: 取消 + show_more: 显示更多评论 + tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。 + tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名过滤 + no_description: 此标签无描述。 + more: 更多 + ask: + title: 提交新的问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + similar_questions: 相似的问题 + form: + fields: + revision: + label: 编辑历史 + title: + label: 标题 + placeholder: 请详细描述你的问题 + msg: + empty: 标题不能为空 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 直接发表回答 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: 选择至少一个与问题相关的标签。 + no_result: 没有匹配的标签 + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + profile: 用户主页 + setting: 账号设置 + logout: 退出登录 + admin: 后台管理 + search: + placeholder: 搜索 + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: 更改图片 + loading: 加载中... + pic_auth_code: + title: 验证码 + placeholder: 输入图片中的文字 + msg: + empty: 不能为空 + inactive: + first: '马上就好了!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活您的帐户。' + info: 如果没有收到,请检查您的垃圾邮件文件夹。 + another: '我们向您发送了另一封激活电子邮件,地址为 {{mail}}。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。' + btn_name: 重新发送激活邮件 + msg: + empty: 不能为空 + login: + page_title: 欢迎来到 Answer + info_sign: 没有帐户?<1>注册 + info_login: 已经有一个帐户?<1>登录 + forgot_pass: 忘记密码? + name: + label: 昵称 + msg: + empty: 昵称不能为空 + range: 昵称最多 30 个字符 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password: + label: 密码 + msg: + empty: 密码不能为空 + different: 两次输入密码不一致 + account_forgot: + page_title: 忘记密码 + btn_name: 发送恢复邮件 + send_success: '如无意外,你的邮箱 {{mail}} 将会收到一封重置密码的邮件,请根据指引重置你的密码。' + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password_reset: + page_title: 密码重置 + btn_name: 重置我的密码 + reset_success: 你已经成功更改密码,将返回登录页面 + link_invalid: 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? + to_login: 前往登录页面 + password: + label: 密码 + msg: + empty: 密码不能为空 + length: 密码长度在8-32个字符之间 + different: 两次输入密码不一致 + password_confirm: + label: 确认新密码 + settings: + page_title: 设置 + nav: + profile: 我的资料 + notification: 通知 + account: 账号 + interface: 界面 + profile: + btn_name: 保存更改 + display_name: + label: 昵称 + msg: 昵称不能为空 + msg_range: 昵称不能超过 30 个字符 + username: + label: 用户名 + caption: 用户之间可以通过 "@用户名" 进行交互。 + msg: 用户名不能为空 + msg_range: 用户名不能超过 30 个字符 + character: '用户名只能由 "a-z", "0-9", " - . _" 组成' + avatar: + label: 头像 + text: 您可以上传图片作为头像,也可以 <1>重置 为 + bio: + label: 关于我 (可选) + website: + label: 网站 (可选) + placeholder: 'https://example.com' + msg: 格式不正确 + location: + label: 位置 (可选) + placeholder: '城市, 国家' + notification: + email: + label: 邮件通知 + radio: 你的提问有新的回答,评论,和其他 + account: + change_email_btn: 更改邮箱 + change_pass_btn: 更改密码 + change_email_info: 邮件已发送。请根据指引完成验证。 + email: + label: 邮箱 + msg: 邮箱不能为空 + password_title: 密码 + current_pass: + label: 当前密码 + msg: + empty: 当前密码不能为空 + length: 密码长度必须在 8 至 32 之间 + different: 两次输入的密码不匹配 + new_pass: + label: 新密码 + pass_confirm: + label: 确认新密码 + interface: + lang: + label: 界面语言 + text: 设置用户界面语言,在刷新页面后生效。 + toast: + update: 更新成功 + update_password: 更改密码成功。 + flag_success: 感谢您的标记,我们会尽快处理。 + related_question: + title: 相关问题 + btn: 我要提问 + answers: 个回答 + question_detail: + Asked: 提问于 + asked: 提问于 + update: 修改于 + edit: 最后编辑于 + Views: 阅读次数 + Follow: 关注此问题 + Following: 已关注 + answered: 回答于 + closed_in: 关闭于 + show_exist: 查看相关问题。 + answers: + title: 个回答 + score: 评分 + newest: 最新 + btn_accept: 采纳 + btn_accepted: 已被采纳 + write_answer: + title: 你的回答 + btn_name: 提交你的回答 + confirm_title: 继续回答 + continue: 继续 + confirm_info:

您确定要提交一个新的回答吗?

您可以直接编辑和改善您之前的回答的。

+ empty: 回答内容不能为空。 + delete: + title: 删除 + question: >- + 我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_question_deleted: 此问题已被删除 + tip_answer_deleted: 此回答已被删除 + btns: + confirm: 确认 + cancel: 取消 + save: 保存 + delete: 删除 + login: 登录 + signup: 注册 + logout: 退出登录 + verify: 验证 + add_question: 我要提问 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: '{{count}} 个结果' + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + tips: + title: 高级搜索提示 + tag: '<1>[tag] 在指定标签中搜索' + user: '<1>user:username 根据作者搜索' + answer: '<1>answers:0 搜索未回答的问题' + score: '<1>score:3 评分 3 分或以上' + question: '<1>is:question 只搜索问题' + is_answer: '<1>is:answer 只搜索回答' + empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 + share: + name: 分享 + copy: 复制链接 + via: 分享在... + copied: 已复制 + facebook: 分享到 Facebook + twitter: 分享到 Twitter + cannot_vote_for_self: 不能给自己投票 + modal_confirm: + title: 发生错误... + account_result: + page_title: 欢迎来到 Answer + success: 你的账号已通过验证,即将返回首页。 + link: 返回首页 + invalid: 抱歉,此验证链接已失效。也许是你的账号已经通过验证了? + confirm_new_email: 你的电子邮箱已更新 + confirm_new_email_invalid: 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? + question: + following_tags: 已关注的标签 + edit: 编辑 + save: 保存 + follow_tag_tip: 按照标签整理您的问题列表。 + hot_questions: 热点问题 + all_questions: 全部问题 + x_questions: '{{ count }} 个问题' + x_answers: '{{ count }} 个回答' + questions: 个问题 + answers: 回答 + newest: 最新 + active: 活跃 + frequent: 浏览量 + score: 评分 + unanswered: 未回答 + modified: 修改于 + answered: 回答于 + asked: 提问于 + closed: 已关闭 + follow_a_tag: 关注一个标签 + more: 更多 + personal: + overview: 概览 + answers: 回答 + answer: 回答 + questions: 问题 + question: 问题 + bookmarks: 收藏 + reputation: 声望 + comments: 评论 + votes: 得票 + newest: 最新 + score: 评分 + edit_profile: 编辑我的资料 + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: 加入于 + last_login: 上次登录 + about_me: 关于我 + about_me_empty: '// Hello, World !' + top_answers: 热门回答 + top_questions: 热门问题 + stats: 状态 + list_empty: 没有找到相关的内容。
试试看其他标签? + accepted: 已采纳 + answered: 回答于 + asked: 提问于 + upvote: 赞同 + downvote: 反对 + mod_short: 管理员 + mod_long: 管理员 + x_reputation: 声望 + x_votes: 得票 + x_answers: 个回答 + x_questions: 个问题 + page_404: + description: 页面不存在 + back_home: 回到主页 + page_50X: + description: 服务器遇到了一个错误,无法完成你的请求。 + back_home: 回到主页 + admin: + admin_header: + title: 后台管理 + nav_menus: + dashboard: 后台管理 + contents: 内容管理 + questions: 问题 + answers: 回答 + users: 用户管理 + flags: 举报管理 + settings: 站点设置 + general: 一般 + interface: 界面 + smtp: SMTP + dashboard: + title: 后台管理 + welcome: 欢迎来到 Answer 后台管理! + version: 版本 + flags: + title: 举报 + pending: 等待处理 + completed: 已完成 + flagged: 被举报内容 + created: 创建于 + action: 操作 + review: 审查 + change_modal: + title: 更改用户状态为... + btn_cancel: 取消 + btn_submit: 提交 + normal_name: 正常 + normal_description: 正常状态的用户可以提问和回答。 + suspended_name: 封禁 + suspended_description: 被封禁的用户将无法登录。 + deleted_name: 删除 + deleted_description: 删除用户的个人信息,认证等等。 + inactive_name: 不活跃 + inactive_description: 不活跃的用户必须重新验证邮箱。 + confirm_title: 删除此用户 + confirm_content: 确定要删除此用户?此操作无法撤销! + confirm_btn: 删除 + msg: + empty: 请选择一个原因 + status_modal: + title: '更改 {{ type }} 状态为...' + normal_name: 正常 + normal_description: 所有用户都可以访问 + closed_name: 关闭 + closed_description: 不能回答,但仍然可以编辑、投票和评论。 + deleted_name: 删除 + deleted_description: 所有获得/损失的声望将会恢复。 + btn_cancel: 取消 + btn_submit: 提交 + btn_next: 下一步 + users: + title: 用户 + name: 名称 + email: 邮箱 + reputation: 声望 + created_at: 创建时间 + delete_at: 删除时间 + suspend_at: 封禁时间 + status: 状态 + action: 操作 + change: 更改 + all: 全部 + inactive: 不活跃 + suspended: 已封禁 + deleted: 已删除 + normal: 正常 + questions: + page_title: 问题 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + post: 标题 + votes: 得票数 + answers: 回答数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + answers: + page_title: 回答 + normal: 正常 + deleted: 已删除 + post: 标题 + votes: 得票数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + general: + page_title: 一般 + name: + label: 站点名称 + msg: 不能为空 + text: 站点的名称,作为站点的标题(HTML 的 title 标签)。 + short_description: + label: 简短的站点标语 (可选) + msg: 不能为空 + text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。 + description: + label: 网站描述 (可选) + msg: 不能为空 + text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。 + interface: + page_title: 界面 + logo: + label: Logo (可选) + msg: 不能为空 + text: 可以上传图片,或者<1>重置为站点标题。 + theme: + label: 主题 + msg: 不能为空 + text: 选择一个主题 + language: + label: 界面语言 + msg: 不能为空 + text: 设置用户界面语言,在刷新页面后生效。 + smtp: + page_title: SMTP + from_email: + label: 发件人地址 + msg: 不能为空 + text: 用于发送邮件的地址。 + from_name: + label: 发件人名称 + msg: 不能为空 + text: 发件人的名称 + smtp_host: + label: SMTP 主机 + msg: 不能为空 + text: 邮件服务器 + encryption: + label: 加密 + msg: 不能为空 + text: 对于大多数服务器而言,SSL 是推荐开启的。 + ssl: SSL + none: 无加密 + smtp_port: + label: SMTP 端口 + msg: SMTP 端口必须在 1 ~ 65535 之间。 + text: 邮件服务器的端口号。 + smtp_username: + label: SMTP 用户名 + msg: 不能为空 + smtp_password: + label: SMTP 密码 + msg: 不能为空 + test_email_recipient: + label: 测试邮件收件人 + text: 提供用于接收测试邮件的邮箱地址。 + msg: 地址无效 + smtp_authentication: + label: SMTP 认证 + msg: 不能为空 + 'yes': 是 + 'no': 否 From 5952114feb24965f92eec19f11e9d441ba0209c0 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 8 Nov 2022 18:10:51 +0800 Subject: [PATCH 109/157] refactor(i18n): remove fs.existsSync in resolvePresetLocales --- ui/scripts/i18n-locale-tool.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ui/scripts/i18n-locale-tool.js b/ui/scripts/i18n-locale-tool.js index 456b117b..05d55a50 100644 --- a/ui/scripts/i18n-locale-tool.js +++ b/ui/scripts/i18n-locale-tool.js @@ -46,10 +46,7 @@ const autoSync = () => { const resolvePresetLocales = () => { PRESET_LANG.forEach((lng) => { const sp = path.resolve(SRC_PATH, `${lng}.yaml`); - const tp = path.resolve(DEST_PATH, `${lng}.yaml`); - if (fs.existsSync(tp) === false) { - copyLocaleFile(sp); - } + copyLocaleFile(sp); }); }; From 8dda61d6855af817f809cc98fee30122ad12612d Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 18:17:08 +0800 Subject: [PATCH 110/157] fix: install page add title and install page forms add default value --- ui/src/components/PageTitle/index.tsx | 2 +- ui/src/i18n/locales/en_US.yaml | 49 ++++++++----------- ui/src/i18n/locales/zh_CN.yaml | 25 +++++----- .../Install/components/SecondStep/index.tsx | 2 - ui/src/pages/Install/index.tsx | 8 +-- ui/src/utils/guard.ts | 1 - 6 files changed, 38 insertions(+), 49 deletions(-) diff --git a/ui/src/components/PageTitle/index.tsx b/ui/src/components/PageTitle/index.tsx index a11ea992..a581af2e 100644 --- a/ui/src/components/PageTitle/index.tsx +++ b/ui/src/components/PageTitle/index.tsx @@ -18,7 +18,7 @@ const PageTitle: FC = ({ title = '', suffix = '' }) => { if (!suffix) { suffix = `${siteInfo.name}`; } - title = title ? `${title} - ${suffix}` : suffix; + title = title ? `${title}${suffix ? ` - ${suffix}` : ''}` : suffix; return <>{setPageTitle(title)}; }; diff --git a/ui/src/i18n/locales/en_US.yaml b/ui/src/i18n/locales/en_US.yaml index 27300e92..9f2a06ab 100644 --- a/ui/src/i18n/locales/en_US.yaml +++ b/ui/src/i18n/locales/en_US.yaml @@ -173,17 +173,17 @@ notification: # The following fields are used for interface presentation(Front-end) ui: how_to_format: - title: How to Format - description: >- -
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by - placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ title: How to Format + description: >- +
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by + placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next @@ -816,7 +816,7 @@ ui: done: Done config_yaml_error: Can’t create the config.yaml file. lang: - label: Please choose a language + label: Please Choose a Language db_type: label: Database Engine db_username: @@ -883,26 +883,15 @@ ui: If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: 'Have fun, and good luck!' - warning: Warning - warning_description: >- + warn_title: Warning + warn_description: >- The file <1>config.yaml already exists. If you need to reset any of the - configuration items in this file, please delete it first. You may try - <2>installing now. + configuration items in this file, please delete it first. + install_now: You may try <1>installing now. installed: Already installed installed_description: >- You appear to have already installed. To reinstall please clear your old database tables first. - upgrade: - title: Answer - update_btn: Update data - update_title: Data update required - update_description: >- - <1>Answer has been updated! Before you continue, we have to update your data - to the newest version.<1>The update process may take a little while, so - please be patient. - done_title: No update required - done_btn: Done - done_desscription: Your Answer data is already up-to-date. page_404: description: 'Unfortunately, this page doesn''t exist.' back_home: Back to homepage @@ -927,7 +916,7 @@ ui: smtp: SMTP dashboard: title: Dashboard - welcome: Welcome to Answer Admin ! + welcome: Welcome to Answer Admin! site_statistics: Site Statistics questions: 'Questions:' answers: 'Answers:' @@ -1047,6 +1036,7 @@ ui: site_url: label: Site URL msg: Site url cannot be empty. + validate: Please enter a valid URL. text: The address of your site. short_description: label: Short Site Description (optional) @@ -1059,6 +1049,7 @@ ui: contact_email: label: Contact Email msg: Contact email cannot be empty. + validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface @@ -1077,7 +1068,7 @@ ui: time_zone: label: Timezone msg: Timezone cannot be empty. - text: Choose a UTC (Coordinated Universal Time) time offset. + text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: diff --git a/ui/src/i18n/locales/zh_CN.yaml b/ui/src/i18n/locales/zh_CN.yaml index 34fd9dc9..a02f4219 100644 --- a/ui/src/i18n/locales/zh_CN.yaml +++ b/ui/src/i18n/locales/zh_CN.yaml @@ -173,17 +173,17 @@ notification: # The following fields are used for interface presentation(Front-end) ui: how_to_format: - title: 如何设定文本格式 - description: >- -
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 - **粗体**

  • 使用 4 - 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 - `像 _这样_`

  • 使用```创建代码块

    ```
    // - 这是代码
    ```
+ title: 如何设定文本格式 + description: >- +
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 + **粗体**

  • 使用 4 + 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 + `像 _这样_`

  • 使用```创建代码块

    ```
    // + 这是代码
    ```
pagination: prev: 上一页 next: 下一页 @@ -396,7 +396,7 @@ ui: btn_save_edits: 保存更改 btn_cancel: 取消 dates: - long_date: YYYY年MM月 + long_date: MM月DD日 long_date_with_year: YYYY年MM月DD日 long_date_with_time: 'YYYY年MM月DD日 HH:mm' now: 刚刚 @@ -917,3 +917,4 @@ ui: msg: 不能为空 'yes': 是 'no': 否 + diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx index 30e48930..6c7125f8 100644 --- a/ui/src/pages/Install/components/SecondStep/index.tsx +++ b/ui/src/pages/Install/components/SecondStep/index.tsx @@ -148,8 +148,6 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {t('db_password.label')} { diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 84a350bb..ae8f037b 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -43,22 +43,22 @@ const Index: FC = () => { errorMsg: '', }, db_username: { - value: '', + value: 'root', isInvalid: false, errorMsg: '', }, db_password: { - value: '', + value: 'root', isInvalid: false, errorMsg: '', }, db_host: { - value: '', + value: 'db:3306', isInvalid: false, errorMsg: '', }, db_name: { - value: '', + value: 'answer', isInvalid: false, errorMsg: '', }, diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index 7f926fba..4fb21496 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -185,7 +185,6 @@ export const tryNormalLogged = (canNavigate: boolean = false) => { export const tryLoggedAndActicevated = () => { const gr: TGuardResult = { ok: true }; const us = deriveLoginState(); - console.log('tryLogged', us); if (!us.isLogged || !us.isActivated) { gr.ok = false; } From b474d3ade30a8cde24a1b11f37d358d56e3a3741 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Tue, 8 Nov 2022 18:28:15 +0800 Subject: [PATCH 111/157] feat: set install router in release mode. remove install mock router. --- internal/install/install_main.go | 2 + internal/install/install_server.go | 2 +- internal/router/ui.go | 67 ------------------------------ 3 files changed, 3 insertions(+), 68 deletions(-) diff --git a/internal/install/install_main.go b/internal/install/install_main.go index a961c280..6586b9d3 100644 --- a/internal/install/install_main.go +++ b/internal/install/install_main.go @@ -1,6 +1,7 @@ package install import ( + "fmt" "os" "github.com/answerdev/answer/internal/base/translator" @@ -24,6 +25,7 @@ func Run(configPath string) { if len(port) == 0 { port = "80" } + fmt.Printf("[SUCCESS] answer installation service will run at: http://localhost:%s/install/ \n", port) if err = installServer.Run(":" + port); err != nil { panic(err) } diff --git a/internal/install/install_server.go b/internal/install/install_server.go index a301c197..b2ada51e 100644 --- a/internal/install/install_server.go +++ b/internal/install/install_server.go @@ -26,8 +26,8 @@ func (r *_resource) Open(name string) (fs.File, error) { // NewInstallHTTPServer new install http server. func NewInstallHTTPServer() *gin.Engine { + gin.SetMode(gin.ReleaseMode) r := gin.New() - gin.SetMode(gin.DebugMode) r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") }) r.StaticFS("/static", http.FS(&_resource{ fs: ui.Build, diff --git a/internal/router/ui.go b/internal/router/ui.go index 84231b0c..3498265c 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -4,16 +4,11 @@ import ( "embed" "fmt" "io/fs" - "math/rand" "net/http" "os" - "github.com/answerdev/answer/i18n" - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" - "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -45,7 +40,6 @@ func (r *_resource) Open(name string) (fs.File, error) { // Register a new static resource which generated by ui directory func (a *UIRouter) Register(r *gin.Engine) { staticPath := os.Getenv("ANSWER_STATIC_PATH") - r.StaticFS("/i18n/", http.FS(i18n.I18n)) // if ANSWER_STATIC_PATH is set and not empty, ignore embed resource if staticPath != "" { @@ -72,67 +66,6 @@ func (a *UIRouter) Register(r *gin.Engine) { fs: ui.Build, })) - // Install godoc - // @Summary Install - // @Description Install - // @Tags Install - // @Accept json - // @Produce json - // @Success 200 {object} handler.RespBody{} - // @Router /install [get] - r.GET("/install", func(c *gin.Context) { - filePath := "" - var file []byte - var err error - filePath = "build/index.html" - c.Header("content-type", "text/html;charset=utf-8") - file, err = ui.Build.ReadFile(filePath) - if err != nil { - log.Error(err) - c.Status(http.StatusNotFound) - return - } - c.String(http.StatusOK, string(file)) - }) - - r.GET("/installation/language/options", func(c *gin.Context) { - handler.HandleResponse(c, nil, translator.LanguageOptions) - }) - - r.POST("/installation/db/check", func(c *gin.Context) { - num := rand.Intn(10) - if num > 5 { - err := errors.BadRequest("connection error") - handler.HandleResponse(c, err, gin.H{}) - } else { - handler.HandleResponse(c, nil, gin.H{ - "connection_success": true, - }) - } - }) - - r.POST("/installation/config-file/check", func(c *gin.Context) { - num := rand.Intn(10) - if num > 5 { - handler.HandleResponse(c, nil, gin.H{ - "exist": true, - }) - } else { - handler.HandleResponse(c, nil, gin.H{ - "exist": false, - }) - } - - }) - - r.POST("/installation/init", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) - - r.POST("/installation/base-info", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) - // specify the not router for default routes and redirect r.NoRoute(func(c *gin.Context) { name := c.Request.URL.Path From 6626f966145a4ce82ca56704c5a0fd88b37d8939 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 10:22:52 +0800 Subject: [PATCH 112/157] fix: style adjustment --- ui/src/components/QueryGroup/index.tsx | 6 +++-- ui/src/components/QuestionList/index.tsx | 32 +++++++++++------------- ui/src/index.scss | 5 ++++ 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/ui/src/components/QueryGroup/index.tsx b/ui/src/components/QueryGroup/index.tsx index 15c3ad49..f69e3596 100644 --- a/ui/src/components/QueryGroup/index.tsx +++ b/ui/src/components/QueryGroup/index.tsx @@ -12,6 +12,7 @@ interface Props { sortKey?: string; className?: string; pathname?: string; + wrapClassName?: string; } const MAX_BUTTON_COUNT = 3; const Index: FC = ({ @@ -21,6 +22,7 @@ const Index: FC = ({ i18nKeyPrefix = '', className = '', pathname = '', + wrapClassName = '', }) => { const [searchParams, setUrlSearchParams] = useSearchParams(); const navigate = useNavigate(); @@ -51,7 +53,7 @@ const Index: FC = ({ return (typeof btn === 'string' ? btn : btn.name) === currentSort; }); return ( - + {data.map((btn, index) => { const key = typeof btn === 'string' ? btn : btn.sort; const name = typeof btn === 'string' ? btn : btn.name; @@ -62,7 +64,7 @@ const Index: FC = ({ variant="outline-secondary" active={currentSort === name} className={classNames( - 'text-capitalize', + 'text-capitalize fit-content', data.length > MAX_BUTTON_COUNT && index > MAX_BUTTON_COUNT - 2 && 'd-none d-md-block', diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 469a6e58..a45cd401 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { Row, Col, ListGroup } from 'react-bootstrap'; +import { ListGroup } from 'react-bootstrap'; import { NavLink, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -103,23 +103,19 @@ const QuestionList: FC = ({ source }) => { return (
- -
-
- {source === 'questions' - ? t('all_questions') - : t('x_questions', { count })} -
- - - - - +
+
+ {source === 'questions' + ? t('all_questions') + : t('x_questions', { count })} +
+ +
{listData?.list?.map((li) => { return ( diff --git a/ui/src/index.scss b/ui/src/index.scss index b225a924..097dafa0 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -141,6 +141,11 @@ a { background-color: #fff3cd80; } +.fit-content { + height: fit-content; + flex: none; +} + // fix bug for React-Bootstrap Form.Text .form-text { display: inline-block; From 38c8f80849f5fd6534c7474ebdb6a0b26c37ef55 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 10:24:27 +0800 Subject: [PATCH 113/157] feat: remove user status api --- internal/controller/user_controller.go | 16 -------------- internal/router/answer_api_router.go | 1 - internal/service/user_service.go | 29 -------------------------- 3 files changed, 46 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 028f9302..3d4d2d3a 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -89,22 +89,6 @@ func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// GetUserStatus get user status info -// @Summary get user status info -// @Description get user status info -// @Tags User -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Success 200 {object} handler.RespBody{data=schema.GetUserResp} -// @Router /answer/api/v1/user/status [get] -func (uc *UserController) GetUserStatus(ctx *gin.Context) { - userID := middleware.GetLoginUserIDFromContext(ctx) - token := middleware.ExtractToken(ctx) - resp, err := uc.userService.GetUserStatus(ctx, userID, token) - handler.HandleResponse(ctx, err, resp) -} - // UserEmailLogin godoc // @Summary UserEmailLogin // @Description UserEmailLogin diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 522eac09..fc7fab95 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -91,7 +91,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // user r.GET("/user/info", a.userController.GetUserInfoByUserID) - r.GET("/user/status", a.userController.GetUserStatus) r.GET("/user/action/record", a.userController.ActionRecord) r.POST("/user/login/email", a.userController.UserEmailLogin) r.POST("/user/register/email", a.userController.UserRegisterByEmail) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 76ce0d8a..07594c90 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -71,35 +71,6 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st return resp, nil } -// GetUserStatus get user info by user id -func (us *UserService) GetUserStatus(ctx context.Context, userID, token string) (resp *schema.GetUserStatusResp, err error) { - resp = &schema.GetUserStatusResp{} - if len(userID) == 0 { - return resp, nil - } - userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID) - if err != nil { - return nil, err - } - if !exist { - return nil, errors.BadRequest(reason.UserNotFound) - } - - userCacheInfo := &entity.UserCacheInfo{ - UserID: userID, - UserStatus: userInfo.Status, - EmailStatus: userInfo.MailStatus, - } - err = us.authService.UpdateUserCacheInfo(ctx, token, userCacheInfo) - if err != nil { - return nil, err - } - resp = &schema.GetUserStatusResp{ - Status: schema.UserStatusShow[userInfo.Status], - } - return resp, nil -} - func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) ( resp *schema.GetOtherUserInfoResp, err error, ) { From 6800cb43b06e7fae493383045fb039083232b46c Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 10:29:15 +0800 Subject: [PATCH 114/157] fix: add i18n key --- i18n/en_US.yaml | 1 + ui/src/i18n/locales/en_US.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 9f2a06ab..fa6a1de3 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -724,6 +724,7 @@ ui: newest: Newest active: Active score: Score + more: More tips: title: Advanced Search Tips tag: '<1>[tag] search withing a tag' diff --git a/ui/src/i18n/locales/en_US.yaml b/ui/src/i18n/locales/en_US.yaml index 9f2a06ab..fa6a1de3 100644 --- a/ui/src/i18n/locales/en_US.yaml +++ b/ui/src/i18n/locales/en_US.yaml @@ -724,6 +724,7 @@ ui: newest: Newest active: Active score: Score + more: More tips: title: Advanced Search Tips tag: '<1>[tag] search withing a tag' From 9509a9a964e3a278f389c48b511e740c8ef8d618 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 11:00:34 +0800 Subject: [PATCH 115/157] feat: change site url from config file to database --- internal/install/install_controller.go | 6 ------ .../service/service_config/service_config.go | 1 - internal/service/uploader/upload.go | 10 ++++++++-- internal/service/user_service.go | 18 ++++++++++++++---- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index c732f562..1bc0984f 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -156,12 +156,6 @@ func InitBaseInfo(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) return } - c.ServiceConfig.WebHost = req.SiteURL - if err := conf.RewriteConfig(confPath, c); err != nil { - log.Errorf("rewrite config failed %s", err) - handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) - return - } err = migrations.UpdateInstallInfo(c.Data.Database, req.Language, req.SiteName, req.SiteURL, req.ContactEmail, req.AdminName, req.AdminPassword, req.AdminEmail) diff --git a/internal/service/service_config/service_config.go b/internal/service/service_config/service_config.go index 92d46393..b5c1f56b 100644 --- a/internal/service/service_config/service_config.go +++ b/internal/service/service_config/service_config.go @@ -2,6 +2,5 @@ package service_config type ServiceConfig struct { SecretKey string `json:"secret_key" mapstructure:"secret_key" yaml:"secret_key"` - WebHost string `json:"web_host" mapstructure:"web_host" yaml:"web_host"` UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"` } diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index fab199e3..f4a8e61d 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -12,6 +12,7 @@ import ( "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/uid" "github.com/disintegration/imaging" @@ -27,7 +28,8 @@ const ( // UploaderService user service type UploaderService struct { - serviceConfig *service_config.ServiceConfig + serviceConfig *service_config.ServiceConfig + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewUploaderService new upload service @@ -122,10 +124,14 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.File 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 { + return "", err + } filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath) if err := ctx.SaveUploadedFile(file, filePath); err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } - url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, fileSubPath) + url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath) return url, nil } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 07594c90..a4fc1fad 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -144,7 +144,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet UserID: userInfo.ID, } code := uuid.NewString() - verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.serviceConfig.WebHost, code) + verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL) if err != nil { return "", err @@ -308,7 +308,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo UserID: userInfo.ID, } code := uuid.NewString() - verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return nil, err @@ -351,7 +351,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e UserID: userInfo.ID, } code := uuid.NewString() - verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return err @@ -500,7 +500,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. } code := uuid.NewString() var title, body string - verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.serviceConfig.WebHost, code) + verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.getSiteUrl(ctx), code) if userInfo.MailStatus == entity.EmailStatusToBeVerified { title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL) } else { @@ -548,3 +548,13 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string } return nil } + +// getSiteUrl get site url +func (us *UserService) getSiteUrl(ctx context.Context) string { + siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site general failed: %s", err) + return "" + } + return siteGeneral.SiteUrl +} From 5d3ee17a255de1d709711ee9a28b504310fda427 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 12:28:19 +0800 Subject: [PATCH 116/157] fix: i18n file parsing error --- go.mod | 2 +- go.sum | 2 ++ internal/base/translator/provider.go | 40 +++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d6d85117..0da95f1c 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index be85073b..8a6c2d9b 100644 --- a/go.sum +++ b/go.sum @@ -596,6 +596,8 @@ github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd143 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A= diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index e047ceb2..527f9137 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -8,7 +8,7 @@ import ( "github.com/google/wire" myTran "github.com/segmentfault/pacman/contrib/i18n" "github.com/segmentfault/pacman/i18n" - "sigs.k8s.io/yaml" + "gopkg.in/yaml.v3" ) // ProviderSet is providers. @@ -31,18 +31,52 @@ var ( // NewTranslator new a translator func NewTranslator(c *I18n) (tr i18n.Translator, err error) { - GlobalTrans, err = myTran.NewTranslator(c.BundleDir) + entries, err := os.ReadDir(c.BundleDir) if err != nil { return nil, err } + // read the Bundle resources file from entries + for _, file := range entries { + // ignore directory + if file.IsDir() { + continue + } + // ignore non-YAML file + if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" { + continue + } + buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name())) + if err != nil { + return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err) + } + + // only parse the backend translation + translation := struct { + Content map[string]interface{} `yaml:"backend"` + }{} + if err = yaml.Unmarshal(buf, &translation); err != nil { + return nil, err + } + content, err := yaml.Marshal(translation.Content) + if err != nil { + return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err) + } + + // add translator use backend translation + if err = myTran.AddTranslator(content, file.Name()); err != nil { + return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err) + } + } + GlobalTrans = myTran.GlobalTrans + i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml")) if err != nil { return nil, fmt.Errorf("read i18n file failed: %s", err) } s := struct { - LangOption []*LangOption `json:"language_options"` + LangOption []*LangOption `yaml:"language_options"` }{} err = yaml.Unmarshal(i18nFile, &s) if err != nil { From afb3ecd63b919d0f301e96de6793b95b2c2aef06 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 13:19:44 +0800 Subject: [PATCH 117/157] doc: update i18n file --- i18n/en_US.yaml | 332 ++++++++++++++++++++++++------------------------ i18n/it_IT.yaml | 312 +++++++++++++++++++++++---------------------- i18n/zh_CN.yaml | 313 ++++++++++++++++++++++----------------------- 3 files changed, 481 insertions(+), 476 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index f287444f..0ae83bf3 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1,184 +1,186 @@ -base: - success: - other: "Success." - unknown: - other: "Unknown error." - request_format_error: - other: "Request format is not valid." - unauthorized_error: - other: "Unauthorized." - database_error: - other: "Data server error." +# The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." -email: - other: "Email" -password: - other: "Password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email and password do not match." - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Answer do not found." - comment: - edit_without_permission: - other: "Comment are not allowed to edit." - not_found: - other: "Comment not found." email: - duplicate: - other: "Email already exists." - need_to_be_verified: - other: "Email should be verified." - verify_url_expired: - other: "Email verified URL has expired, please resend the email." - lang: - not_found: - other: "Language file not found." - object: - captcha_verification_failed: - other: "Captcha wrong." - disallow_follow: - other: "You are not allowed to follow." - disallow_vote: - other: "You are not allowed to vote." - disallow_vote_your_self: - other: "You can't vote for your own post." - not_found: - other: "Object not found." - verification_failed: - other: "Verification failed." - email_or_password_incorrect: - other: "Email and password do not match." - old_password_verification_failed: - other: "The old password verification failed" - new_password_same_as_previous_setting: - other: "The new password is the same as the previous one." - question: - not_found: - other: "Question not found." - rank: - fail_to_meet_the_condition: - other: "Rank fail to meet the condition." + other: "Email" + password: + other: "Password" + + email_or_password_wrong_error: &email_or_password_wrong + other: "Email and password do not match." + + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Answer do not found." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + theme: + not_found: + other: "Theme not found." + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + install: + create_config_failed: + other: "Can’t create the config.yaml file." report: - handle_failed: - other: "Report handle failed." - not_found: - other: "Report not found." - tag: - not_found: - other: "Tag not found." - theme: - not_found: - other: "Theme not found." - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "User not found." - suspended: - other: "User has been suspended." - username_invalid: - other: "Username is invalid." - username_duplicate: - other: "Username is already in use." - set_avatar: - other: "Avatar set failed." - - config: - read_config_failed: - other: "Read config failed" - database: - connection_failed: - other: "Database connection failed" - install: - create_config_failed: - other: "Can’t create the config.yaml file." -report: - spam: - name: - other: "spam" - description: - other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." - rude: - name: - other: "rude or abusive" - description: - other: "A reasonable person would find this content inappropriate for respectful discourse." - duplicate: - name: - other: "a duplicate" - description: - other: "This question has been asked before and already has an answer." - not_answer: - name: - other: "not an answer" - description: - other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." - not_need: - name: - other: "no longer needed" - description: - other: "This comment is outdated, conversational or not relevant to this post." - other: - name: - other: "something else" - description: - other: "This post requires staff attention for another reason not listed above." - -question: - close: - duplicate: + spam: name: other: "spam" + description: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + description: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" description: other: "This question has been asked before and already has an answer." - guideline: + not_answer: name: - other: "a community-specific reason" + other: "not an answer" description: - other: "This question doesn't meet a community guideline." - multiple: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: name: - other: "needs details or clarity" + other: "no longer needed" description: - other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" description: - other: "This post requires another reason not listed above." + other: "This post requires staff attention for another reason not listed above." -notification: - action: - update_question: - other: "updated question" - answer_the_question: - other: "answered question" - update_answer: - other: "updated answer" - adopt_answer: - other: "accepted answer" - comment_question: - other: "commented question" - comment_answer: - other: "commented answer" - reply_to_you: - other: "replied to you" - mention_you: - other: "mentioned you" - your_question_is_closed: - other: "Your question has been closed" - your_question_was_deleted: - other: "Your question has been deleted" - your_answer_was_deleted: - other: "Your answer has been deleted" - your_comment_was_deleted: - other: "Your comment has been deleted" + question: + close: + duplicate: + name: + other: "spam" + description: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + description: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + description: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + description: + other: "This post requires another reason not listed above." + + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + adopt_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" # The following fields are used for interface presentation(Front-end) ui: how_to_format: diff --git a/i18n/it_IT.yaml b/i18n/it_IT.yaml index 0b943941..61de40f8 100644 --- a/i18n/it_IT.yaml +++ b/i18n/it_IT.yaml @@ -1,170 +1,172 @@ -base: - success: - other: "Successo" - unknown: - other: "Errore sconosciuto" - request_format_error: - other: "Il formato della richiesta non è valido" - unauthorized_error: - other: "Non autorizzato" - database_error: - other: "Errore server dati" +# The following fields are used for back-end +backend: + base: + success: + other: "Successo" + unknown: + other: "Errore sconosciuto" + request_format_error: + other: "Il formato della richiesta non è valido" + unauthorized_error: + other: "Non autorizzato" + database_error: + other: "Errore server dati" -email: - other: "email" -password: - other: "password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email o password errati" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Risposta non trovata" - comment: - edit_without_permission: - other: "Non si hanno di privilegi sufficienti per modificare il commento" - not_found: - other: "Commento non trovato" email: - duplicate: - other: "email già esistente" - need_to_be_verified: - other: "email deve essere verificata" - verify_url_expired: - other: "l'url di verifica email è scaduto, si prega di reinviare la email" - lang: - not_found: - other: "lingua non trovata" - object: - captcha_verification_failed: - other: "captcha errato" - disallow_follow: - other: "Non sei autorizzato a seguire" - disallow_vote: - other: "non sei autorizzato a votare" - disallow_vote_your_self: - other: "Non puoi votare un tuo post!" - not_found: - other: "oggetto non trovato" - verification_failed: - other: "verifica fallita" - email_or_password_incorrect: - other: "email o password incorretti" - old_password_verification_failed: - other: "la verifica della vecchia password è fallita" - new_password_same_as_previous_setting: - other: "La nuova password è identica alla precedente" - question: - not_found: - other: "domanda non trovata" - rank: - fail_to_meet_the_condition: - other: "Condizioni non valide per il grado" + other: "email" + password: + other: "password" + + email_or_password_wrong_error: &email_or_password_wrong + other: "Email o password errati" + + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Risposta non trovata" + comment: + edit_without_permission: + other: "Non si hanno di privilegi sufficienti per modificare il commento" + not_found: + other: "Commento non trovato" + email: + duplicate: + other: "email già esistente" + need_to_be_verified: + other: "email deve essere verificata" + verify_url_expired: + other: "l'url di verifica email è scaduto, si prega di reinviare la email" + lang: + not_found: + other: "lingua non trovata" + object: + captcha_verification_failed: + other: "captcha errato" + disallow_follow: + other: "Non sei autorizzato a seguire" + disallow_vote: + other: "non sei autorizzato a votare" + disallow_vote_your_self: + other: "Non puoi votare un tuo post!" + not_found: + other: "oggetto non trovato" + verification_failed: + other: "verifica fallita" + email_or_password_incorrect: + other: "email o password incorretti" + old_password_verification_failed: + other: "la verifica della vecchia password è fallita" + new_password_same_as_previous_setting: + other: "La nuova password è identica alla precedente" + question: + not_found: + other: "domanda non trovata" + rank: + fail_to_meet_the_condition: + other: "Condizioni non valide per il grado" + report: + handle_failed: + other: "Gestione del report fallita" + not_found: + other: "Report non trovato" + tag: + not_found: + other: "Etichetta non trovata" + theme: + not_found: + other: "tema non trovato" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "utente non trovato" + suspended: + other: "utente sospeso" + username_invalid: + other: "utente non valido" + username_duplicate: + other: "utente già in uso" + report: - handle_failed: - other: "Gestione del report fallita" - not_found: - other: "Report non trovato" - tag: - not_found: - other: "Etichetta non trovata" - theme: - not_found: - other: "tema non trovato" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "utente non trovato" - suspended: - other: "utente sospeso" - username_invalid: - other: "utente non valido" - username_duplicate: - other: "utente già in uso" - -report: - spam: - name: - other: "spam" - description: - other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" - rude: - name: - other: "scortese o violento" - description: - other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" - duplicate: - name: - other: "duplicato" - description: - other: "Questa domanda è già stata posta e ha già una risposta." - not_answer: - name: - other: "non è una risposta" - description: - other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." - not_need: - name: - other: "non più necessario" - description: - other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." - other: - name: - other: "altro" - description: - other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." - -question: - close: - duplicate: + spam: name: other: "spam" description: - other: "Questa domanda è già stata chiesta o ha già una risposta." - guideline: + other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" + rude: name: - other: "motivo legato alla community" + other: "scortese o violento" description: - other: "Questa domanda non soddisfa le linee guida della comunità." - multiple: + other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" + duplicate: name: - other: "richiede maggiori dettagli o chiarezza" + other: "duplicato" description: - other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: "Questa domanda è già stata posta e ha già una risposta." + not_answer: + name: + other: "non è una risposta" + description: + other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." + not_need: + name: + other: "non più necessario" + description: + other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." other: name: other: "altro" description: - other: "Questo articolo richiede un'altro motivo non listato sopra." + other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." -notification: - action: - update_question: - other: "domanda aggiornata" - answer_the_question: - other: "domanda risposta" - update_answer: - other: "risposta aggiornata" - adopt_answer: - other: "risposta accettata" - comment_question: - other: "domanda commentata" - comment_answer: - other: "risposta commentata" - reply_to_you: - other: "hai ricevuto risposta" - mention_you: - other: "sei stato menzionato" - your_question_is_closed: - other: "la tua domanda è stata chiusa" - your_question_was_deleted: - other: "la tua domanda è stata rimossa" - your_answer_was_deleted: - other: "la tua risposta è stata rimossa" - your_comment_was_deleted: - other: "il tuo commento è stato rimosso" + question: + close: + duplicate: + name: + other: "spam" + description: + other: "Questa domanda è già stata chiesta o ha già una risposta." + guideline: + name: + other: "motivo legato alla community" + description: + other: "Questa domanda non soddisfa le linee guida della comunità." + multiple: + name: + other: "richiede maggiori dettagli o chiarezza" + description: + other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: + name: + other: "altro" + description: + other: "Questo articolo richiede un'altro motivo non listato sopra." + + notification: + action: + update_question: + other: "domanda aggiornata" + answer_the_question: + other: "domanda risposta" + update_answer: + other: "risposta aggiornata" + adopt_answer: + other: "risposta accettata" + comment_question: + other: "domanda commentata" + comment_answer: + other: "risposta commentata" + reply_to_you: + other: "hai ricevuto risposta" + mention_you: + other: "sei stato menzionato" + your_question_is_closed: + other: "la tua domanda è stata chiusa" + your_question_was_deleted: + other: "la tua domanda è stata rimossa" + your_answer_was_deleted: + other: "la tua risposta è stata rimossa" + your_comment_was_deleted: + other: "il tuo commento è stato rimosso" diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index a02f4219..65e0c5ca 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1,175 +1,176 @@ -base: - success: - other: "成功" - unknown: - other: "未知错误" - request_format_error: - other: "请求格式错误" - unauthorized_error: - other: "未登录" - database_error: - other: "数据服务异常" +backend: + base: + success: + other: "成功" + unknown: + other: "未知错误" + request_format_error: + other: "请求格式错误" + unauthorized_error: + other: "未登录" + database_error: + other: "数据服务异常" -email: - other: "邮箱" -password: - other: "密码" - -email_or_password_wrong_error: &email_or_password_wrong - other: "邮箱或密码错误" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "答案未找到" - comment: - edit_without_permission: - other: "不允许编辑评论" - not_found: - other: "评论未找到" email: - duplicate: - other: "邮箱已经存在" - need_to_be_verified: - other: "邮箱需要验证" - verify_url_expired: - other: "邮箱验证的网址已过期,请重新发送邮件" - lang: - not_found: - other: "语言未找到" - object: - captcha_verification_failed: - other: "验证码错误" - disallow_follow: - other: "你不能关注" - disallow_vote: - other: "你不能投票" - disallow_vote_your_self: - other: "你不能为自己的帖子投票!" - not_found: - other: "对象未找到" - verification_failed: - other: "验证失败" - email_or_password_incorrect: - other: "邮箱或密码不正确" - old_password_verification_failed: - other: "旧密码验证失败" - new_password_same_as_previous_setting: - other: "新密码与之前的设置相同" - question: - not_found: - other: "问题未找到" - rank: - fail_to_meet_the_condition: - other: "级别不符合条件" + other: "邮箱" + password: + other: "密码" + + email_or_password_wrong_error: &email_or_password_wrong + other: "邮箱或密码错误" + + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "答案未找到" + comment: + edit_without_permission: + other: "不允许编辑评论" + not_found: + other: "评论未找到" + email: + duplicate: + other: "邮箱已经存在" + need_to_be_verified: + other: "邮箱需要验证" + verify_url_expired: + other: "邮箱验证的网址已过期,请重新发送邮件" + lang: + not_found: + other: "语言未找到" + object: + captcha_verification_failed: + other: "验证码错误" + disallow_follow: + other: "你不能关注" + disallow_vote: + other: "你不能投票" + disallow_vote_your_self: + other: "你不能为自己的帖子投票!" + not_found: + other: "对象未找到" + verification_failed: + other: "验证失败" + email_or_password_incorrect: + other: "邮箱或密码不正确" + old_password_verification_failed: + other: "旧密码验证失败" + new_password_same_as_previous_setting: + other: "新密码与之前的设置相同" + question: + not_found: + other: "问题未找到" + rank: + fail_to_meet_the_condition: + other: "级别不符合条件" + report: + handle_failed: + other: "报告处理失败" + not_found: + other: "报告未找到" + tag: + not_found: + other: "标签未找到" + theme: + not_found: + other: "主题未找到" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "用户未找到" + suspended: + other: "用户已被暂停" + username_invalid: + other: "用户名无效" + username_duplicate: + other: "用户名已被使用" + set_avatar: + other: "头像设置错误" + report: - handle_failed: - other: "报告处理失败" - not_found: - other: "报告未找到" - tag: - not_found: - other: "标签未找到" - theme: - not_found: - other: "主题未找到" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "用户未找到" - suspended: - other: "用户已被暂停" - username_invalid: - other: "用户名无效" - username_duplicate: - other: "用户名已被使用" - set_avatar: - other: "头像设置错误" - -report: - spam: - name: - other: "垃圾信息" - description: - other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" - rude: - name: - other: "粗鲁或辱骂的" - description: - other: "有理智的人都会发现此内容不适合进行尊重的讨论。" - duplicate: - name: - other: "重复信息" - description: - other: "此问题以前就有人问过,而且已经有了答案。" - not_answer: - name: - other: "不是答案" - description: - other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" - not_need: - name: - other: "不再需要" - description: - other: "此条评论是过时的,对话性的或与本帖无关。" - other: - name: - other: "其他原因" - description: - other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" - -question: - close: - duplicate: + spam: name: other: "垃圾信息" + description: + other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" + rude: + name: + other: "粗鲁或辱骂的" + description: + other: "有理智的人都会发现此内容不适合进行尊重的讨论。" + duplicate: + name: + other: "重复信息" description: other: "此问题以前就有人问过,而且已经有了答案。" - guideline: + not_answer: name: - other: "社区特定原因" + other: "不是答案" description: - other: "此问题不符合社区准则。" - multiple: + other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" + not_need: name: - other: "需要细节或澄清" + other: "不再需要" description: - other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: "此条评论是过时的,对话性的或与本帖无关。" other: name: other: "其他原因" description: - other: "此帖子需要上述所列以外的其他理由。" + other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" -notification: - action: - update_question: - other: "更新了问题" - answer_the_question: - other: "回答了问题" - update_answer: - other: "更新了答案" - adopt_answer: - other: "接受了答案" - comment_question: - other: "评论了问题" - comment_answer: - other: "评论了答案" - reply_to_you: - other: "回复了你" - mention_you: - other: "提到了你" - your_question_is_closed: - other: "你的问题已被关闭" - your_question_was_deleted: - other: "你的问题已被删除" - your_answer_was_deleted: - other: "你的答案已被删除" - your_comment_was_deleted: - other: "你的评论已被删除" + question: + close: + duplicate: + name: + other: "垃圾信息" + description: + other: "此问题以前就有人问过,而且已经有了答案。" + guideline: + name: + other: "社区特定原因" + description: + other: "此问题不符合社区准则。" + multiple: + name: + other: "需要细节或澄清" + description: + other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: + name: + other: "其他原因" + description: + other: "此帖子需要上述所列以外的其他理由。" + + notification: + action: + update_question: + other: "更新了问题" + answer_the_question: + other: "回答了问题" + update_answer: + other: "更新了答案" + adopt_answer: + other: "接受了答案" + comment_question: + other: "评论了问题" + comment_answer: + other: "评论了答案" + reply_to_you: + other: "回复了你" + mention_you: + other: "提到了你" + your_question_is_closed: + other: "你的问题已被关闭" + your_question_was_deleted: + other: "你的问题已被删除" + your_answer_was_deleted: + other: "你的答案已被删除" + your_comment_was_deleted: + other: "你的评论已被删除" # The following fields are used for interface presentation(Front-end) ui: how_to_format: From 68fce365c8f684fa62f43bcd93d49058b9282a40 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 12:28:19 +0800 Subject: [PATCH 118/157] fix: i18n file parsing error --- go.mod | 2 +- go.sum | 2 ++ internal/base/translator/provider.go | 40 +++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d6d85117..0da95f1c 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index be85073b..8a6c2d9b 100644 --- a/go.sum +++ b/go.sum @@ -596,6 +596,8 @@ github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd143 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A= diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index e047ceb2..527f9137 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -8,7 +8,7 @@ import ( "github.com/google/wire" myTran "github.com/segmentfault/pacman/contrib/i18n" "github.com/segmentfault/pacman/i18n" - "sigs.k8s.io/yaml" + "gopkg.in/yaml.v3" ) // ProviderSet is providers. @@ -31,18 +31,52 @@ var ( // NewTranslator new a translator func NewTranslator(c *I18n) (tr i18n.Translator, err error) { - GlobalTrans, err = myTran.NewTranslator(c.BundleDir) + entries, err := os.ReadDir(c.BundleDir) if err != nil { return nil, err } + // read the Bundle resources file from entries + for _, file := range entries { + // ignore directory + if file.IsDir() { + continue + } + // ignore non-YAML file + if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" { + continue + } + buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name())) + if err != nil { + return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err) + } + + // only parse the backend translation + translation := struct { + Content map[string]interface{} `yaml:"backend"` + }{} + if err = yaml.Unmarshal(buf, &translation); err != nil { + return nil, err + } + content, err := yaml.Marshal(translation.Content) + if err != nil { + return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err) + } + + // add translator use backend translation + if err = myTran.AddTranslator(content, file.Name()); err != nil { + return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err) + } + } + GlobalTrans = myTran.GlobalTrans + i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml")) if err != nil { return nil, fmt.Errorf("read i18n file failed: %s", err) } s := struct { - LangOption []*LangOption `json:"language_options"` + LangOption []*LangOption `yaml:"language_options"` }{} err = yaml.Unmarshal(i18nFile, &s) if err != nil { From fa3838b6048e1f4964c2dadfdd792ea3bef6a2e9 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 13:19:44 +0800 Subject: [PATCH 119/157] doc: update i18n file --- i18n/en_US.yaml | 1270 +++++++++++++++++++++++++++++++++++++++++------ i18n/it_IT.yaml | 312 ++++++------ i18n/zh_CN.yaml | 1061 +++++++++++++++++++++++++++++++++------ 3 files changed, 2167 insertions(+), 476 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 84afa373..0ae83bf3 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1,182 +1,1122 @@ -base: - success: - other: "Success." - unknown: - other: "Unknown error." - request_format_error: - other: "Request format is not valid." - unauthorized_error: - other: "Unauthorized." - database_error: - other: "Data server error." +# The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." -email: - other: "Email" -password: - other: "Password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email and password do not match." - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Answer do not found." - comment: - edit_without_permission: - other: "Comment are not allowed to edit." - not_found: - other: "Comment not found." email: - duplicate: - other: "Email already exists." - need_to_be_verified: - other: "Email should be verified." - verify_url_expired: - other: "Email verified URL has expired, please resend the email." - lang: - not_found: - other: "Language file not found." - object: - captcha_verification_failed: - other: "Captcha wrong." - disallow_follow: - other: "You are not allowed to follow." - disallow_vote: - other: "You are not allowed to vote." - disallow_vote_your_self: - other: "You can't vote for your own post." - not_found: - other: "Object not found." - verification_failed: - other: "Verification failed." - email_or_password_incorrect: - other: "Email and password do not match." - old_password_verification_failed: - other: "The old password verification failed" - new_password_same_as_previous_setting: - other: "The new password is the same as the previous one." - question: - not_found: - other: "Question not found." - rank: - fail_to_meet_the_condition: - other: "Rank fail to meet the condition." + other: "Email" + password: + other: "Password" + + email_or_password_wrong_error: &email_or_password_wrong + other: "Email and password do not match." + + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Answer do not found." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + theme: + not_found: + other: "Theme not found." + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + install: + create_config_failed: + other: "Can’t create the config.yaml file." report: - handle_failed: - other: "Report handle failed." - not_found: - other: "Report not found." - tag: - not_found: - other: "Tag not found." - theme: - not_found: - other: "Theme not found." - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "User not found." - suspended: - other: "User has been suspended." - username_invalid: - other: "Username is invalid." - username_duplicate: - other: "Username is already in use." - set_avatar: - other: "Avatar set failed." - - config: - read_config_failed: - other: "Read config failed" - database: - connection_failed: - other: "Database connection failed" - install: - create_config_failed: - other: "Can’t create the config.yaml file." -report: - spam: - name: - other: "spam" - description: - other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." - rude: - name: - other: "rude or abusive" - description: - other: "A reasonable person would find this content inappropriate for respectful discourse." - duplicate: - name: - other: "a duplicate" - description: - other: "This question has been asked before and already has an answer." - not_answer: - name: - other: "not an answer" - description: - other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." - not_need: - name: - other: "no longer needed" - description: - other: "This comment is outdated, conversational or not relevant to this post." - other: - name: - other: "something else" - description: - other: "This post requires staff attention for another reason not listed above." - -question: - close: - duplicate: + spam: name: other: "spam" + description: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + description: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" description: other: "This question has been asked before and already has an answer." - guideline: + not_answer: name: - other: "a community-specific reason" + other: "not an answer" description: - other: "This question doesn't meet a community guideline." - multiple: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: name: - other: "needs details or clarity" + other: "no longer needed" description: - other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" description: - other: "This post requires another reason not listed above." + other: "This post requires staff attention for another reason not listed above." -notification: - action: - update_question: - other: "updated question" - answer_the_question: - other: "answered question" - update_answer: - other: "updated answer" - adopt_answer: - other: "accepted answer" - comment_question: - other: "commented question" - comment_answer: - other: "commented answer" - reply_to_you: - other: "replied to you" - mention_you: - other: "mentioned you" - your_question_is_closed: - other: "Your question has been closed" - your_question_was_deleted: - other: "Your question has been deleted" - your_answer_was_deleted: - other: "Your answer has been deleted" - your_comment_was_deleted: - other: "Your comment has been deleted" + question: + close: + duplicate: + name: + other: "spam" + description: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + description: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + description: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + description: + other: "This post requires another reason not listed above." + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + adopt_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + description: >- +
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by + placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Webite Maintenance + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: 'Your account was suspended until {{ time }}.' + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4MB. + description: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + description: 'Must use the character set "a-z", "0-9", "+ # - ."' + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + description: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allowed deleting tag with posts.

Please remove this tag + from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: 'Must use the character set "a-z", "0-9", "+ # - ."' + description: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: 'MMM D, YYYY' + long_date_with_time: 'MMM D, YYYY [at] HH:mm' + now: now + x_seconds_ago: '{{count}}s ago' + x_minutes_ago: '{{count}}m ago' + x_hours_ago: '{{count}}h ago' + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid + answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are + adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_description: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: 'Describe what your question is about, at least one tag is required.' + no_result: No tags matched + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. + Please follow the instructions in the mail to activate your account. + info: 'If it doesn''t arrive, check your spam folder.' + another: >- + We sent another activation email to you at {{mail}}. It might + take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to Answer + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name up to 30 characters. + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in + page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is + already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + btn_name: Update profile + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: Default + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: 'https://example.com' + msg: Website incorrect format + location: + label: Location (optional) + placeholder: 'City, Country' + notification: + email: + label: Email Notifications + radio: 'Answers to your questions, comments, and more' + account: + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation + instructions. + email: + label: Email + msg: Email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the + edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because + doing so deprives future readers of this knowledge.

Repeated deletion + of answered questions can result in your account being blocked from asking. + Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because + doing so deprives future readers of this knowledge.

Repeated deletion + of accepted answers can result in your account being blocked from answering. + Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: '{{count}} Results' + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: '<1>[tag] search withing a tag' + user: '<1>user:username search by author' + answer: '<1>answers:0 unanswered questions' + score: '<1>score:3 posts with a 3+ score' + question: '<1>is:question search questions' + is_answer: '<1>is:answer search answers' + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to Twitter + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your + account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was + already changed? + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: '{{ count }} Questions' + x_answers: '{{ count }} answers' + questions: Questions + answers: Answers + newest: Newest + active: Active + frequent: Frequent + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: '// Hello, World !' + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Answer + next: Next + done: Done + config_yaml_error: Can’t create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: 'db:3306' + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + description: >- + You can create the <1>config.yaml file manually in the + <1>/var/wwww/xxx/ directory and paste the following text into it. + info: 'After you’ve done that, click “Next” button.' + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure + location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your Answer is Ready! + ready_description: >- + If you ever feel like changing more settings, visit <1>admin section; + find it in the site menu. + good_luck: 'Have fun, and good luck!' + warn_title: Warning + warn_description: >- + The file <1>config.yaml already exists. If you need to reset any of the + configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_description: >- + You appear to have already installed. To reinstall please clear your old + database tables first. + page_404: + description: 'Unfortunately, this page doesn''t exist.' + back_home: Back to homepage + page_50X: + description: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + description: 'We are under maintenance, we’ll be back soon.' + admin: + admin_header: + title: Admin + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + dashboard: + title: Dashboard + welcome: Welcome to Answer Admin! + site_statistics: Site Statistics + questions: 'Questions:' + answers: 'Answers:' + comments: 'Comments:' + votes: 'Votes:' + active_users: 'Active users:' + flags: 'Flags:' + site_health_status: Site Health Status + version: 'Version:' + https: 'HTTPS:' + uploading_files: 'Uploading files:' + smtp: 'SMTP:' + timezone: 'Timezone:' + system_info: System Info + storage_used: 'Storage used:' + uptime: 'Uptime:' + answer_links: Answer Links + documents: Documents + feedback: Feedback + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + 'yes': 'Yes' + 'no': 'No' + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_description: A normal user can ask and answer questions. + suspended_name: suspended + suspended_description: A suspended user can't log in. + deleted_name: deleted + deleted_description: 'Delete profile, authentication associations.' + inactive_name: inactive + inactive_description: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: 'Change {{ type }} status to...' + normal_name: normal + normal_description: A normal post available to everyone. + closed_name: closed + closed_description: 'A closed question can''t answer, but still can edit, vote and comment.' + deleted_name: deleted + deleted_description: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + action: Action + change: Change + all: All + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + filter: + placeholder: 'Filter by name, user:id' + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, question:id' + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, answer:id' + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: 'The name of this site, as used in the title tag.' + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_description: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: 'Short description, as used in the title tag on homepage.' + description: + label: Site Description (optional) + msg: Site description cannot be empty. + text: 'Describe this site in one sentence, as used in the meta description tag.' + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: SMTP Authentication + msg: SMTP authentication cannot be empty. + 'yes': 'Yes' + 'no': 'No' diff --git a/i18n/it_IT.yaml b/i18n/it_IT.yaml index 0b943941..61de40f8 100644 --- a/i18n/it_IT.yaml +++ b/i18n/it_IT.yaml @@ -1,170 +1,172 @@ -base: - success: - other: "Successo" - unknown: - other: "Errore sconosciuto" - request_format_error: - other: "Il formato della richiesta non è valido" - unauthorized_error: - other: "Non autorizzato" - database_error: - other: "Errore server dati" +# The following fields are used for back-end +backend: + base: + success: + other: "Successo" + unknown: + other: "Errore sconosciuto" + request_format_error: + other: "Il formato della richiesta non è valido" + unauthorized_error: + other: "Non autorizzato" + database_error: + other: "Errore server dati" -email: - other: "email" -password: - other: "password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email o password errati" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Risposta non trovata" - comment: - edit_without_permission: - other: "Non si hanno di privilegi sufficienti per modificare il commento" - not_found: - other: "Commento non trovato" email: - duplicate: - other: "email già esistente" - need_to_be_verified: - other: "email deve essere verificata" - verify_url_expired: - other: "l'url di verifica email è scaduto, si prega di reinviare la email" - lang: - not_found: - other: "lingua non trovata" - object: - captcha_verification_failed: - other: "captcha errato" - disallow_follow: - other: "Non sei autorizzato a seguire" - disallow_vote: - other: "non sei autorizzato a votare" - disallow_vote_your_self: - other: "Non puoi votare un tuo post!" - not_found: - other: "oggetto non trovato" - verification_failed: - other: "verifica fallita" - email_or_password_incorrect: - other: "email o password incorretti" - old_password_verification_failed: - other: "la verifica della vecchia password è fallita" - new_password_same_as_previous_setting: - other: "La nuova password è identica alla precedente" - question: - not_found: - other: "domanda non trovata" - rank: - fail_to_meet_the_condition: - other: "Condizioni non valide per il grado" + other: "email" + password: + other: "password" + + email_or_password_wrong_error: &email_or_password_wrong + other: "Email o password errati" + + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Risposta non trovata" + comment: + edit_without_permission: + other: "Non si hanno di privilegi sufficienti per modificare il commento" + not_found: + other: "Commento non trovato" + email: + duplicate: + other: "email già esistente" + need_to_be_verified: + other: "email deve essere verificata" + verify_url_expired: + other: "l'url di verifica email è scaduto, si prega di reinviare la email" + lang: + not_found: + other: "lingua non trovata" + object: + captcha_verification_failed: + other: "captcha errato" + disallow_follow: + other: "Non sei autorizzato a seguire" + disallow_vote: + other: "non sei autorizzato a votare" + disallow_vote_your_self: + other: "Non puoi votare un tuo post!" + not_found: + other: "oggetto non trovato" + verification_failed: + other: "verifica fallita" + email_or_password_incorrect: + other: "email o password incorretti" + old_password_verification_failed: + other: "la verifica della vecchia password è fallita" + new_password_same_as_previous_setting: + other: "La nuova password è identica alla precedente" + question: + not_found: + other: "domanda non trovata" + rank: + fail_to_meet_the_condition: + other: "Condizioni non valide per il grado" + report: + handle_failed: + other: "Gestione del report fallita" + not_found: + other: "Report non trovato" + tag: + not_found: + other: "Etichetta non trovata" + theme: + not_found: + other: "tema non trovato" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "utente non trovato" + suspended: + other: "utente sospeso" + username_invalid: + other: "utente non valido" + username_duplicate: + other: "utente già in uso" + report: - handle_failed: - other: "Gestione del report fallita" - not_found: - other: "Report non trovato" - tag: - not_found: - other: "Etichetta non trovata" - theme: - not_found: - other: "tema non trovato" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "utente non trovato" - suspended: - other: "utente sospeso" - username_invalid: - other: "utente non valido" - username_duplicate: - other: "utente già in uso" - -report: - spam: - name: - other: "spam" - description: - other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" - rude: - name: - other: "scortese o violento" - description: - other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" - duplicate: - name: - other: "duplicato" - description: - other: "Questa domanda è già stata posta e ha già una risposta." - not_answer: - name: - other: "non è una risposta" - description: - other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." - not_need: - name: - other: "non più necessario" - description: - other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." - other: - name: - other: "altro" - description: - other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." - -question: - close: - duplicate: + spam: name: other: "spam" description: - other: "Questa domanda è già stata chiesta o ha già una risposta." - guideline: + other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" + rude: name: - other: "motivo legato alla community" + other: "scortese o violento" description: - other: "Questa domanda non soddisfa le linee guida della comunità." - multiple: + other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" + duplicate: name: - other: "richiede maggiori dettagli o chiarezza" + other: "duplicato" description: - other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: "Questa domanda è già stata posta e ha già una risposta." + not_answer: + name: + other: "non è una risposta" + description: + other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." + not_need: + name: + other: "non più necessario" + description: + other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." other: name: other: "altro" description: - other: "Questo articolo richiede un'altro motivo non listato sopra." + other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." -notification: - action: - update_question: - other: "domanda aggiornata" - answer_the_question: - other: "domanda risposta" - update_answer: - other: "risposta aggiornata" - adopt_answer: - other: "risposta accettata" - comment_question: - other: "domanda commentata" - comment_answer: - other: "risposta commentata" - reply_to_you: - other: "hai ricevuto risposta" - mention_you: - other: "sei stato menzionato" - your_question_is_closed: - other: "la tua domanda è stata chiusa" - your_question_was_deleted: - other: "la tua domanda è stata rimossa" - your_answer_was_deleted: - other: "la tua risposta è stata rimossa" - your_comment_was_deleted: - other: "il tuo commento è stato rimosso" + question: + close: + duplicate: + name: + other: "spam" + description: + other: "Questa domanda è già stata chiesta o ha già una risposta." + guideline: + name: + other: "motivo legato alla community" + description: + other: "Questa domanda non soddisfa le linee guida della comunità." + multiple: + name: + other: "richiede maggiori dettagli o chiarezza" + description: + other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: + name: + other: "altro" + description: + other: "Questo articolo richiede un'altro motivo non listato sopra." + + notification: + action: + update_question: + other: "domanda aggiornata" + answer_the_question: + other: "domanda risposta" + update_answer: + other: "risposta aggiornata" + adopt_answer: + other: "risposta accettata" + comment_question: + other: "domanda commentata" + comment_answer: + other: "risposta commentata" + reply_to_you: + other: "hai ricevuto risposta" + mention_you: + other: "sei stato menzionato" + your_question_is_closed: + other: "la tua domanda è stata chiusa" + your_question_was_deleted: + other: "la tua domanda è stata rimossa" + your_answer_was_deleted: + other: "la tua risposta è stata rimossa" + your_comment_was_deleted: + other: "il tuo commento è stato rimosso" diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 7d76cc06..65e0c5ca 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1,172 +1,921 @@ -base: - success: - other: "成功" - unknown: - other: "未知错误" - request_format_error: - other: "请求格式错误" - unauthorized_error: - other: "未登录" - database_error: - other: "数据服务异常" +backend: + base: + success: + other: "成功" + unknown: + other: "未知错误" + request_format_error: + other: "请求格式错误" + unauthorized_error: + other: "未登录" + database_error: + other: "数据服务异常" -email: - other: "邮箱" -password: - other: "密码" - -email_or_password_wrong_error: &email_or_password_wrong - other: "邮箱或密码错误" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "答案未找到" - comment: - edit_without_permission: - other: "不允许编辑评论" - not_found: - other: "评论未找到" email: - duplicate: - other: "邮箱已经存在" - need_to_be_verified: - other: "邮箱需要验证" - verify_url_expired: - other: "邮箱验证的网址已过期,请重新发送邮件" - lang: - not_found: - other: "语言未找到" - object: - captcha_verification_failed: - other: "验证码错误" - disallow_follow: - other: "你不能关注" - disallow_vote: - other: "你不能投票" - disallow_vote_your_self: - other: "你不能为自己的帖子投票!" - not_found: - other: "对象未找到" - verification_failed: - other: "验证失败" - email_or_password_incorrect: - other: "邮箱或密码不正确" - old_password_verification_failed: - other: "旧密码验证失败" - new_password_same_as_previous_setting: - other: "新密码与之前的设置相同" - question: - not_found: - other: "问题未找到" - rank: - fail_to_meet_the_condition: - other: "级别不符合条件" + other: "邮箱" + password: + other: "密码" + + email_or_password_wrong_error: &email_or_password_wrong + other: "邮箱或密码错误" + + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "答案未找到" + comment: + edit_without_permission: + other: "不允许编辑评论" + not_found: + other: "评论未找到" + email: + duplicate: + other: "邮箱已经存在" + need_to_be_verified: + other: "邮箱需要验证" + verify_url_expired: + other: "邮箱验证的网址已过期,请重新发送邮件" + lang: + not_found: + other: "语言未找到" + object: + captcha_verification_failed: + other: "验证码错误" + disallow_follow: + other: "你不能关注" + disallow_vote: + other: "你不能投票" + disallow_vote_your_self: + other: "你不能为自己的帖子投票!" + not_found: + other: "对象未找到" + verification_failed: + other: "验证失败" + email_or_password_incorrect: + other: "邮箱或密码不正确" + old_password_verification_failed: + other: "旧密码验证失败" + new_password_same_as_previous_setting: + other: "新密码与之前的设置相同" + question: + not_found: + other: "问题未找到" + rank: + fail_to_meet_the_condition: + other: "级别不符合条件" + report: + handle_failed: + other: "报告处理失败" + not_found: + other: "报告未找到" + tag: + not_found: + other: "标签未找到" + theme: + not_found: + other: "主题未找到" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "用户未找到" + suspended: + other: "用户已被暂停" + username_invalid: + other: "用户名无效" + username_duplicate: + other: "用户名已被使用" + set_avatar: + other: "头像设置错误" + report: - handle_failed: - other: "报告处理失败" - not_found: - other: "报告未找到" - tag: - not_found: - other: "标签未找到" - theme: - not_found: - other: "主题未找到" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "用户未找到" - suspended: - other: "用户已被暂停" - username_invalid: - other: "用户名无效" - username_duplicate: - other: "用户名已被使用" - set_avatar: - other: "头像设置错误" - -report: - spam: - name: - other: "垃圾信息" - description: - other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" - rude: - name: - other: "粗鲁或辱骂的" - description: - other: "有理智的人都会发现此内容不适合进行尊重的讨论。" - duplicate: - name: - other: "重复信息" - description: - other: "此问题以前就有人问过,而且已经有了答案。" - not_answer: - name: - other: "不是答案" - description: - other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" - not_need: - name: - other: "不再需要" - description: - other: "此条评论是过时的,对话性的或与本帖无关。" - other: - name: - other: "其他原因" - description: - other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" - -question: - close: - duplicate: + spam: name: other: "垃圾信息" + description: + other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" + rude: + name: + other: "粗鲁或辱骂的" + description: + other: "有理智的人都会发现此内容不适合进行尊重的讨论。" + duplicate: + name: + other: "重复信息" description: other: "此问题以前就有人问过,而且已经有了答案。" - guideline: + not_answer: name: - other: "社区特定原因" + other: "不是答案" description: - other: "此问题不符合社区准则。" - multiple: + other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" + not_need: name: - other: "需要细节或澄清" + other: "不再需要" description: - other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: "此条评论是过时的,对话性的或与本帖无关。" other: name: other: "其他原因" description: - other: "此帖子需要上述所列以外的其他理由。" + other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" + + question: + close: + duplicate: + name: + other: "垃圾信息" + description: + other: "此问题以前就有人问过,而且已经有了答案。" + guideline: + name: + other: "社区特定原因" + description: + other: "此问题不符合社区准则。" + multiple: + name: + other: "需要细节或澄清" + description: + other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: + name: + other: "其他原因" + description: + other: "此帖子需要上述所列以外的其他理由。" + + notification: + action: + update_question: + other: "更新了问题" + answer_the_question: + other: "回答了问题" + update_answer: + other: "更新了答案" + adopt_answer: + other: "接受了答案" + comment_question: + other: "评论了问题" + comment_answer: + other: "评论了答案" + reply_to_you: + other: "回复了你" + mention_you: + other: "提到了你" + your_question_is_closed: + other: "你的问题已被关闭" + your_question_was_deleted: + other: "你的问题已被删除" + your_answer_was_deleted: + other: "你的答案已被删除" + your_comment_was_deleted: + other: "你的评论已被删除" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何设定文本格式 + description: >- +
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 + **粗体**

  • 使用 4 + 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 + `像 _这样_`

  • 使用```创建代码块

    ```
    // + 这是代码
    ```
+ pagination: + prev: 上一页 + next: 下一页 + page_title: + question: 问题 + questions: 问题 + tag: 标签 + tags: 标签 + tag_wiki: 标签 wiki + edit_tag: 编辑标签 + ask_a_question: 提问题 + edit_question: 编辑问题 + edit_answer: 编辑回答 + search: 搜索 + posts_containing: 包含 + settings: 设定 + notifications: 通知 + login: 登录 + sign_up: 注册 + account_recovery: 账号恢复 + account_activation: 账号激活 + confirm_email: 确认电子邮件 + account_suspended: 账号已封禁 + admin: 后台管理 + notifications: + title: 通知 + inbox: 收件箱 + achievement: 成就 + all_read: 全部标记为已读 + show_more: 显示更多 + suspended: + title: 账号已封禁 + until_time: '你的账号被封禁至{{ time }}。' + forever: 你的账号已被永久封禁。 + end: 违反了我们的社区准则。 + editor: + blockquote: + text: 引用 + bold: + text: 粗体 + chart: + text: 图表 + flow_chart: 流程图 + sequence_diagram: 时序图 + class_diagram: 类图 + state_diagram: 状态图 + entity_relationship_diagram: ER 图 + user_defined_diagram: User defined diagram + gantt_chart: 甘特图 + pie_chart: 饼图 + code: + text: 代码块 + add_code: 添加代码块 + form: + fields: + code: + label: 代码块 + msg: + empty: 代码块不能为空 + language: + label: 语言 (可选) + placeholder: 自动识别 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 行内公式 + block: 公式块 + heading: + text: 标题 + options: + h1: 标题 1 + h2: 标题 2 + h3: 标题 3 + h4: 标题 4 + h5: 标题 5 + h6: 标题 6 + help: + text: 帮助 + hr: + text: 水平分割线 + image: + text: 图片 + add_image: 添加图片 + tab_image: 上传图片 + form_image: + fields: + file: + label: 图片文件 + btn: 选择图片 + msg: + empty: 请选择图片文件。 + only_image: 只能上传图片文件。 + max_size: 图片文件大小不能超过 4 MB。 + description: + label: 图片描述(可选) + tab_url: 网络图片 + form_url: + fields: + url: + label: 图片地址 + msg: + empty: 图片地址不能为空 + name: + label: 图片描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上传中... + indent: + text: 添加缩进 + outdent: + text: 减少缩进 + italic: + text: 斜体 + link: + text: 超链接 + add_link: 添加超链接 + form: + fields: + url: + label: 链接 + msg: + empty: 链接不能为空。 + name: + label: 链接描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: 有编号列表 + unordered_list: + text: 无编号列表 + table: + text: 表格 + heading: 表头 + cell: 单元格 + close_modal: + title: 关闭原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空。 + msg: + empty: 请选择一个原因。 + report_modal: + flag_title: 举报原因是... + close_title: 关闭原因是... + review_question_title: 审查问题 + review_answer_title: 审查回答 + review_comment_title: 审查评论 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空 + msg: + empty: 请选择一个原因。 + tag_modal: + title: 创建新标签 + form: + fields: + display_name: + label: 显示名称(别名) + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + slug_name: + label: URL 固定链接 + description: '必须由 "a-z", "0-9", "+ # - ." 组成' + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + character: 包含非法字符 + description: + label: 标签描述(可选) + btn_cancel: 取消 + btn_submit: 提交 + tag_info: + created_at: 创建于 + edited_at: 编辑于 + synonyms: + title: 同义词 + text: 以下标签等同于 + empty: 此标签目前没有同义词。 + btn_add: 添加同义词 + btn_edit: 编辑 + btn_save: 保存 + synonyms_text: 以下标签等同于 + delete: + title: 删除标签 + content:

不允许删除有关联问题的标签。

请先从关联的问题中删除此标签的引用。

+ content2: 确定要删除吗? + close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + form: + fields: + revision: + label: 编辑历史 + display_name: + label: 名称 + slug_name: + label: URL 固定链接 + info: '必须由 "a-z", "0-9", "+ # - ." 组成' + description: + label: 描述 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: MM月DD日 + long_date_with_year: YYYY年MM月DD日 + long_date_with_time: 'YYYY年MM月DD日 HH:mm' + now: 刚刚 + x_seconds_ago: '{{count}} 秒前' + x_minutes_ago: '{{count}} 分钟前' + x_hours_ago: '{{count}} 小时前' + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存 + btn_cancel: 取消 + show_more: 显示更多评论 + tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。 + tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名过滤 + no_description: 此标签无描述。 + more: 更多 + ask: + title: 提交新的问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + similar_questions: 相似的问题 + form: + fields: + revision: + label: 编辑历史 + title: + label: 标题 + placeholder: 请详细描述你的问题 + msg: + empty: 标题不能为空 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 直接发表回答 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: 选择至少一个与问题相关的标签。 + no_result: 没有匹配的标签 + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + profile: 用户主页 + setting: 账号设置 + logout: 退出登录 + admin: 后台管理 + search: + placeholder: 搜索 + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: 更改图片 + loading: 加载中... + pic_auth_code: + title: 验证码 + placeholder: 输入图片中的文字 + msg: + empty: 不能为空 + inactive: + first: '马上就好了!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活您的帐户。' + info: 如果没有收到,请检查您的垃圾邮件文件夹。 + another: '我们向您发送了另一封激活电子邮件,地址为 {{mail}}。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。' + btn_name: 重新发送激活邮件 + msg: + empty: 不能为空 + login: + page_title: 欢迎来到 Answer + info_sign: 没有帐户?<1>注册 + info_login: 已经有一个帐户?<1>登录 + forgot_pass: 忘记密码? + name: + label: 昵称 + msg: + empty: 昵称不能为空 + range: 昵称最多 30 个字符 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password: + label: 密码 + msg: + empty: 密码不能为空 + different: 两次输入密码不一致 + account_forgot: + page_title: 忘记密码 + btn_name: 发送恢复邮件 + send_success: '如无意外,你的邮箱 {{mail}} 将会收到一封重置密码的邮件,请根据指引重置你的密码。' + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password_reset: + page_title: 密码重置 + btn_name: 重置我的密码 + reset_success: 你已经成功更改密码,将返回登录页面 + link_invalid: 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? + to_login: 前往登录页面 + password: + label: 密码 + msg: + empty: 密码不能为空 + length: 密码长度在8-32个字符之间 + different: 两次输入密码不一致 + password_confirm: + label: 确认新密码 + settings: + page_title: 设置 + nav: + profile: 我的资料 + notification: 通知 + account: 账号 + interface: 界面 + profile: + btn_name: 保存更改 + display_name: + label: 昵称 + msg: 昵称不能为空 + msg_range: 昵称不能超过 30 个字符 + username: + label: 用户名 + caption: 用户之间可以通过 "@用户名" 进行交互。 + msg: 用户名不能为空 + msg_range: 用户名不能超过 30 个字符 + character: '用户名只能由 "a-z", "0-9", " - . _" 组成' + avatar: + label: 头像 + text: 您可以上传图片作为头像,也可以 <1>重置 为 + bio: + label: 关于我 (可选) + website: + label: 网站 (可选) + placeholder: 'https://example.com' + msg: 格式不正确 + location: + label: 位置 (可选) + placeholder: '城市, 国家' + notification: + email: + label: 邮件通知 + radio: 你的提问有新的回答,评论,和其他 + account: + change_email_btn: 更改邮箱 + change_pass_btn: 更改密码 + change_email_info: 邮件已发送。请根据指引完成验证。 + email: + label: 邮箱 + msg: 邮箱不能为空 + password_title: 密码 + current_pass: + label: 当前密码 + msg: + empty: 当前密码不能为空 + length: 密码长度必须在 8 至 32 之间 + different: 两次输入的密码不匹配 + new_pass: + label: 新密码 + pass_confirm: + label: 确认新密码 + interface: + lang: + label: 界面语言 + text: 设置用户界面语言,在刷新页面后生效。 + toast: + update: 更新成功 + update_password: 更改密码成功。 + flag_success: 感谢您的标记,我们会尽快处理。 + related_question: + title: 相关问题 + btn: 我要提问 + answers: 个回答 + question_detail: + Asked: 提问于 + asked: 提问于 + update: 修改于 + edit: 最后编辑于 + Views: 阅读次数 + Follow: 关注此问题 + Following: 已关注 + answered: 回答于 + closed_in: 关闭于 + show_exist: 查看相关问题。 + answers: + title: 个回答 + score: 评分 + newest: 最新 + btn_accept: 采纳 + btn_accepted: 已被采纳 + write_answer: + title: 你的回答 + btn_name: 提交你的回答 + confirm_title: 继续回答 + continue: 继续 + confirm_info:

您确定要提交一个新的回答吗?

您可以直接编辑和改善您之前的回答的。

+ empty: 回答内容不能为空。 + delete: + title: 删除 + question: >- + 我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_question_deleted: 此问题已被删除 + tip_answer_deleted: 此回答已被删除 + btns: + confirm: 确认 + cancel: 取消 + save: 保存 + delete: 删除 + login: 登录 + signup: 注册 + logout: 退出登录 + verify: 验证 + add_question: 我要提问 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: '{{count}} 个结果' + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + tips: + title: 高级搜索提示 + tag: '<1>[tag] 在指定标签中搜索' + user: '<1>user:username 根据作者搜索' + answer: '<1>answers:0 搜索未回答的问题' + score: '<1>score:3 评分 3 分或以上' + question: '<1>is:question 只搜索问题' + is_answer: '<1>is:answer 只搜索回答' + empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 + share: + name: 分享 + copy: 复制链接 + via: 分享在... + copied: 已复制 + facebook: 分享到 Facebook + twitter: 分享到 Twitter + cannot_vote_for_self: 不能给自己投票 + modal_confirm: + title: 发生错误... + account_result: + page_title: 欢迎来到 Answer + success: 你的账号已通过验证,即将返回首页。 + link: 返回首页 + invalid: 抱歉,此验证链接已失效。也许是你的账号已经通过验证了? + confirm_new_email: 你的电子邮箱已更新 + confirm_new_email_invalid: 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? + question: + following_tags: 已关注的标签 + edit: 编辑 + save: 保存 + follow_tag_tip: 按照标签整理您的问题列表。 + hot_questions: 热点问题 + all_questions: 全部问题 + x_questions: '{{ count }} 个问题' + x_answers: '{{ count }} 个回答' + questions: 个问题 + answers: 回答 + newest: 最新 + active: 活跃 + frequent: 浏览量 + score: 评分 + unanswered: 未回答 + modified: 修改于 + answered: 回答于 + asked: 提问于 + closed: 已关闭 + follow_a_tag: 关注一个标签 + more: 更多 + personal: + overview: 概览 + answers: 回答 + answer: 回答 + questions: 问题 + question: 问题 + bookmarks: 收藏 + reputation: 声望 + comments: 评论 + votes: 得票 + newest: 最新 + score: 评分 + edit_profile: 编辑我的资料 + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: 加入于 + last_login: 上次登录 + about_me: 关于我 + about_me_empty: '// Hello, World !' + top_answers: 热门回答 + top_questions: 热门问题 + stats: 状态 + list_empty: 没有找到相关的内容。
试试看其他标签? + accepted: 已采纳 + answered: 回答于 + asked: 提问于 + upvote: 赞同 + downvote: 反对 + mod_short: 管理员 + mod_long: 管理员 + x_reputation: 声望 + x_votes: 得票 + x_answers: 个回答 + x_questions: 个问题 + page_404: + description: 页面不存在 + back_home: 回到主页 + page_50X: + description: 服务器遇到了一个错误,无法完成你的请求。 + back_home: 回到主页 + admin: + admin_header: + title: 后台管理 + nav_menus: + dashboard: 后台管理 + contents: 内容管理 + questions: 问题 + answers: 回答 + users: 用户管理 + flags: 举报管理 + settings: 站点设置 + general: 一般 + interface: 界面 + smtp: SMTP + dashboard: + title: 后台管理 + welcome: 欢迎来到 Answer 后台管理! + version: 版本 + flags: + title: 举报 + pending: 等待处理 + completed: 已完成 + flagged: 被举报内容 + created: 创建于 + action: 操作 + review: 审查 + change_modal: + title: 更改用户状态为... + btn_cancel: 取消 + btn_submit: 提交 + normal_name: 正常 + normal_description: 正常状态的用户可以提问和回答。 + suspended_name: 封禁 + suspended_description: 被封禁的用户将无法登录。 + deleted_name: 删除 + deleted_description: 删除用户的个人信息,认证等等。 + inactive_name: 不活跃 + inactive_description: 不活跃的用户必须重新验证邮箱。 + confirm_title: 删除此用户 + confirm_content: 确定要删除此用户?此操作无法撤销! + confirm_btn: 删除 + msg: + empty: 请选择一个原因 + status_modal: + title: '更改 {{ type }} 状态为...' + normal_name: 正常 + normal_description: 所有用户都可以访问 + closed_name: 关闭 + closed_description: 不能回答,但仍然可以编辑、投票和评论。 + deleted_name: 删除 + deleted_description: 所有获得/损失的声望将会恢复。 + btn_cancel: 取消 + btn_submit: 提交 + btn_next: 下一步 + users: + title: 用户 + name: 名称 + email: 邮箱 + reputation: 声望 + created_at: 创建时间 + delete_at: 删除时间 + suspend_at: 封禁时间 + status: 状态 + action: 操作 + change: 更改 + all: 全部 + inactive: 不活跃 + suspended: 已封禁 + deleted: 已删除 + normal: 正常 + questions: + page_title: 问题 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + post: 标题 + votes: 得票数 + answers: 回答数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + answers: + page_title: 回答 + normal: 正常 + deleted: 已删除 + post: 标题 + votes: 得票数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + general: + page_title: 一般 + name: + label: 站点名称 + msg: 不能为空 + text: 站点的名称,作为站点的标题(HTML 的 title 标签)。 + short_description: + label: 简短的站点标语 (可选) + msg: 不能为空 + text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。 + description: + label: 网站描述 (可选) + msg: 不能为空 + text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。 + interface: + page_title: 界面 + logo: + label: Logo (可选) + msg: 不能为空 + text: 可以上传图片,或者<1>重置为站点标题。 + theme: + label: 主题 + msg: 不能为空 + text: 选择一个主题 + language: + label: 界面语言 + msg: 不能为空 + text: 设置用户界面语言,在刷新页面后生效。 + smtp: + page_title: SMTP + from_email: + label: 发件人地址 + msg: 不能为空 + text: 用于发送邮件的地址。 + from_name: + label: 发件人名称 + msg: 不能为空 + text: 发件人的名称 + smtp_host: + label: SMTP 主机 + msg: 不能为空 + text: 邮件服务器 + encryption: + label: 加密 + msg: 不能为空 + text: 对于大多数服务器而言,SSL 是推荐开启的。 + ssl: SSL + none: 无加密 + smtp_port: + label: SMTP 端口 + msg: SMTP 端口必须在 1 ~ 65535 之间。 + text: 邮件服务器的端口号。 + smtp_username: + label: SMTP 用户名 + msg: 不能为空 + smtp_password: + label: SMTP 密码 + msg: 不能为空 + test_email_recipient: + label: 测试邮件收件人 + text: 提供用于接收测试邮件的邮箱地址。 + msg: 地址无效 + smtp_authentication: + label: SMTP 认证 + msg: 不能为空 + 'yes': 是 + 'no': 否 -notification: - action: - update_question: - other: "更新了问题" - answer_the_question: - other: "回答了问题" - update_answer: - other: "更新了答案" - adopt_answer: - other: "接受了答案" - comment_question: - other: "评论了问题" - comment_answer: - other: "评论了答案" - reply_to_you: - other: "回复了你" - mention_you: - other: "提到了你" - your_question_is_closed: - other: "你的问题已被关闭" - your_question_was_deleted: - other: "你的问题已被删除" - your_answer_was_deleted: - other: "你的答案已被删除" - your_comment_was_deleted: - other: "你的评论已被删除" From e81d9cfeddd63aff7e93885355088303a1a9104e Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 14:46:22 +0800 Subject: [PATCH 120/157] feat: database init when base-info install --- internal/install/install_controller.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index 1bc0984f..ab1d9790 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -126,12 +126,6 @@ func InitEnvironment(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) return } - - if err := migrations.InitDB(c.Data.Database); err != nil { - log.Error("init database error: ", err.Error()) - handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) - return - } handler.HandleResponse(ctx, nil, nil) } @@ -157,6 +151,12 @@ func InitBaseInfo(ctx *gin.Context) { return } + if err := migrations.InitDB(c.Data.Database); err != nil { + log.Error("init database error: ", err.Error()) + handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) + return + } + err = migrations.UpdateInstallInfo(c.Data.Database, req.Language, req.SiteName, req.SiteURL, req.ContactEmail, req.AdminName, req.AdminPassword, req.AdminEmail) if err != nil { From a5150d06a8383cf15864a362b2c32f8cb1cdce4f Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 14:54:06 +0800 Subject: [PATCH 121/157] fix: install process adjustment --- ui/src/pages/Install/index.tsx | 47 +++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index ae8f037b..b87b13b4 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -116,26 +116,6 @@ const Index: FC = () => { setStep((pre) => pre + 1); }; - const submitDatabaseForm = () => { - const params = { - lang: formData.lang.value, - db_type: formData.db_type.value, - db_username: formData.db_username.value, - db_password: formData.db_password.value, - db_host: formData.db_host.value, - db_name: formData.db_name.value, - db_file: formData.db_file.value, - }; - dbCheck(params) - .then(() => { - handleNext(); - }) - .catch((err) => { - console.log(err); - handleErr(err); - }); - }; - const checkInstall = () => { const params = { lang: formData.lang.value, @@ -155,6 +135,27 @@ const Index: FC = () => { }); }; + const submitDatabaseForm = () => { + const params = { + lang: formData.lang.value, + db_type: formData.db_type.value, + db_username: formData.db_username.value, + db_password: formData.db_password.value, + db_host: formData.db_host.value, + db_name: formData.db_name.value, + db_file: formData.db_file.value, + }; + dbCheck(params) + .then(() => { + // handleNext(); + checkInstall(); + }) + .catch((err) => { + console.log(err); + handleErr(err); + }); + }; + const submitSiteConfig = () => { const params = { lang: formData.lang.value, @@ -183,7 +184,11 @@ const Index: FC = () => { submitDatabaseForm(); } if (step === 3) { - checkInstall(); + if (errorData.msg) { + checkInstall(); + } else { + handleNext(); + } } if (step === 4) { submitSiteConfig(); From 21b7560f2c1ccf88a8944ff733a3dce3028cf8cd Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 9 Nov 2022 14:58:15 +0800 Subject: [PATCH 122/157] add dashboard cache --- cmd/answer/wire_gen.go | 2 +- internal/controller/cron_controller.go | 1 + internal/controller/dashboard_controller.go | 2 +- internal/schema/dashboard_schema.go | 5 +++ .../service/dashboard/dashboard_service.go | 34 +++++++++++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 internal/controller/cron_controller.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 49e980eb..e7ade4d0 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -152,7 +152,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, dataData) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) diff --git a/internal/controller/cron_controller.go b/internal/controller/cron_controller.go new file mode 100644 index 00000000..b0b429f8 --- /dev/null +++ b/internal/controller/cron_controller.go @@ -0,0 +1 @@ +package controller diff --git a/internal/controller/dashboard_controller.go b/internal/controller/dashboard_controller.go index 6ee50f4c..a525adeb 100644 --- a/internal/controller/dashboard_controller.go +++ b/internal/controller/dashboard_controller.go @@ -29,7 +29,7 @@ func NewDashboardController( // @Router /answer/admin/api/dashboard [get] // @Success 200 {object} handler.RespBody func (ac *DashboardController) DashboardInfo(ctx *gin.Context) { - info, err := ac.dashboardService.Statistical(ctx) + info, err := ac.dashboardService.StatisticalByCache(ctx) handler.HandleResponse(ctx, err, gin.H{ "info": info, }) diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index 2f5b3622..d08ee2b2 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -4,6 +4,11 @@ import "time" var AppStartTime time.Time +const ( + DashBoardCachekey = "answer@dashboard" + DashBoardCacheTime = 31 * time.Minute +) + type DashboardInfo struct { QuestionCount int64 `json:"question_count"` AnswerCount int64 `json:"answer_count"` diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index aa876507..f1ce90ad 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -9,6 +9,7 @@ import ( "time" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity_common" @@ -33,6 +34,7 @@ type DashboardService struct { reportRepo report_common.ReportRepo configRepo config.ConfigRepo siteInfoService *siteinfo_common.SiteInfoCommonService + data *data.Data } func NewDashboardService( @@ -44,6 +46,7 @@ func NewDashboardService( reportRepo report_common.ReportRepo, configRepo config.ConfigRepo, siteInfoService *siteinfo_common.SiteInfoCommonService, + data *data.Data, ) *DashboardService { return &DashboardService{ questionRepo: questionRepo, @@ -54,9 +57,40 @@ func NewDashboardService( reportRepo: reportRepo, configRepo: configRepo, siteInfoService: siteInfoService, + data: data, } } +func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.DashboardInfo, error) { + ds.SetCache(ctx) + dashboardInfo := &schema.DashboardInfo{} + infoStr, err := ds.data.Cache.GetString(ctx, schema.DashBoardCachekey) + if err != nil { + return dashboardInfo, err + } + err = json.Unmarshal([]byte(infoStr), dashboardInfo) + if err != nil { + return dashboardInfo, err + } + return dashboardInfo, nil +} + +func (ds *DashboardService) SetCache(ctx context.Context) error { + info, err := ds.Statistical(ctx) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + infoStr, err := json.Marshal(info) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + err = ds.data.Cache.SetString(ctx, schema.DashBoardCachekey, string(infoStr), schema.DashBoardCacheTime) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + return nil +} + // Statistical func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) { dashboardInfo := &schema.DashboardInfo{} From 55060c1a10a01a5ce84392f5ac5832fd0d3578a5 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 14:59:07 +0800 Subject: [PATCH 123/157] feat: user change email if email exist return form error --- internal/controller/user_controller.go | 16 +++++++++++----- internal/service/user_service.go | 19 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 3d4d2d3a..92d491fe 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -495,6 +495,10 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { req.UserID = middleware.GetLoginUserIDFromContext(ctx) // If the user is not logged in, the api cannot be used. // If the user email is not verified, that also can use this api to modify the email. + if len(req.UserID) == 0 { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + return + } captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { @@ -506,13 +510,15 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) return } - - if len(req.UserID) == 0 { - handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) + resp, err := uc.userService.UserChangeEmailSendCode(ctx, req) + if err != nil { + if resp != nil { + resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) + } + handler.HandleResponse(ctx, err, resp) return } - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) - err := uc.userService.UserChangeEmailSendCode(ctx, req) handler.HandleResponse(ctx, err, nil) } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index a4fc1fad..8673c3d2 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -477,21 +477,26 @@ func (us *UserService) encryptPassword(ctx context.Context, Pass string) (string } // UserChangeEmailSendCode user change email verification -func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) error { +func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) ( + resp *schema.UserVerifyEmailErrorResponse, err error) { userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { - return err + return nil, err } if !exist { - return errors.BadRequest(reason.UserNotFound) + return nil, errors.BadRequest(reason.UserNotFound) } _, exist, err = us.userRepo.GetByEmail(ctx, req.Email) if err != nil { - return err + return nil, err } if exist { - return errors.BadRequest(reason.EmailDuplicate) + resp = &schema.UserVerifyEmailErrorResponse{ + Key: "e_mail", + Value: reason.EmailDuplicate, + } + return resp, errors.BadRequest(reason.EmailDuplicate) } data := &schema.EmailCodeContent{ @@ -507,12 +512,12 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL) } if err != nil { - return err + return nil, err } log.Infof("send email confirmation %s", verifyEmailURL) go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString()) - return nil + return nil, nil } // UserChangeEmailVerify user change email verify code From 407e29f63dfc5a3b8e9eea602c29c9b4667ecb29 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 9 Nov 2022 15:00:17 +0800 Subject: [PATCH 124/157] fix uploads dir --- .gitignore | 2 +- Dockerfile | 2 +- INSTALL.md | 4 ++-- INSTALL_CN.md | 4 ++-- configs/config.yaml | 2 +- internal/cli/install.go | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 5f313528..84af0c17 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ /.fleet /.vscode/*.log /cmd/answer/*.sh -/cmd/answer/upfiles/* +/cmd/answer/uploads/* /cmd/logs /configs/config-dev.yaml /go.work* diff --git a/Dockerfile b/Dockerfile index 8b55d58f..ac06839a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ RUN apk --no-cache add build-base git \ && make clean build \ && cp answer /usr/bin/answer -RUN mkdir -p /data/upfiles && chmod 777 /data/upfiles \ +RUN mkdir -p /data/uploads && chmod 777 /data/uploads \ && mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n # stage3 copy the binary and resource files into fresh container diff --git a/INSTALL.md b/INSTALL.md index 0d3969d2..7bc307b5 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -91,7 +91,7 @@ swaggerui: service_config: secret_key: "answer" #encryption key web_host: "http://127.0.0.1" #Page access using domain name address - upload_path: "./upfiles" #upload directory + upload_path: "./uploads" #upload directory ``` ## Compile the image @@ -100,4 +100,4 @@ If you have modified the source files and want to repackage the image, you can u docker build -t answer:v1.0.0 . ``` ## common problem - 1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`upfiles`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and upfiles directories. + 1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`uploads`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and uploads directories. diff --git a/INSTALL_CN.md b/INSTALL_CN.md index 300851a2..ec855e94 100644 --- a/INSTALL_CN.md +++ b/INSTALL_CN.md @@ -94,7 +94,7 @@ swaggerui: service_config: secret_key: "answer" #加密key web_host: "http://127.0.0.1" #页面访问使用域名地址 - upload_path: "./upfiles" #上传目录 + upload_path: "./uploads" #上传目录 ``` ## 编译镜像 @@ -103,4 +103,4 @@ service_config: docker build -t answer:v1.0.0 . ``` ## 常见问题 - 1. 项目无法启动,answer 主程序启动依赖配置文件 config.yaml 、国际化翻译目录 /i18n 、上传文件存放目录 /upfiles,需要确保项目启动时加载了配置文件 answer run -c config.yaml 以及在 config.yaml 正确的指定 i18n 和 upfiles 目录的配置项 + 1. 项目无法启动,answer 主程序启动依赖配置文件 config.yaml 、国际化翻译目录 /i18n 、上传文件存放目录 /uploads answer run -c config.yaml 以及在 config.yaml 正确的指定 i18n 和 uploads 目录的配置项 diff --git a/configs/config.yaml b/configs/config.yaml index 6830048f..8032deed 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -17,4 +17,4 @@ swaggerui: service_config: secret_key: "answer" web_host: "http://127.0.0.1:9080" - upload_path: "/data/upfiles" + upload_path: "/data/uploads" diff --git a/internal/cli/install.go b/internal/cli/install.go index 8c8ee64f..28b9966e 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -17,7 +17,7 @@ const ( var ( ConfigFilePath = "/conf/" - UploadFilePath = "/upfiles/" + UploadFilePath = "/uploads/" I18nPath = "/i18n/" ) From 7f49f8bcb143076d2d6a95f9c70e83a230e5e10a Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 9 Nov 2022 15:23:08 +0800 Subject: [PATCH 125/157] fix uploads dir --- internal/cli/install.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/install.go b/internal/cli/install.go index 9273c8e6..1f759b7b 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -16,8 +16,8 @@ const ( ) var ( - ConfigFilePath = "/conf/" - UploadFilePath = "/upfiles/" + ConfigFileDir = "/conf/" + UploadFilePath = "/uploads/" I18nPath = "/i18n/" CacheDir = "/cache/" ) From 5bce99208c749c4c3e9ef189e36a141579adc1ac Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 16:24:25 +0800 Subject: [PATCH 126/157] feat: cut off answer html description --- internal/service/answer_common/answer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index ef611351..196d6628 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -5,6 +5,7 @@ import ( "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/pkg/htmltext" ) type AnswerRepo interface { @@ -75,11 +76,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer info := schema.AdminAnswerInfo{} info.ID = data.ID info.QuestionID = data.QuestionID - info.Description = data.ParsedText info.Adopted = data.Adopted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() info.UserID = data.UserID + info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240) return &info } From b766824a4eaa7ae6dceb1a2799a38b0df7efd5ea Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 16:24:25 +0800 Subject: [PATCH 127/157] feat: cut off answer html description --- internal/service/answer_common/answer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index ef611351..196d6628 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -5,6 +5,7 @@ import ( "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/pkg/htmltext" ) type AnswerRepo interface { @@ -75,11 +76,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer info := schema.AdminAnswerInfo{} info.ID = data.ID info.QuestionID = data.QuestionID - info.Description = data.ParsedText info.Adopted = data.Adopted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() info.UserID = data.UserID + info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240) return &info } From ac62d12ec1220ee9dd18d1d7cc3d2a097adb92c9 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 16:27:23 +0800 Subject: [PATCH 128/157] fix: change email route add guard --- ui/src/router/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index c745ba1d..a723284c 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -155,7 +155,7 @@ const routes: RouteNode[] = [ path: 'users/change-email', page: 'pages/Users/ChangeEmail', guard: async () => { - return guard.notLogged(); + return guard.notActivated(); }, }, { From 3d95d0391146251d0f2b981082d15aaa9c4c86a7 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 17:20:16 +0800 Subject: [PATCH 129/157] feat: add html text cut pkg --- go.mod | 3 +- go.sum | 4 +-- pkg/htmltext/htmltext.go | 61 +++++++++++++++++++++++++++++++++++ pkg/htmltext/htmltext_test.go | 52 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 pkg/htmltext/htmltext.go create mode 100644 pkg/htmltext/htmltext_test.go diff --git a/go.mod b/go.mod index 0da95f1c..95796a93 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/goccy/go-json v0.9.11 github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 + github.com/grokify/html-strip-tags-go v0.0.1 github.com/jinzhu/copier v0.3.5 github.com/jinzhu/now v1.1.5 github.com/lib/pq v1.10.7 @@ -35,6 +36,7 @@ require ( golang.org/x/crypto v0.1.0 golang.org/x/net v0.1.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/yaml.v3 v3.0.1 xorm.io/builder v0.3.12 xorm.io/core v0.7.3 xorm.io/xorm v1.3.2 @@ -110,6 +112,5 @@ require ( gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 8a6c2d9b..f12aeb7a 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -594,8 +596,6 @@ github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go new file mode 100644 index 00000000..e2acb315 --- /dev/null +++ b/pkg/htmltext/htmltext.go @@ -0,0 +1,61 @@ +package htmltext + +import ( + "regexp" + "strings" + + "github.com/grokify/html-strip-tags-go" +) + +// ClearText clear HTML, get the clear text +func ClearText(html string) (text string) { + if len(html) == 0 { + text = html + return + } + + var ( + re *regexp.Regexp + codeReg = `(?ism)<(pre)>.*<\/pre>` + codeRepl = "{code...}" + linkReg = `(?ism).*?<\/a>` + linkRepl = "[link]" + spaceReg = ` +` + spaceRepl = " " + ) + re = regexp.MustCompile(codeReg) + html = re.ReplaceAllString(html, codeRepl) + + re = regexp.MustCompile(linkReg) + html = re.ReplaceAllString(html, linkRepl) + + text = strings.NewReplacer( + "\n", " ", + "\r", " ", + "\t", " ", + ).Replace(strip.StripTags(html)) + + // replace multiple spaces to one space + re = regexp.MustCompile(spaceReg) + text = strings.TrimSpace(re.ReplaceAllString(text, spaceRepl)) + return +} + +// FetchExcerpt return the excerpt from the HTML string +func FetchExcerpt(html, trimMarker string, limit int) (text string) { + if len(html) == 0 { + text = html + return + } + + text = ClearText(html) + runeText := []rune(text) + if len(runeText) <= limit { + text = string(runeText) + } else { + text = string(runeText[0:limit]) + } + + text += trimMarker + return +} diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go new file mode 100644 index 00000000..568299af --- /dev/null +++ b/pkg/htmltext/htmltext_test.go @@ -0,0 +1,52 @@ +package htmltext + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClearText(t *testing.T) { + var ( + expected, + clearedText string + ) + + // test code clear text + expected = "hello{code...}" + clearedText = ClearText("

hello

var a = \"good\"

") + assert.Equal(t, expected, clearedText) + + // test link clear text + expected = "hello[link]" + clearedText = ClearText("

helloexample.com

") + assert.Equal(t, expected, clearedText) + clearedText = ClearText("

helloexample.com

") + assert.Equal(t, expected, clearedText) + + expected = "hello world" + clearedText = ClearText("
hello
\n
world
") + assert.Equal(t, expected, clearedText) +} + +func TestFetchExcerpt(t *testing.T) { + var ( + expected, + text string + ) + + // test english string + expected = "hello..." + text = FetchExcerpt("

hello world

", "...", 5) + assert.Equal(t, expected, text) + + // test mixed string + expected = "hello你好..." + text = FetchExcerpt("

hello你好world

", "...", 7) + assert.Equal(t, expected, text) + + // test mixed string with emoticon + expected = "hello你好😂..." + text = FetchExcerpt("

hello你好😂world

", "...", 8) + assert.Equal(t, expected, text) +} From 8ac92750a41bad3cd5906cc62fdbe9149d29f69b Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 17:22:20 +0800 Subject: [PATCH 130/157] feat: add html text cut pkg --- pkg/htmltext/htmltext.go | 3 +-- pkg/htmltext/htmltext_test.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go index e2acb315..31ae1a22 100644 --- a/pkg/htmltext/htmltext.go +++ b/pkg/htmltext/htmltext.go @@ -1,10 +1,9 @@ package htmltext import ( + "github.com/grokify/html-strip-tags-go" "regexp" "strings" - - "github.com/grokify/html-strip-tags-go" ) // ClearText clear HTML, get the clear text diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go index 568299af..71aafbb6 100644 --- a/pkg/htmltext/htmltext_test.go +++ b/pkg/htmltext/htmltext_test.go @@ -1,9 +1,8 @@ package htmltext import ( - "testing" - "github.com/stretchr/testify/assert" + "testing" ) func TestClearText(t *testing.T) { From 498b7f489334d7c75b5f82b44e9f7d0e53ffbc7e Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 17:42:10 +0800 Subject: [PATCH 131/157] fix: upload add siteinfo service --- cmd/answer/wire_gen.go | 2 +- internal/service/uploader/upload.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 302a67de..2e002347 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -108,7 +108,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) - uploaderService := uploader.NewUploaderService(serviceConf) + uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService) userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService) commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index f4a8e61d..1d6d2491 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -33,7 +33,8 @@ type UploaderService struct { } // NewUploaderService new upload service -func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderService { +func NewUploaderService(serviceConfig *service_config.ServiceConfig, + siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService { err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath)) if err != nil { panic(err) @@ -43,7 +44,8 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderSe panic(err) } return &UploaderService{ - serviceConfig: serviceConfig, + serviceConfig: serviceConfig, + siteInfoService: siteInfoService, } } From 596b104e3c593c051cd0a80279eafec6f47415cc Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 18:00:58 +0800 Subject: [PATCH 132/157] fix: table add width --- ui/src/pages/Admin/Answers/index.tsx | 15 ++++++++++----- ui/src/pages/Admin/Questions/index.tsx | 17 +++++++++++------ ui/src/pages/Admin/Users/index.tsx | 16 +++++++++------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 3a33fe49..cc584c31 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -110,10 +110,12 @@ const Answers: FC = () => {
- - - - {curFilter !== 'deleted' && } + + + + {curFilter !== 'deleted' && ( + + )} @@ -164,7 +166,10 @@ const Answers: FC = () => { {curFilter !== 'deleted' && ( diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index c2f5e5c6..76a65de9 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -126,12 +126,14 @@ const Questions: FC = () => {
{t('post')}{t('post')} {t('votes')}{t('created')}{t('created')} {t('status')}{t('action')}
{li.vote_count} - {li.answer_count} - + From 18e880b9c872a1e649a961b81a553e07d4aa8607 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 15:02:27 +0800 Subject: [PATCH 014/157] feat: add time zone config --- docs/docs.go | 30 ++++++++++++------- docs/swagger.json | 30 ++++++++++++------- docs/swagger.yaml | 24 ++++++++++----- .../siteinfo_controller.go | 24 +++++++-------- internal/schema/siteinfo_schema.go | 13 ++++---- 5 files changed, 75 insertions(+), 46 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index f21f508c..507c0d3b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -487,14 +487,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -522,14 +522,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -558,14 +558,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "get site interface", "parameters": [ { "description": "general", @@ -604,14 +604,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -5326,7 +5326,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5340,6 +5341,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5347,7 +5352,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5361,6 +5367,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 039f67f5..caa69fd0 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -475,14 +475,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -510,14 +510,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -546,14 +546,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "get site interface", "parameters": [ { "description": "general", @@ -592,14 +592,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -5314,7 +5314,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5328,6 +5329,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5335,7 +5340,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5349,6 +5355,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c419b60e..fd74cb63 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1024,9 +1024,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.SiteInterfaceResp: properties: @@ -1039,9 +1043,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.TagItem: properties: @@ -1662,7 +1670,7 @@ paths: - admin /answer/admin/api/siteinfo/general: get: - description: Get siteinfo general + description: get site general information produces: - application/json responses: @@ -1677,11 +1685,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo general + summary: get site general information tags: - admin put: - description: Get siteinfo interface + description: update site general information parameters: - description: general in: body @@ -1698,12 +1706,12 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site general information tags: - admin /answer/admin/api/siteinfo/interface: get: - description: Get siteinfo interface + description: get site interface parameters: - description: general in: body @@ -1725,11 +1733,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: get site interface tags: - admin put: - description: Get siteinfo interface + description: update site info interface parameters: - description: general in: body @@ -1746,7 +1754,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site info interface tags: - admin /answer/admin/api/theme/options: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 30bfd1bf..821b517e 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -18,9 +18,9 @@ func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoCo } } -// GetGeneral godoc -// @Summary Get siteinfo general -// @Description Get siteinfo general +// GetGeneral get site general information +// @Summary get site general information +// @Description get site general information // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -31,9 +31,9 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// GetInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// GetInterface get site interface +// @Summary get site interface +// @Description get site interface // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -45,9 +45,9 @@ func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// UpdateGeneral godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// UpdateGeneral update site general information +// @Summary update site general information +// @Description update site general information // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -63,9 +63,9 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) } -// UpdateInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// UpdateInterface update site interface +// @Summary update site info interface +// @Description update site info interface // @Security ApiKeyAuth // @Tags admin // @Produce json diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index ff93e72d..446b986d 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -2,16 +2,17 @@ package schema // SiteGeneralReq site general request type SiteGeneralReq struct { - Name string `validate:"required,gt=1,lte=128" comment:"site name" form:"name" json:"name"` - ShortDescription string `validate:"required,gt=3,lte=255" comment:"short site description" form:"short_description" json:"short_description"` - Description string `validate:"required,gt=3,lte=2000" comment:"site description" form:"description" json:"description"` + Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` + ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` + Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` } // SiteInterfaceReq site interface request type SiteInterfaceReq struct { - Logo string `validate:"omitempty,gt=0,lte=256" comment:"logo" form:"logo" json:"logo"` - Theme string `validate:"required,gt=1,lte=128" comment:"theme" form:"theme" json:"theme"` - Language string `validate:"required,gt=1,lte=128" comment:"interface language" form:"language" json:"language"` + Logo string `validate:"omitempty,gt=0,lte=256" form:"logo" json:"logo"` + Theme string `validate:"required,gt=1,lte=128" form:"theme" json:"theme"` + Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"` + TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"` } // SiteGeneralResp site general response From 720754c8bbbf88d4f30adf9f2f4eb03ef1514127 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 15:25:27 +0800 Subject: [PATCH 015/157] feat: support to config the default language --- cmd/answer/wire_gen.go | 12 ++++---- i18n/i18n.yaml | 6 ++++ internal/base/translator/provider.go | 41 ++++++++++++++++++++++++++ internal/controller/lang_controller.go | 41 +++++++++++++++++++++----- internal/controller/user_controller.go | 21 +++++++++++++ internal/entity/user_entity.go | 1 + internal/migrations/migrations.go | 12 ++++---- internal/migrations/v1.go | 12 ++++++++ internal/repo/user/user_repo.go | 8 +++++ internal/router/answer_api_router.go | 5 ++-- internal/router/ui.go | 2 ++ internal/schema/lang_schema.go | 18 ----------- internal/schema/user_schema.go | 10 +++++++ internal/service/siteinfo_service.go | 16 ++++------ internal/service/user_common/user.go | 1 + internal/service/user_service.go | 13 ++++++++ 16 files changed, 168 insertions(+), 51 deletions(-) create mode 100644 i18n/i18n.yaml create mode 100644 internal/migrations/v1.go delete mode 100644 internal/schema/lang_schema.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index a965ed8b..746321b7 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -76,7 +76,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, if err != nil { return nil, nil, err } - langController := controller.NewLangController(i18nTranslator) engine, err := data.NewDB(debug, dbConf) if err != nil { return nil, nil, err @@ -90,17 +89,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cleanup() return nil, nil, err } + siteInfoRepo := site_info.NewSiteInfo(dataData) + configRepo := config.NewConfigRepo(dataData) + emailRepo := export.NewEmailRepo(dataData) + emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) + siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) + langController := controller.NewLangController(i18nTranslator, siteInfoService) authRepo := auth.NewAuthRepo(dataData) authService := auth2.NewAuthService(authRepo) - configRepo := config.NewConfigRepo(dataData) userRepo := user.NewUserRepo(dataData, configRepo) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo) userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) - emailRepo := export.NewEmailRepo(dataData) - siteInfoRepo := site_info.NewSiteInfo(dataData) - emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) @@ -166,7 +167,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_backyard.NewThemeController() - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) siteinfoController := controller.NewSiteinfoController(siteInfoService) notificationRepo := notification.NewNotificationRepo(dataData) diff --git a/i18n/i18n.yaml b/i18n/i18n.yaml new file mode 100644 index 00000000..13a6c522 --- /dev/null +++ b/i18n/i18n.yaml @@ -0,0 +1,6 @@ +# all support language +language_options: + - label: "简体中文(CN)" + value: "zh_CN" + - label: "English(US)" + value: "en_US" diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index 0c0bcac6..a81576bd 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -1,17 +1,58 @@ package translator import ( + "fmt" + "os" + "path/filepath" + "github.com/google/wire" myTran "github.com/segmentfault/pacman/contrib/i18n" "github.com/segmentfault/pacman/i18n" + "sigs.k8s.io/yaml" ) // ProviderSet is providers. var ProviderSet = wire.NewSet(NewTranslator) var GlobalTrans i18n.Translator +// LangOption language option +type LangOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// LanguageOptions language +var LanguageOptions []*LangOption + // NewTranslator new a translator func NewTranslator(c *I18n) (tr i18n.Translator, err error) { GlobalTrans, err = myTran.NewTranslator(c.BundleDir) + if err != nil { + return nil, err + } + + i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml")) + if err != nil { + return nil, fmt.Errorf("read i18n file failed: %s", err) + } + + s := struct { + LangOption []*LangOption `json:"language_options"` + }{} + err = yaml.Unmarshal(i18nFile, &s) + if err != nil { + return nil, fmt.Errorf("i18n file parsing failed: %s", err) + } + LanguageOptions = s.LangOption return GlobalTrans, err } + +// CheckLanguageIsValid check user input language is valid +func CheckLanguageIsValid(lang string) bool { + for _, option := range LanguageOptions { + if option.Value == lang { + return true + } + } + return false +} diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index a689fffc..384d711c 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -4,18 +4,20 @@ import ( "encoding/json" "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/internal/base/translator" + "github.com/answerdev/answer/internal/service" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { - translator i18n.Translator + translator i18n.Translator + siteInfoService *service.SiteInfoService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator) *LangController { - return &LangController{translator: tr} +func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController { + return &LangController{translator: tr, siteInfoService: siteInfoService} } // GetLangMapping get language config mapping @@ -33,15 +35,38 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) { handler.HandleResponse(ctx, nil, resp) } -// GetLangOptions Get language options +// GetAdminLangOptions Get language options // @Summary Get language options // @Description Get language options -// @Security ApiKeyAuth // @Tags Lang // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/language/options [get] // @Router /answer/admin/api/language/options [get] -func (u *LangController) GetLangOptions(ctx *gin.Context) { - handler.HandleResponse(ctx, nil, schema.GetLangOptions) +func (u *LangController) GetAdminLangOptions(ctx *gin.Context) { + handler.HandleResponse(ctx, nil, translator.LanguageOptions) +} + +// GetUserLangOptions Get language options +// @Summary Get language options +// @Description Get language options +// @Tags Lang +// @Produce json +// @Success 200 {object} handler.RespBody{} +// @Router /answer/api/v1/language/options [get] +func (u *LangController) GetUserLangOptions(ctx *gin.Context) { + siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + options := translator.LanguageOptions + if len(siteInterfaceResp.Language) > 0 { + defaultOption := []*translator.LangOption{ + {Label: "Default", Value: siteInterfaceResp.Language}, + } + options = append(defaultOption, options...) + } + handler.HandleResponse(ctx, nil, options) } diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 495b1659..028f9302 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -373,6 +373,27 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) } +// UserUpdateInterface update user interface config +// @Summary UserUpdateInterface update user interface config +// @Description UserUpdateInterface update user interface config +// @Tags User +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param Authorization header string true "access-token" +// @Param data body schema.UpdateUserInterfaceRequest true "UpdateInfoRequest" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/user/interface [put] +func (uc *UserController) UserUpdateInterface(ctx *gin.Context) { + req := &schema.UpdateUserInterfaceRequest{} + if handler.BindAndCheck(ctx, req) { + return + } + req.UserId = middleware.GetLoginUserIDFromContext(ctx) + err := uc.userService.UserUpdateInterface(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + // UploadUserAvatar godoc // @Summary UserUpdateInfo // @Description UserUpdateInfo diff --git a/internal/entity/user_entity.go b/internal/entity/user_entity.go index 7ab239a7..5d615482 100644 --- a/internal/entity/user_entity.go +++ b/internal/entity/user_entity.go @@ -45,6 +45,7 @@ type User struct { Location string `xorm:"not null default '' VARCHAR(100) location"` IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"` IsAdmin bool `xorm:"not null default false BOOL is_admin"` + Language string `xorm:"not null default '' VARCHAR(100) language"` } // TableName user table name diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index f4f4efd8..200e4813 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -5,7 +5,6 @@ import ( "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/entity" - "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) @@ -43,6 +42,7 @@ var noopMigration = func(_ *xorm.Engine) error { return nil } var migrations = []Migration{ // 0->1 NewMigration("this is first version, no operation", noopMigration), + NewMigration("add user language", addUserLanguage), } // GetCurrentDBVersion returns the current db version @@ -86,17 +86,17 @@ func Migrate(dataConf *data.Database) error { expectedVersion := ExpectedVersion() for currentDBVersion < expectedVersion { - log.Infof("[migrate] current db version is %d, try to migrate version %d, latest version is %d", + fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n", currentDBVersion, currentDBVersion+1, expectedVersion) migrationFunc := migrations[currentDBVersion] - log.Infof("[migrate] try to migrate db version %d, description: %s", currentDBVersion+1, migrationFunc.Description()) + fmt.Printf("[migrate] try to migrate db version %d, description: %s\n", currentDBVersion+1, migrationFunc.Description()) if err := migrationFunc.Migrate(engine); err != nil { - log.Errorf("[migrate] migrate to db version %d failed: ", currentDBVersion+1, err.Error()) + fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error()) return err } - log.Infof("[migrate] migrate to db version %d success", currentDBVersion+1) + fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1) if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil { - log.Errorf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error()) + fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error()) return err } currentDBVersion++ diff --git a/internal/migrations/v1.go b/internal/migrations/v1.go new file mode 100644 index 00000000..b9e316d9 --- /dev/null +++ b/internal/migrations/v1.go @@ -0,0 +1,12 @@ +package migrations + +import ( + "xorm.io/xorm" +) + +func addUserLanguage(x *xorm.Engine) error { + type User struct { + Language string `xorm:"not null default '' VARCHAR(100) language"` + } + return x.Sync(new(User)) +} diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index db1679ed..feaf675d 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -101,6 +101,14 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err return } +func (ur *userRepo) UpdateLanguage(ctx context.Context, userID, language string) (err error) { + _, err = ur.data.DB.Where("id = ?", userID).Update(&entity.User{Language: language}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // UpdateInfo update user info func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) { _, err = ur.data.DB.Where("id = ?", userInfo.ID). diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 56a972a8..06848687 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -79,7 +79,7 @@ func NewAnswerAPIRouter( func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // i18n r.GET("/language/config", a.langController.GetLangMapping) - r.GET("/language/options", a.langController.GetLangOptions) + r.GET("/language/options", a.langController.GetUserLangOptions) // comment r.GET("/comment/page", a.commentController.GetCommentWithPage) @@ -177,6 +177,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { // user r.PUT("/user/password", a.userController.UserModifyPassWord) r.PUT("/user/info", a.userController.UserUpdateInfo) + r.PUT("/user/interface", a.userController.UserUpdateInterface) r.POST("/user/avatar/upload", a.userController.UploadUserAvatar) r.POST("/user/post/file", a.userController.UploadUserPostFile) r.POST("/user/notice/set", a.userController.UserNoticeSet) @@ -213,7 +214,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) { r.GET("/reasons", a.reasonController.Reasons) // language - r.GET("/language/options", a.langController.GetLangOptions) + r.GET("/language/options", a.langController.GetAdminLangOptions) // theme r.GET("/theme/options", a.themeController.GetThemeOptions) diff --git a/internal/router/ui.go b/internal/router/ui.go index 3498265c..4d465716 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -7,6 +7,7 @@ import ( "net/http" "os" + "github.com/answerdev/answer/i18n" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -52,6 +53,7 @@ func (a *UIRouter) Register(r *gin.Engine) { r.LoadHTMLGlob(staticPath + "/*.html") r.Static("/static", staticPath+"/static") + r.StaticFS("/i18n/", http.FS(i18n.I18n)) r.NoRoute(func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }) diff --git a/internal/schema/lang_schema.go b/internal/schema/lang_schema.go deleted file mode 100644 index 98abe874..00000000 --- a/internal/schema/lang_schema.go +++ /dev/null @@ -1,18 +0,0 @@ -package schema - -// GetLangOption get label option -type GetLangOption struct { - Label string `json:"label"` - Value string `json:"value"` -} - -var GetLangOptions = []*GetLangOption{ - { - Label: "English(US)", - Value: "en_US", - }, - { - Label: "中文(CN)", - Value: "zh_CN", - }, -} diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index d871d5ea..d9455101 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -62,6 +62,8 @@ type GetUserResp struct { Location string `json:"location"` // ip info IPInfo string `json:"ip_info"` + // language + Language string `json:"language"` // access token AccessToken string `json:"access_token"` // is admin @@ -305,6 +307,14 @@ func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error) return nil, nil } +// UpdateUserInterfaceRequest update user interface request +type UpdateUserInterfaceRequest struct { + // language + Language string `validate:"required,gt=1,lte=100" json:"language"` + // user id + UserId string `json:"-" ` +} + type UserRetrievePassWordRequest struct { Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail CaptchaID string `json:"captcha_id" ` // captcha_id diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go index a4f23fd3..03e598e4 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo_service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/export" @@ -77,10 +78,9 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) { var ( - siteType = "interface" - themeExist, - langExist bool - content []byte + siteType = "interface" + themeExist bool + content []byte ) // check theme @@ -96,13 +96,7 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site } // check language - for _, lang := range schema.GetLangOptions { - if lang.Value == req.Language { - langExist = true - break - } - } - if !langExist { + if !translator.CheckLanguageIsValid(req.Language) { err = errors.BadRequest(reason.LangNotFound) return } diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 5cc194b6..86f9566d 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -15,6 +15,7 @@ type UserRepo interface { UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error UpdateEmail(ctx context.Context, userID, email string) error + UpdateLanguage(ctx context.Context, userID, language string) error UpdatePass(ctx context.Context, userID, pass string) error UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 17341095..fbc22741 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -11,6 +11,7 @@ import ( "github.com/Chain-Zhang/pinyin" "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity" @@ -283,6 +284,18 @@ func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, er return has, nil } +// UserUpdateInterface update user interface +func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) { + if !translator.CheckLanguageIsValid(req.Language) { + return errors.BadRequest(reason.LangNotFound) + } + err = us.userRepo.UpdateLanguage(ctx, req.UserId, req.Language) + if err != nil { + return + } + return nil +} + // UserRegisterByEmail user register func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) ( resp *schema.GetUserResp, err error, From ca09c60ee1a906f53274ed37870ac4571d5cdff4 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 15:27:52 +0800 Subject: [PATCH 016/157] doc: update swagger --- docs/docs.go | 77 +++++++++++++++++++++++++++++++++++++++++------ docs/swagger.json | 77 +++++++++++++++++++++++++++++++++++++++++------ docs/swagger.yaml | 48 ++++++++++++++++++++++++++--- 3 files changed, 178 insertions(+), 24 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index f21f508c..7a37d7e3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -121,11 +121,6 @@ const docTemplate = `{ }, "/answer/admin/api/language/options": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Get language options", "produces": [ "application/json" @@ -1432,11 +1427,6 @@ const docTemplate = `{ }, "/answer/api/v1/language/options": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Get language options", "produces": [ "application/json" @@ -3391,6 +3381,52 @@ const docTemplate = `{ } } }, + "/answer/api/v1/user/interface": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserUpdateInterface update user interface config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserUpdateInterface update user interface config", + "parameters": [ + { + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "UpdateInfoRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", @@ -4822,6 +4858,10 @@ const docTemplate = `{ "description": "is admin", "type": "boolean" }, + "language": { + "description": "language", + "type": "string" + }, "last_login_date": { "description": "last login date", "type": "integer" @@ -4918,6 +4958,10 @@ const docTemplate = `{ "description": "is admin", "type": "boolean" }, + "language": { + "description": "language", + "type": "string" + }, "last_login_date": { "description": "last login date", "type": "integer" @@ -5573,6 +5617,19 @@ const docTemplate = `{ } } }, + "schema.UpdateUserInterfaceRequest": { + "type": "object", + "required": [ + "language" + ], + "properties": { + "language": { + "description": "language", + "type": "string", + "maxLength": 100 + } + } + }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 039f67f5..405c5d2a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -109,11 +109,6 @@ }, "/answer/admin/api/language/options": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Get language options", "produces": [ "application/json" @@ -1420,11 +1415,6 @@ }, "/answer/api/v1/language/options": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Get language options", "produces": [ "application/json" @@ -3379,6 +3369,52 @@ } } }, + "/answer/api/v1/user/interface": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserUpdateInterface update user interface config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserUpdateInterface update user interface config", + "parameters": [ + { + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "UpdateInfoRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", @@ -4810,6 +4846,10 @@ "description": "is admin", "type": "boolean" }, + "language": { + "description": "language", + "type": "string" + }, "last_login_date": { "description": "last login date", "type": "integer" @@ -4906,6 +4946,10 @@ "description": "is admin", "type": "boolean" }, + "language": { + "description": "language", + "type": "string" + }, "last_login_date": { "description": "last login date", "type": "integer" @@ -5561,6 +5605,19 @@ } } }, + "schema.UpdateUserInterfaceRequest": { + "type": "object", + "required": [ + "language" + ], + "properties": { + "language": { + "description": "language", + "type": "string", + "maxLength": 100 + } + } + }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c419b60e..d2a9dab8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -652,6 +652,9 @@ definitions: is_admin: description: is admin type: boolean + language: + description: language + type: string last_login_date: description: last login date type: integer @@ -723,6 +726,9 @@ definitions: is_admin: description: is admin type: boolean + language: + description: language + type: string last_login_date: description: last login date type: integer @@ -1195,6 +1201,15 @@ definitions: - synonym_tag_list - tag_id type: object + schema.UpdateUserInterfaceRequest: + properties: + language: + description: language + maxLength: 100 + type: string + required: + - language + type: object schema.UpdateUserStatusReq: properties: status: @@ -1444,8 +1459,6 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] summary: Get language options tags: - Lang @@ -2237,8 +2250,6 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] summary: Get language options tags: - Lang @@ -3425,6 +3436,35 @@ paths: summary: UserUpdateInfo update user info tags: - User + /answer/api/v1/user/interface: + put: + consumes: + - application/json + description: UserUpdateInterface update user interface config + parameters: + - description: access-token + in: header + name: Authorization + required: true + type: string + - description: UpdateInfoRequest + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateUserInterfaceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: UserUpdateInterface update user interface config + tags: + - User /answer/api/v1/user/login/email: post: consumes: From 872689b93a87824bb7868174de7ea10592b58fc3 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 15:50:32 +0800 Subject: [PATCH 017/157] add dashboard service --- cmd/answer/wire_gen.go | 4 +- internal/controller/answer_controller.go | 27 +++++- internal/repo/activity_common/vote.go | 11 +++ internal/repo/answer/answer_repo.go | 11 +++ internal/repo/comment/comment_repo.go | 9 ++ internal/repo/question/question_repo.go | 11 +++ internal/router/answer_api_router.go | 3 + internal/service/activity_common/vote.go | 1 + internal/service/answer_common/answer.go | 1 + .../service/comment_common/comment_service.go | 1 + .../service/dashboard/dashboard_service.go | 85 +++++++++++++++++++ internal/service/dashboard/dashboard_test.go | 1 + internal/service/provider.go | 2 + internal/service/question_common/question.go | 1 + 14 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 internal/service/dashboard/dashboard_service.go create mode 100644 internal/service/dashboard/dashboard_test.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index a965ed8b..1a1720ff 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -44,6 +44,7 @@ import ( auth2 "github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/collection_common" comment2 "github.com/answerdev/answer/internal/service/comment" + "github.com/answerdev/answer/internal/service/dashboard" export2 "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/follow" meta2 "github.com/answerdev/answer/internal/service/meta" @@ -148,7 +149,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - answerController := controller.NewAnswerController(answerService, rankService) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo) + answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) searchController := controller.NewSearchController(searchService) diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go index 43ea51a5..842b107f 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -9,20 +9,30 @@ import ( "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/dashboard" "github.com/answerdev/answer/internal/service/rank" + "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // AnswerController answer controller type AnswerController struct { - answerService *service.AnswerService - rankService *rank.RankService + answerService *service.AnswerService + rankService *rank.RankService + dashboardService *dashboard.DashboardService } // NewAnswerController new controller -func NewAnswerController(answerService *service.AnswerService, rankService *rank.RankService) *AnswerController { - return &AnswerController{answerService: answerService, rankService: rankService} +func NewAnswerController(answerService *service.AnswerService, + rankService *rank.RankService, + dashboardService *dashboard.DashboardService, +) *AnswerController { + return &AnswerController{ + answerService: answerService, + rankService: rankService, + dashboardService: dashboardService, + } } // RemoveAnswer delete answer @@ -236,3 +246,12 @@ func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) { err := ac.answerService.AdminSetAnswerStatus(ctx, req.AnswerID, req.StatusStr) handler.HandleResponse(ctx, err, gin.H{}) } + +// dashboardService +func (ac *AnswerController) Dashboard(ctx *gin.Context) { + err := ac.dashboardService.Statistical(ctx) + spew.Dump(err) + handler.HandleResponse(ctx, err, gin.H{ + "ping": "pong", + }) +} diff --git a/internal/repo/activity_common/vote.go b/internal/repo/activity_common/vote.go index abbea28f..16f73fc4 100644 --- a/internal/repo/activity_common/vote.go +++ b/internal/repo/activity_common/vote.go @@ -4,8 +4,10 @@ import ( "context" "github.com/answerdev/answer/internal/base/data" + "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/service/activity_common" + "github.com/segmentfault/pacman/errors" ) // VoteRepo activity repository @@ -39,3 +41,12 @@ func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string) } return "" } + +func (vr *VoteRepo) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) { + list := make([]*entity.Activity, 0) + count, err = vr.data.DB.Where("cancelled =0").In("activity_type", activityTypes).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index 2020fc2f..ac6c974d 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -5,6 +5,7 @@ import ( "strings" "time" "unicode" + "xorm.io/builder" "github.com/answerdev/answer/internal/base/constant" @@ -102,6 +103,16 @@ func (ar *answerRepo) GetAnswer(ctx context.Context, id string) ( return } +// GetQuestionCount +func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.Answer, 0) + count, err = ar.data.DB.Where("status = ?", entity.AnswerStatusAvailable).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // GetAnswerList get answer list all func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) { answerList = make([]*entity.Answer, 0) diff --git a/internal/repo/comment/comment_repo.go b/internal/repo/comment/comment_repo.go index a1a7ab90..992e6a2f 100644 --- a/internal/repo/comment/comment_repo.go +++ b/internal/repo/comment/comment_repo.go @@ -79,6 +79,15 @@ func (cr *commentRepo) GetComment(ctx context.Context, commentID string) ( return } +func (cr *commentRepo) GetCommentCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.Comment, 0) + count, err = cr.data.DB.Where("status = ?", entity.CommentStatusAvailable).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // GetCommentPage get comment page func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) ( commentList []*entity.Comment, total int64, err error, diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 2e26006f..4d90a09e 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -5,6 +5,7 @@ import ( "strings" "time" "unicode" + "xorm.io/builder" "github.com/answerdev/answer/internal/base/constant" @@ -162,6 +163,16 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu return } +func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) { + questionList := make([]*entity.Question, 0) + + count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}).FindAndCount(&questionList) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // GetQuestionPage get question page func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) { questionList = make([]*entity.Question, 0) diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 56a972a8..68192ecb 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -86,6 +86,9 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage) r.GET("/comment", a.commentController.GetComment) + //test + r.GET("/test", a.answerController.Dashboard) + // user r.GET("/user/info", a.userController.GetUserInfoByUserID) r.GET("/user/status", a.userController.GetUserStatus) diff --git a/internal/service/activity_common/vote.go b/internal/service/activity_common/vote.go index fb97885b..16de6967 100644 --- a/internal/service/activity_common/vote.go +++ b/internal/service/activity_common/vote.go @@ -7,4 +7,5 @@ import ( // VoteRepo activity repository type VoteRepo interface { GetVoteStatus(ctx context.Context, objectId, userId string) (status string) + GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) } diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index c84a1d15..ef611351 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -20,6 +20,7 @@ type AnswerRepo interface { SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error) + GetAnswerCount(ctx context.Context) (count int64, err error) } // AnswerCommon user service diff --git a/internal/service/comment_common/comment_service.go b/internal/service/comment_common/comment_service.go index dc1f9fc9..19c2b90a 100644 --- a/internal/service/comment_common/comment_service.go +++ b/internal/service/comment_common/comment_service.go @@ -12,6 +12,7 @@ import ( // CommentCommonRepo comment repository type CommentCommonRepo interface { GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) + GetCommentCount(ctx context.Context) (count int64, err error) } // CommentCommonService user service diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go new file mode 100644 index 00000000..1d0f5400 --- /dev/null +++ b/internal/service/dashboard/dashboard_service.go @@ -0,0 +1,85 @@ +package dashboard + +import ( + "context" + + "github.com/answerdev/answer/internal/service/activity_common" + answercommon "github.com/answerdev/answer/internal/service/answer_common" + "github.com/answerdev/answer/internal/service/comment_common" + "github.com/answerdev/answer/internal/service/config" + questioncommon "github.com/answerdev/answer/internal/service/question_common" + "github.com/answerdev/answer/internal/service/report_common" + usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/davecgh/go-spew/spew" +) + +type DashboardService struct { + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + commentRepo comment_common.CommentCommonRepo + voteRepo activity_common.VoteRepo + userRepo usercommon.UserRepo + reportRepo report_common.ReportRepo + configRepo config.ConfigRepo +} + +func NewDashboardService( + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, + commentRepo comment_common.CommentCommonRepo, + voteRepo activity_common.VoteRepo, + userRepo usercommon.UserRepo, + reportRepo report_common.ReportRepo, + configRepo config.ConfigRepo, + +) *DashboardService { + return &DashboardService{ + questionRepo: questionRepo, + answerRepo: answerRepo, + commentRepo: commentRepo, + voteRepo: voteRepo, + userRepo: userRepo, + reportRepo: reportRepo, + configRepo: configRepo, + } +} + +// Statistical +func (ds *DashboardService) Statistical(ctx context.Context) error { + questionCount, err := ds.questionRepo.GetQuestionCount(ctx) + if err != nil { + return err + } + answerCount, err := ds.answerRepo.GetAnswerCount(ctx) + if err != nil { + return err + } + commentCount, err := ds.commentRepo.GetCommentCount(ctx) + if err != nil { + return err + } + + typeKeys := []string{ + "question.vote_up", + "question.vote_down", + "answer.vote_up", + "answer.vote_down", + } + var activityTypes []int + + for _, typeKey := range typeKeys { + var t int + t, err = ds.configRepo.GetConfigType(typeKey) + if err != nil { + continue + } + activityTypes = append(activityTypes, t) + } + + voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes) + if err != nil { + return err + } + spew.Dump(questionCount, answerCount, commentCount, activityTypes, voteCount) + return nil +} diff --git a/internal/service/dashboard/dashboard_test.go b/internal/service/dashboard/dashboard_test.go new file mode 100644 index 00000000..cfdd5f81 --- /dev/null +++ b/internal/service/dashboard/dashboard_test.go @@ -0,0 +1 @@ +package dashboard diff --git a/internal/service/provider.go b/internal/service/provider.go index 3901766e..4562572a 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -8,6 +8,7 @@ import ( collectioncommon "github.com/answerdev/answer/internal/service/collection_common" "github.com/answerdev/answer/internal/service/comment" "github.com/answerdev/answer/internal/service/comment_common" + "github.com/answerdev/answer/internal/service/dashboard" "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/follow" "github.com/answerdev/answer/internal/service/meta" @@ -65,4 +66,5 @@ var ProviderSetService = wire.NewSet( notficationcommon.NewNotificationCommon, notification.NewNotificationService, activity.NewAnswerActivityService, + dashboard.NewDashboardService, ) diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index 3cb944ff..e8b97e18 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -38,6 +38,7 @@ type QuestionRepo interface { UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error) + GetQuestionCount(ctx context.Context) (count int64, err error) } // QuestionCommon user service From 11ebf6e96bb6c91bb4d6d77343194606783353f2 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 16:01:17 +0800 Subject: [PATCH 018/157] fix: swagger wrong request parameter --- docs/docs.go | 11 ----------- docs/swagger.json | 11 ----------- docs/swagger.yaml | 7 ------- internal/controller_backyard/siteinfo_controller.go | 1 - 4 files changed, 30 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 507c0d3b..03a20ec9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -566,17 +566,6 @@ const docTemplate = `{ "admin" ], "summary": "get site interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], "responses": { "200": { "description": "OK", diff --git a/docs/swagger.json b/docs/swagger.json index caa69fd0..7062f7eb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -554,17 +554,6 @@ "admin" ], "summary": "get site interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], "responses": { "200": { "description": "OK", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fd74cb63..3d707b46 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1712,13 +1712,6 @@ paths: /answer/admin/api/siteinfo/interface: get: description: get site interface - parameters: - - description: general - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.AddCommentReq' produces: - application/json responses: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 821b517e..339765aa 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -39,7 +39,6 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp} // @Router /answer/admin/api/siteinfo/interface [get] -// @Param data body schema.AddCommentReq true "general" func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) From bbbc1db10566e869ab4f9df42858ed3db1afe612 Mon Sep 17 00:00:00 2001 From: ppchart Date: Wed, 2 Nov 2022 16:20:53 +0800 Subject: [PATCH 019/157] Only allow use pnpm --- ui/package.json | 5 +++-- ui/scripts/preinstall.js | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 ui/scripts/preinstall.js diff --git a/ui/package.json b/ui/package.json index 069456ef..c7512460 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,7 +15,8 @@ "prepare": "cd .. && husky install", "cz": "cz", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", - "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" + "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"", + "preinstall": "node ./scripts/preinstall.js" }, "config": { "commitizen": { @@ -109,4 +110,4 @@ "pnpm": ">=7" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/ui/scripts/preinstall.js b/ui/scripts/preinstall.js new file mode 100644 index 00000000..076ad606 --- /dev/null +++ b/ui/scripts/preinstall.js @@ -0,0 +1,5 @@ +// There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed. +if (!/pnpm/.test(process.env.npm_execpath)) { + console.warn(`\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\u001b[39m\n`) + process.exit(1) +} \ No newline at end of file From e84a681ce43398ff76974c0b892b302ceceb7256 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 16:29:10 +0800 Subject: [PATCH 020/157] add dashboard info --- cmd/answer/wire_gen.go | 3 +- docs/docs.go | 28 +++++++++++++++ docs/swagger.json | 28 +++++++++++++++ docs/swagger.yaml | 17 +++++++++ internal/controller/answer_controller.go | 10 ------ internal/controller/controller.go | 1 + internal/controller/dashboard_controller.go | 36 +++++++++++++++++++ internal/repo/report/report_repo.go | 9 +++++ internal/repo/user/user_repo.go | 9 +++++ internal/router/answer_api_router.go | 9 +++-- internal/schema/dashboard_schema.go | 15 ++++++++ .../service/dashboard/dashboard_service.go | 36 ++++++++++++++----- .../service/report_common/report_common.go | 2 ++ internal/service/user_common/user.go | 1 + 14 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 internal/controller/dashboard_controller.go create mode 100644 internal/schema/dashboard_schema.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 1a1720ff..2d902504 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -175,7 +175,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) notificationController := controller.NewNotificationController(notificationService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController) + dashboardController := controller.NewDashboardController(dashboardService) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter() authUserMiddleware := middleware.NewAuthUserMiddleware(authService) diff --git a/docs/docs.go b/docs/docs.go index 4162d8a0..4f0bfb5a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -113,6 +113,34 @@ const docTemplate = `{ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "security": [ diff --git a/docs/swagger.json b/docs/swagger.json index e60de534..e856d606 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -101,6 +101,34 @@ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "security": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 810cfef8..adc19f98 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1430,6 +1430,23 @@ paths: summary: AdminSetAnswerStatus tags: - admin + /answer/admin/api/dashboard: + get: + consumes: + - application/json + description: DashboardInfo + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: DashboardInfo + tags: + - admin /answer/admin/api/language/options: get: description: Get language options diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go index 842b107f..a6443a91 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -11,7 +11,6 @@ import ( "github.com/answerdev/answer/internal/service" "github.com/answerdev/answer/internal/service/dashboard" "github.com/answerdev/answer/internal/service/rank" - "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) @@ -246,12 +245,3 @@ func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) { err := ac.answerService.AdminSetAnswerStatus(ctx, req.AnswerID, req.StatusStr) handler.HandleResponse(ctx, err, gin.H{}) } - -// dashboardService -func (ac *AnswerController) Dashboard(ctx *gin.Context) { - err := ac.dashboardService.Statistical(ctx) - spew.Dump(err) - handler.HandleResponse(ctx, err, gin.H{ - "ping": "pong", - }) -} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 65776043..4a76acf7 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -20,4 +20,5 @@ var ProviderSetController = wire.NewSet( NewReasonController, NewNotificationController, NewSiteinfoController, + NewDashboardController, ) diff --git a/internal/controller/dashboard_controller.go b/internal/controller/dashboard_controller.go new file mode 100644 index 00000000..6ee50f4c --- /dev/null +++ b/internal/controller/dashboard_controller.go @@ -0,0 +1,36 @@ +package controller + +import ( + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/service/dashboard" + "github.com/gin-gonic/gin" +) + +type DashboardController struct { + dashboardService *dashboard.DashboardService +} + +// NewDashboardController new controller +func NewDashboardController( + dashboardService *dashboard.DashboardService, +) *DashboardController { + return &DashboardController{ + dashboardService: dashboardService, + } +} + +// DashboardInfo godoc +// @Summary DashboardInfo +// @Description DashboardInfo +// @Tags admin +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Router /answer/admin/api/dashboard [get] +// @Success 200 {object} handler.RespBody +func (ac *DashboardController) DashboardInfo(ctx *gin.Context) { + info, err := ac.dashboardService.Statistical(ctx) + handler.HandleResponse(ctx, err, gin.H{ + "info": info, + }) +} diff --git a/internal/repo/report/report_repo.go b/internal/repo/report/report_repo.go index 1ebf820e..08747246 100644 --- a/internal/repo/report/report_repo.go +++ b/internal/repo/report/report_repo.go @@ -94,3 +94,12 @@ func (ar *reportRepo) UpdateByID( } return } + +func (vr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.Report, 0) + count, err = vr.data.DB.Where("status =?", entity.ReportStatusPending).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index db1679ed..f6ba0b6a 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -149,3 +149,12 @@ func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *ent } return } + +func (vr *userRepo) GetUserCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.User, 0) + count, err = vr.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 68192ecb..dbe34405 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -27,6 +27,7 @@ type AnswerAPIRouter struct { siteInfoController *controller_backyard.SiteInfoController siteinfoController *controller.SiteinfoController notificationController *controller.NotificationController + dashboardController *controller.DashboardController } func NewAnswerAPIRouter( @@ -50,6 +51,7 @@ func NewAnswerAPIRouter( siteInfoController *controller_backyard.SiteInfoController, siteinfoController *controller.SiteinfoController, notificationController *controller.NotificationController, + dashboardController *controller.DashboardController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ @@ -73,6 +75,7 @@ func NewAnswerAPIRouter( siteInfoController: siteInfoController, notificationController: notificationController, siteinfoController: siteinfoController, + dashboardController: dashboardController, } } @@ -86,9 +89,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage) r.GET("/comment", a.commentController.GetComment) - //test - r.GET("/test", a.answerController.Dashboard) - // user r.GET("/user/info", a.userController.GetUserInfoByUserID) r.GET("/user/status", a.userController.GetUserStatus) @@ -228,4 +228,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) { r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface) r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig) r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig) + + //dashboard + r.GET("/dashboard", a.dashboardController.DashboardInfo) } diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go new file mode 100644 index 00000000..f14c9e9b --- /dev/null +++ b/internal/schema/dashboard_schema.go @@ -0,0 +1,15 @@ +package schema + +type DashboardInfo struct { + QuestionCount int64 `json:"question_count"` + AnswerCount int64 `json:"answer_count"` + CommentCount int64 `json:"comment_count"` + VoteCount int64 `json:"vote_count"` + UserCount int64 `json:"user_count"` + ReportCount int64 `json:"report_count"` + UploadingFiles string `json:"uploading_files"` //Allowed or Not allowed + SMTP string `json:"smtp"` //Enabled or Disabled + TimeZone string `json:"time_zone"` + OccupyingStorageSpace string `json:"occupying_storage_space"` + AppStartTime string `json:"app_start_time"` +} diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index 1d0f5400..aaeabcfc 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -3,6 +3,7 @@ package dashboard import ( "context" + "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity_common" answercommon "github.com/answerdev/answer/internal/service/answer_common" "github.com/answerdev/answer/internal/service/comment_common" @@ -10,7 +11,6 @@ import ( questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/report_common" usercommon "github.com/answerdev/answer/internal/service/user_common" - "github.com/davecgh/go-spew/spew" ) type DashboardService struct { @@ -45,18 +45,19 @@ func NewDashboardService( } // Statistical -func (ds *DashboardService) Statistical(ctx context.Context) error { +func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) { + dashboardInfo := &schema.DashboardInfo{} questionCount, err := ds.questionRepo.GetQuestionCount(ctx) if err != nil { - return err + return dashboardInfo, err } answerCount, err := ds.answerRepo.GetAnswerCount(ctx) if err != nil { - return err + return dashboardInfo, err } commentCount, err := ds.commentRepo.GetCommentCount(ctx) if err != nil { - return err + return dashboardInfo, err } typeKeys := []string{ @@ -78,8 +79,27 @@ func (ds *DashboardService) Statistical(ctx context.Context) error { voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes) if err != nil { - return err + return dashboardInfo, err } - spew.Dump(questionCount, answerCount, commentCount, activityTypes, voteCount) - return nil + userCount, err := ds.userRepo.GetUserCount(ctx) + if err != nil { + return dashboardInfo, err + } + + reportCount, err := ds.reportRepo.GetReportCount(ctx) + if err != nil { + return dashboardInfo, err + } + dashboardInfo.QuestionCount = questionCount + dashboardInfo.AnswerCount = answerCount + dashboardInfo.CommentCount = commentCount + dashboardInfo.VoteCount = voteCount + dashboardInfo.UserCount = userCount + dashboardInfo.ReportCount = reportCount + + dashboardInfo.UploadingFiles = "Allowed" + dashboardInfo.SMTP = "Enabled" + dashboardInfo.OccupyingStorageSpace = "1MB" + dashboardInfo.AppStartTime = "102" + return dashboardInfo, nil } diff --git a/internal/service/report_common/report_common.go b/internal/service/report_common/report_common.go index 5b5f3827..1b8c59cc 100644 --- a/internal/service/report_common/report_common.go +++ b/internal/service/report_common/report_common.go @@ -2,6 +2,7 @@ package report_common import ( "context" + "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" ) @@ -12,4 +13,5 @@ type ReportRepo interface { GetReportListPage(ctx context.Context, query schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error) GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error) UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error) + GetReportCount(ctx context.Context) (count int64, err error) } diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 5cc194b6..ec995e0e 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -21,6 +21,7 @@ type UserRepo interface { BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) + GetUserCount(ctx context.Context) (count int64, err error) } // UserCommon user service From 412edc1ba40cf9ae73bf39cd983702d57fef5129 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 16:35:41 +0800 Subject: [PATCH 021/157] fix: move i18n dir --- internal/router/ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/router/ui.go b/internal/router/ui.go index 4d465716..6589d5b1 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -41,6 +41,7 @@ func (r *_resource) Open(name string) (fs.File, error) { // Register a new static resource which generated by ui directory func (a *UIRouter) Register(r *gin.Engine) { staticPath := os.Getenv("ANSWER_STATIC_PATH") + r.StaticFS("/i18n/", http.FS(i18n.I18n)) // if ANSWER_STATIC_PATH is set and not empty, ignore embed resource if staticPath != "" { @@ -53,7 +54,6 @@ func (a *UIRouter) Register(r *gin.Engine) { r.LoadHTMLGlob(staticPath + "/*.html") r.Static("/static", staticPath+"/static") - r.StaticFS("/i18n/", http.FS(i18n.I18n)) r.NoRoute(func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }) From 50fa034095d8fc356c77a8bc6ee4721c7ad9a47a Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 16:38:41 +0800 Subject: [PATCH 022/157] feat(admin): Add time zone in admin background --- ui/src/common/constants.ts | 226 +++++++++++++++++++++++++ ui/src/pages/Admin/Interface/index.tsx | 34 +++- 2 files changed, 258 insertions(+), 2 deletions(-) diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index ba74f143..b700ad54 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -56,3 +56,229 @@ export const ADMIN_NAV_MENUS = [ child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], }, ]; +// timezones +export const TIMEZONES = [ + { + label: 'UTC-12', + value: 'UTC-12', + }, + { + label: 'UTC-11:30', + value: 'UTC-11.5', + }, + { + label: 'UTC-11', + value: 'UTC-11', + }, + { + label: 'UTC-10:30', + value: 'UTC-10.5', + }, + { + label: 'UTC-10', + value: 'UTC-10', + }, + { + label: 'UTC-9:30', + value: 'UTC-9.5', + }, + { + label: 'UTC-9', + value: 'UTC-9', + }, + { + label: 'UTC-8:30', + value: 'UTC-8.5', + }, + { + label: 'UTC-8', + value: 'UTC-8', + }, + { + label: 'UTC-7:30', + value: 'UTC-7.5', + }, + { + label: 'UTC-7', + value: 'UTC-7', + }, + { + label: 'UTC-6:30', + value: 'UTC-6.5', + }, + { + label: 'UTC-6', + value: 'UTC-6', + }, + { + label: 'UTC-5:30', + value: 'UTC-5.5', + }, + { + label: 'UTC-5', + value: 'UTC-5', + }, + { + label: 'UTC-4:30', + value: 'UTC-4.5', + }, + { + label: 'UTC-4', + value: 'UTC-4', + }, + { + label: 'UTC-3:30', + value: 'UTC-3.5', + }, + { + label: 'UTC-3', + value: 'UTC-3', + }, + { + label: 'UTC-2:30', + value: 'UTC-2.5', + }, + { + label: 'UTC-2', + value: 'UTC-2', + }, + { + label: 'UTC-1:30', + value: 'UTC-1.5', + }, + { + label: 'UTC-1', + value: 'UTC-1', + }, + { + label: 'UTC-0:30', + value: 'UTC-0.5', + }, + { + label: 'UTC+0', + value: 'UTC+0', + }, + { + label: 'UTC+0:30', + value: 'UTC+0.5', + }, + { + label: 'UTC+1', + value: 'UTC+1', + }, + { + label: 'UTC+1:30', + value: 'UTC+1.5', + }, + { + label: 'UTC+2', + value: 'UTC+2', + }, + { + label: 'UTC+2:30', + value: 'UTC+2.5', + }, + { + label: 'UTC+3', + value: 'UTC+3', + }, + { + label: 'UTC+3:30', + + value: 'UTC+3.5', + }, + { + label: 'UTC+4', + value: 'UTC+4', + }, + { + label: 'UTC+4:30', + value: 'UTC+4.5', + }, + { + label: 'UTC+5', + value: 'UTC+5', + }, + { + label: 'UTC+5:30', + value: 'UTC+5.5', + }, + { + label: 'UTC+5:45', + value: 'UTC+5.75', + }, + { + label: 'UTC+6', + value: 'UTC+6', + }, + { + label: 'UTC+6:30', + + value: 'UTC+6.5', + }, + { + label: 'UTC+7', + value: 'UTC+7', + }, + { + label: 'UTC+7:30', + value: 'UTC+7.5', + }, + { + label: 'UTC+8', + value: 'UTC+8', + }, + { + label: 'UTC+8:30', + value: 'UTC+8.5', + }, + { + label: 'UTC+8:45', + value: 'UTC+8.75', + }, + { + label: 'UTC+9', + value: 'UTC+9', + }, + { + label: 'UTC+9:30', + value: 'UTC+9.5', + }, + { + label: 'UTC+10', + value: 'UTC+10', + }, + { + label: 'UTC+10:30', + value: 'UTC+10.5', + }, + { + label: 'UTC+11', + value: 'UTC+11', + }, + { + label: 'UTC+11:30', + value: 'UTC+11.5', + }, + { + label: 'UTC+12', + value: 'UTC+12', + }, + { + label: 'UTC+12:45', + value: 'UTC+12.75', + }, + { + label: 'UTC+13', + value: 'UTC+13', + }, + { + label: 'UTC+13:45', + value: 'UTC+13.75', + }, + { + label: 'UTC+14', + value: 'UTC+14', + }, +]; +export const DEFAULT_TIMEZONE = 'UTC+0'; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index e5d68f32..deebcf9f 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -10,6 +10,7 @@ import { } from '@answer/common/interface'; import { interfaceStore } from '@answer/stores'; import { UploadImg } from '@answer/components'; +import { TIMEZONES, DEFAULT_TIMEZONE } from '@answer/common/constants'; import { languages, @@ -28,6 +29,7 @@ const Interface: FC = () => { const Toast = useToast(); const [langs, setLangs] = useState(); const { data: setting } = useInterfaceSetting(); + const [formData, setFormData] = useState({ logo: { value: setting?.logo || '', @@ -44,6 +46,11 @@ const Interface: FC = () => { isInvalid: false, errorMsg: '', }, + time_zone: { + value: setting?.time_zone || DEFAULT_TIMEZONE, + isInvalid: false, + errorMsg: '', + }, }); const getLangs = async () => { const res: LangsType[] = await languages(); @@ -107,6 +114,7 @@ const Interface: FC = () => { logo: formData.logo.value, theme: formData.theme.value, language: formData.language.value, + time_zone: formData.time_zone.value, }; updateInterfaceSetting(reqParams) @@ -159,12 +167,14 @@ const Interface: FC = () => { Object.keys(setting).forEach((k) => { formMeta[k] = { ...formData[k], value: setting[k] }; }); - setFormData(formMeta); + setFormData({ ...formData, ...formMeta }); } }, [setting]); useEffect(() => { getLangs(); }, []); + + console.log('formData', formData); return ( <>

{t('page_title')}

@@ -250,7 +260,27 @@ const Interface: FC = () => { {formData.language.errorMsg} - + + {t('time_zone.label')} + { + onChange('time_zone', evt.target.value); + }}> + {TIMEZONES?.map((item) => { + return ( + + ); + })} + + {t('time_zone.text')} + + {formData.time_zone.errorMsg} + + From b78f3061cb294cac31e700af123ab9c57909e62d Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:09:16 +0800 Subject: [PATCH 023/157] add install web api --- cmd/answer/command.go | 3 +++ internal/base/server/install.go | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 internal/base/server/install.go diff --git a/cmd/answer/command.go b/cmd/answer/command.go index d0efb0ab..27655443 100644 --- a/cmd/answer/command.go +++ b/cmd/answer/command.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/answerdev/answer/internal/base/server" "github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/migrations" "github.com/spf13/cobra" @@ -59,6 +60,8 @@ To run answer, use: Short: "init answer application", Long: `init answer application`, Run: func(_ *cobra.Command, _ []string) { + installwebapi := server.NewInstallHTTPServer() + installwebapi.Run(":8088") cli.InstallAllInitialEnvironment(dataDirPath) c, err := readConfig() if err != nil { diff --git a/internal/base/server/install.go b/internal/base/server/install.go new file mode 100644 index 00000000..d4b44d90 --- /dev/null +++ b/internal/base/server/install.go @@ -0,0 +1,26 @@ +package server + +import ( + "embed" + "net/http" + + "github.com/answerdev/answer/ui" + "github.com/gin-gonic/gin" +) + +type _resource struct { + fs embed.FS +} + +// NewHTTPServer new http server. +func NewInstallHTTPServer() *gin.Engine { + r := gin.New() + gin.SetMode(gin.DebugMode) + + r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK??") }) + + // gin.SetMode(gin.ReleaseMode) + r.StaticFS("/static", http.FS(ui.Build)) + + return r +} From 02de299c8692d28abc3b9a9f01180cb0c824156f Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Wed, 2 Nov 2022 17:14:47 +0800 Subject: [PATCH 024/157] refactor(router-guard): refactor router guard --- ui/.eslintrc.js | 2 +- ui/config-overrides.js | 7 - ui/src/components/AccordionNav/index.tsx | 2 +- ui/src/components/Actions/index.tsx | 9 +- ui/src/components/BaseUserCard/index.tsx | 3 +- .../Comment/components/ActionBar/index.tsx | 2 +- .../Comment/components/Form/index.tsx | 4 +- .../Comment/components/Reply/index.tsx | 4 +- ui/src/components/Comment/index.tsx | 15 +- ui/src/components/Editor/ToolBars/image.tsx | 3 +- ui/src/components/FollowingTags/index.tsx | 5 +- .../Header/components/NavItems/index.tsx | 2 +- ui/src/components/Header/index.tsx | 11 +- ui/src/components/HotQuestions/index.tsx | 3 +- ui/src/components/Mentions/index.tsx | 2 +- ui/src/components/Modal/PicAuthCodeModal.tsx | 9 +- ui/src/components/Operate/index.tsx | 7 +- ui/src/components/QuestionList/index.tsx | 5 +- ui/src/components/Share/index.tsx | 2 +- ui/src/components/TagSelector/index.tsx | 5 +- ui/src/components/Unactivate/index.tsx | 11 +- ui/src/components/UserCard/index.tsx | 3 +- ui/src/hooks/useChangeModal/index.tsx | 3 +- ui/src/hooks/usePageUsers/index.tsx | 2 +- ui/src/hooks/useReportModal/index.tsx | 5 +- ui/src/i18n/init.ts | 4 +- ui/src/index.tsx | 6 +- ui/src/pages/Admin/Answers/index.tsx | 9 +- ui/src/pages/Admin/Flags/index.tsx | 7 +- ui/src/pages/Admin/General/index.tsx | 7 +- ui/src/pages/Admin/Interface/index.tsx | 9 +- ui/src/pages/Admin/Questions/index.tsx | 9 +- ui/src/pages/Admin/Smtp/index.tsx | 5 +- ui/src/pages/Admin/Users/index.tsx | 7 +- ui/src/pages/Admin/index.tsx | 4 +- ui/src/pages/Layout/index.tsx | 5 +- .../Ask/components/SearchQuestion/index.tsx | 2 +- ui/src/pages/Questions/Ask/index.tsx | 9 +- .../Detail/components/Answer/index.tsx | 7 +- .../Detail/components/AnswerHead/index.tsx | 2 +- .../Detail/components/Question/index.tsx | 5 +- .../components/RelatedQuestions/index.tsx | 3 +- .../Detail/components/WriteAnswer/index.tsx | 5 +- ui/src/pages/Questions/Detail/index.tsx | 13 +- ui/src/pages/Questions/EditAnswer/index.tsx | 5 +- ui/src/pages/Questions/index.tsx | 3 +- ui/src/pages/Search/components/Head/index.tsx | 2 +- .../Search/components/SearchHead/index.tsx | 2 +- .../Search/components/SearchItem/index.tsx | 4 +- ui/src/pages/Search/index.tsx | 5 +- ui/src/pages/Tags/Detail/index.tsx | 5 +- ui/src/pages/Tags/Edit/index.tsx | 7 +- ui/src/pages/Tags/Info/index.tsx | 9 +- ui/src/pages/Tags/index.tsx | 5 +- .../AccountForgot/components/sendEmail.tsx | 3 +- ui/src/pages/Users/AccountForgot/index.tsx | 6 +- ui/src/pages/Users/ActiveEmail/index.tsx | 5 +- .../ChangeEmail/components/sendEmail.tsx | 5 +- ui/src/pages/Users/ChangeEmail/index.tsx | 4 +- ui/src/pages/Users/Login/index.tsx | 20 +- .../components/Achievements/index.tsx | 2 +- .../Notifications/components/Inbox/index.tsx | 2 +- ui/src/pages/Users/Notifications/index.tsx | 9 +- ui/src/pages/Users/PasswordReset/index.tsx | 9 +- .../Personal/components/Answers/index.tsx | 2 +- .../Personal/components/Comments/index.tsx | 2 +- .../Personal/components/DefaultList/index.tsx | 2 +- .../Personal/components/ListHead/index.tsx | 2 +- .../Personal/components/Reputation/index.tsx | 2 +- .../Personal/components/TopList/index.tsx | 2 +- .../Personal/components/UserInfo/index.tsx | 4 +- .../Users/Personal/components/Votes/index.tsx | 2 +- ui/src/pages/Users/Personal/index.tsx | 15 +- .../Register/components/SignUpForm/index.tsx | 3 +- ui/src/pages/Users/Register/index.tsx | 5 +- .../Account/components/ModifyEmail/index.tsx | 5 +- .../Account/components/ModifyPass/index.tsx | 5 +- .../pages/Users/Settings/Interface/index.tsx | 5 +- .../Users/Settings/Notification/index.tsx | 5 +- ui/src/pages/Users/Settings/Profile/index.tsx | 9 +- ui/src/pages/Users/Settings/index.tsx | 7 +- ui/src/pages/Users/Suspended/index.tsx | 3 +- ui/src/router/guarder.ts | 42 ---- ui/src/router/index.tsx | 15 +- ui/src/router/routes.ts | 76 +++++-- ui/src/services/admin/answer.ts | 4 +- ui/src/services/admin/flag.ts | 4 +- ui/src/services/admin/question.ts | 4 +- ui/src/services/admin/settings.ts | 4 +- ui/src/services/client/activity.ts | 4 +- ui/src/services/client/notification.ts | 7 +- ui/src/services/client/personal.ts | 4 +- ui/src/services/client/question.ts | 4 +- ui/src/services/client/search.ts | 4 +- ui/src/services/client/tag.ts | 7 +- ui/src/services/common.ts | 4 +- ui/src/stores/userInfo.ts | 6 +- ui/src/utils/floppyNavigation.ts | 3 +- ui/src/utils/guard.ts | 182 ++++++++++++++++ ui/src/utils/guards.ts | 196 ------------------ ui/src/utils/index.ts | 6 +- ui/src/utils/request.ts | 11 +- ui/tsconfig.json | 13 +- 103 files changed, 471 insertions(+), 565 deletions(-) delete mode 100644 ui/src/router/guarder.ts create mode 100644 ui/src/utils/guard.ts delete mode 100644 ui/src/utils/guards.ts diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 877a0039..98edc408 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -64,7 +64,7 @@ module.exports = { position: 'before', }, { - pattern: '@answer/**', + pattern: '@/**', group: 'internal', }, { diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 06464361..ac52fa63 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -8,13 +8,6 @@ module.exports = { config.resolve.alias = { ...config.resolve.alias, '@': path.resolve(__dirname, 'src'), - '@answer/pages': path.resolve(__dirname, 'src/pages'), - '@answer/components': path.resolve(__dirname, 'src/components'), - '@answer/stores': path.resolve(__dirname, 'src/stores'), - '@answer/hooks': path.resolve(__dirname, 'src/hooks'), - '@answer/utils': path.resolve(__dirname, 'src/utils'), - '@answer/common': path.resolve(__dirname, 'src/common'), - '@answer/api': path.resolve(__dirname, 'src/services/api'), }; return config; diff --git a/ui/src/components/AccordionNav/index.tsx b/ui/src/components/AccordionNav/index.tsx index ccc14d44..b3403bc8 100644 --- a/ui/src/components/AccordionNav/index.tsx +++ b/ui/src/components/AccordionNav/index.tsx @@ -5,7 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom'; import { useAccordionButton } from 'react-bootstrap/AccordionButton'; -import { Icon } from '@answer/components'; +import { Icon } from '@/components'; function MenuNode({ menu, callback, activeKey, isLeaf = false }) { const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' }); diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx index 25402ab2..d9395723 100644 --- a/ui/src/components/Actions/index.tsx +++ b/ui/src/components/Actions/index.tsx @@ -4,11 +4,10 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { Icon } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import { useToast } from '@answer/hooks'; - -import { tryNormalLogged } from '@/utils/guards'; +import { Icon } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { useToast } from '@/hooks'; +import { tryNormalLogged } from '@/utils/guard'; import { bookmark, postVote } from '@/services'; interface Props { diff --git a/ui/src/components/BaseUserCard/index.tsx b/ui/src/components/BaseUserCard/index.tsx index fa524bda..d170d3f8 100644 --- a/ui/src/components/BaseUserCard/index.tsx +++ b/ui/src/components/BaseUserCard/index.tsx @@ -1,8 +1,7 @@ import { memo, FC } from 'react'; import { Link } from 'react-router-dom'; -import { Avatar } from '@answer/components'; - +import { Avatar } from '@/components'; import { formatCount } from '@/utils'; interface Props { diff --git a/ui/src/components/Comment/components/ActionBar/index.tsx b/ui/src/components/Comment/components/ActionBar/index.tsx index a801a6bb..a13bff1a 100644 --- a/ui/src/components/Comment/components/ActionBar/index.tsx +++ b/ui/src/components/Comment/components/ActionBar/index.tsx @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; -import { Icon, FormatTime } from '@answer/components'; +import { Icon, FormatTime } from '@/components'; const ActionBar = ({ nickName, diff --git a/ui/src/components/Comment/components/Form/index.tsx b/ui/src/components/Comment/components/Form/index.tsx index 4971c4ef..70c62fdd 100644 --- a/ui/src/components/Comment/components/Form/index.tsx +++ b/ui/src/components/Comment/components/Form/index.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { TextArea, Mentions } from '@answer/components'; -import { usePageUsers } from '@answer/hooks'; +import { TextArea, Mentions } from '@/components'; +import { usePageUsers } from '@/hooks'; const Form = ({ className = '', diff --git a/ui/src/components/Comment/components/Reply/index.tsx b/ui/src/components/Comment/components/Reply/index.tsx index dcbf53bb..16b04822 100644 --- a/ui/src/components/Comment/components/Reply/index.tsx +++ b/ui/src/components/Comment/components/Reply/index.tsx @@ -2,8 +2,8 @@ import { useState, memo } from 'react'; import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { TextArea, Mentions } from '@answer/components'; -import { usePageUsers } from '@answer/hooks'; +import { TextArea, Mentions } from '@/components'; +import { usePageUsers } from '@/hooks'; const Form = ({ userName, onSendReply, onCancel, mode }) => { const [value, setValue] = useState(''); diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index a6dcdc98..1f57e737 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -7,14 +7,11 @@ import classNames from 'classnames'; import { unionBy } from 'lodash'; import { marked } from 'marked'; -import * as Types from '@answer/common/interface'; -import { Modal } from '@answer/components'; -import { usePageUsers, useReportModal } from '@answer/hooks'; -import { matchedUsers, parseUserInfo } from '@answer/utils'; - -import { Form, ActionBar, Reply } from './components'; - -import { tryNormalLogged } from '@/utils/guards'; +import * as Types from '@/common/interface'; +import { Modal } from '@/components'; +import { usePageUsers, useReportModal } from '@/hooks'; +import { matchedUsers, parseUserInfo } from '@/utils'; +import { tryNormalLogged } from '@/utils/guard'; import { useQueryComments, addComment, @@ -23,6 +20,8 @@ import { postVote, } from '@/services'; +import { Form, ActionBar, Reply } from './components'; + import './index.scss'; const Comment = ({ objectId, mode }) => { diff --git a/ui/src/components/Editor/ToolBars/image.tsx b/ui/src/components/Editor/ToolBars/image.tsx index ead20eee..bfa4c530 100644 --- a/ui/src/components/Editor/ToolBars/image.tsx +++ b/ui/src/components/Editor/ToolBars/image.tsx @@ -2,10 +2,9 @@ import { FC, useEffect, useState, memo } from 'react'; import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Modal as AnswerModal } from '@answer/components'; +import { Modal as AnswerModal } from '@/components'; import ToolItem from '../toolItem'; import { IEditorContext } from '../types'; - import { uploadImage } from '@/services'; const Image: FC = ({ editor }) => { diff --git a/ui/src/components/FollowingTags/index.tsx b/ui/src/components/FollowingTags/index.tsx index 55e9bc76..06fec6ec 100644 --- a/ui/src/components/FollowingTags/index.tsx +++ b/ui/src/components/FollowingTags/index.tsx @@ -3,9 +3,8 @@ import { Card, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; -import { TagSelector, Tag } from '@answer/components'; - -import { tryNormalLogged } from '@/utils/guards'; +import { TagSelector, Tag } from '@/components'; +import { tryNormalLogged } from '@/utils/guard'; import { useFollowingTags, followTags } from '@/services'; const Index: FC = () => { diff --git a/ui/src/components/Header/components/NavItems/index.tsx b/ui/src/components/Header/components/NavItems/index.tsx index 67112758..8a4872d9 100644 --- a/ui/src/components/Header/components/NavItems/index.tsx +++ b/ui/src/components/Header/components/NavItems/index.tsx @@ -3,7 +3,7 @@ import { Nav, Dropdown } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link, NavLink } from 'react-router-dom'; -import { Avatar, Icon } from '@answer/components'; +import { Avatar, Icon } from '@/components'; interface Props { redDot; diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index b4cd4e16..748eb318 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -17,17 +17,12 @@ import { useLocation, } from 'react-router-dom'; -import { - loggedUserInfoStore, - siteInfoStore, - interfaceStore, -} from '@answer/stores'; - -import NavItems from './components/NavItems'; - +import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores'; import { logout, useQueryNotificationStatus } from '@/services'; import { RouteAlias } from '@/router/alias'; +import NavItems from './components/NavItems'; + import './index.scss'; const Header: FC = () => { diff --git a/ui/src/components/HotQuestions/index.tsx b/ui/src/components/HotQuestions/index.tsx index 920bdce2..da0305d0 100644 --- a/ui/src/components/HotQuestions/index.tsx +++ b/ui/src/components/HotQuestions/index.tsx @@ -3,8 +3,7 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; - +import { Icon } from '@/components'; import { useHotQuestions } from '@/services'; const HotQuestions: FC = () => { diff --git a/ui/src/components/Mentions/index.tsx b/ui/src/components/Mentions/index.tsx index 9b29c86d..23cfa083 100644 --- a/ui/src/components/Mentions/index.tsx +++ b/ui/src/components/Mentions/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState, FC } from 'react'; import { Dropdown } from 'react-bootstrap'; -import * as Types from '@answer/common/interface'; +import * as Types from '@/common/interface'; interface IProps { children: React.ReactNode; diff --git a/ui/src/components/Modal/PicAuthCodeModal.tsx b/ui/src/components/Modal/PicAuthCodeModal.tsx index 3a081a0c..ec194a64 100644 --- a/ui/src/components/Modal/PicAuthCodeModal.tsx +++ b/ui/src/components/Modal/PicAuthCodeModal.tsx @@ -2,13 +2,8 @@ import React from 'react'; import { Modal, Form, Button, InputGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; -import type { - FormValue, - FormDataType, - ImgCodeRes, -} from '@answer/common/interface'; - +import { Icon } from '@/components'; +import type { FormValue, FormDataType, ImgCodeRes } from '@/common/interface'; import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index 201774c6..cdacc751 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -3,12 +3,11 @@ import { Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Modal } from '@answer/components'; -import { useReportModal, useToast } from '@answer/hooks'; +import { Modal } from '@/components'; +import { useReportModal, useToast } from '@/hooks'; import Share from '../Share'; - import { deleteQuestion, deleteAnswer } from '@/services'; -import { tryNormalLogged } from '@/utils/guards'; +import { tryNormalLogged } from '@/utils/guard'; interface IProps { type: 'answer' | 'question'; diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 85b72ce1..76a6ccd9 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -3,7 +3,7 @@ import { Row, Col, ListGroup } from 'react-bootstrap'; import { NavLink, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; +import type * as Type from '@/common/interface'; import { Icon, Tag, @@ -12,8 +12,7 @@ import { Empty, BaseUserCard, QueryGroup, -} from '@answer/components'; - +} from '@/components'; import { useQuestionList } from '@/services'; const QuestionOrderKeys: Type.QuestionOrderBy[] = [ diff --git a/ui/src/components/Share/index.tsx b/ui/src/components/Share/index.tsx index 74b3de34..652dcef1 100644 --- a/ui/src/components/Share/index.tsx +++ b/ui/src/components/Share/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { FacebookShareButton, TwitterShareButton } from 'next-share'; import copy from 'copy-to-clipboard'; -import { loggedUserInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@/stores'; interface IProps { type: 'answer' | 'question'; diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index cedb85a3..8747f65a 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'; import { marked } from 'marked'; import classNames from 'classnames'; -import { useTagModal } from '@answer/hooks'; -import type * as Type from '@answer/common/interface'; - +import { useTagModal } from '@/hooks'; +import type * as Type from '@/common/interface'; import { queryTags } from '@/services'; import './index.scss'; diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index 6b7a26bf..420891cc 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -3,14 +3,9 @@ import { Button, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { PicAuthCodeModal } from '@answer/components/Modal'; -import type { - ImgCodeRes, - ImgCodeReq, - FormDataType, -} from '@answer/common/interface'; -import { loggedUserInfoStore } from '@answer/stores'; - +import { PicAuthCodeModal } from '@/components/Modal'; +import type { ImgCodeRes, ImgCodeReq, FormDataType } from '@/common/interface'; +import { loggedUserInfoStore } from '@/stores'; import { resendEmail, checkImgCode } from '@/services'; import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/components/UserCard/index.tsx b/ui/src/components/UserCard/index.tsx index 0110c82a..27441231 100644 --- a/ui/src/components/UserCard/index.tsx +++ b/ui/src/components/UserCard/index.tsx @@ -3,8 +3,7 @@ import { Link } from 'react-router-dom'; import classnames from 'classnames'; -import { Avatar, FormatTime } from '@answer/components'; - +import { Avatar, FormatTime } from '@/components'; import { formatCount } from '@/utils'; interface Props { diff --git a/ui/src/hooks/useChangeModal/index.tsx b/ui/src/hooks/useChangeModal/index.tsx index f484331c..529f7a3a 100644 --- a/ui/src/hooks/useChangeModal/index.tsx +++ b/ui/src/hooks/useChangeModal/index.tsx @@ -4,8 +4,7 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { Modal as AnswerModal } from '@answer/components'; - +import { Modal as AnswerModal } from '@/components'; import { changeUserStatus } from '@/services'; const div = document.createElement('div'); diff --git a/ui/src/hooks/usePageUsers/index.tsx b/ui/src/hooks/usePageUsers/index.tsx index 72203619..2c828695 100644 --- a/ui/src/hooks/usePageUsers/index.tsx +++ b/ui/src/hooks/usePageUsers/index.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { uniqBy } from 'lodash'; -import * as Types from '@answer/common/interface'; +import * as Types from '@/common/interface'; let globalUsers: Types.PageUser[] = []; const usePageUsers = () => { diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx index 2f7c8f18..28b85eb5 100644 --- a/ui/src/hooks/useReportModal/index.tsx +++ b/ui/src/hooks/useReportModal/index.tsx @@ -4,9 +4,8 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { useToast } from '@answer/hooks'; -import type * as Type from '@answer/common/interface'; - +import { useToast } from '@/hooks'; +import type * as Type from '@/common/interface'; import { reportList, postReport, closeQuestion, putReport } from '@/services'; interface Params { diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index deecc0e2..e8b41e31 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -3,11 +3,11 @@ import { initReactI18next } from 'react-i18next'; import i18next from 'i18next'; import Backend from 'i18next-http-backend'; +import { DEFAULT_LANG } from '@/common/constants'; + import en from './locales/en.json'; import zh from './locales/zh_CN.json'; -import { DEFAULT_LANG } from '@/common/constants'; - i18next // load translation using http .use(Backend) diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 1943b61a..67e5ee50 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -2,9 +2,9 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import { Guard } from '@/utils'; -import { pullLoggedUser } from '@/utils/guards'; +import App from './App'; import './i18n/init'; import './index.scss'; @@ -17,7 +17,7 @@ async function bootstrapApp() { /** * NOTICE: must pre init logged user info for router */ - await pullLoggedUser(); + await Guard.pullLoggedUser(); root.render( diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 2a25f53c..2c14c6d7 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -11,11 +11,10 @@ import { BaseUserCard, Empty, QueryGroup, -} from '@answer/components'; -import { ADMIN_LIST_STATUS } from '@answer/common/constants'; -import { useEditStatusModal } from '@answer/hooks'; -import * as Type from '@answer/common/interface'; - +} from '@/components'; +import { ADMIN_LIST_STATUS } from '@/common/constants'; +import { useEditStatusModal } from '@/hooks'; +import * as Type from '@/common/interface'; import { useAnswerSearch, changeAnswerStatus } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Flags/index.tsx b/ui/src/pages/Admin/Flags/index.tsx index a7842429..353bd382 100644 --- a/ui/src/pages/Admin/Flags/index.tsx +++ b/ui/src/pages/Admin/Flags/index.tsx @@ -9,10 +9,9 @@ import { Empty, Pagination, QueryGroup, -} from '@answer/components'; -import { useReportModal } from '@answer/hooks'; -import * as Type from '@answer/common/interface'; - +} from '@/components'; +import { useReportModal } from '@/hooks'; +import * as Type from '@/common/interface'; import { useFlagSearch } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 3154caec..14b95978 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -2,10 +2,9 @@ import React, { FC, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; -import { siteInfoStore } from '@answer/stores'; - +import type * as Type from '@/common/interface'; +import { useToast } from '@/hooks'; +import { siteInfoStore } from '@/stores'; import { useGeneralSetting, updateGeneralSetting } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index e5d68f32..3f179c57 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -2,15 +2,14 @@ import React, { FC, FormEvent, useEffect, useState } from 'react'; import { Form, Button, Image, Stack } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { useToast } from '@answer/hooks'; +import { useToast } from '@/hooks'; import { LangsType, FormDataType, AdminSettingsInterface, -} from '@answer/common/interface'; -import { interfaceStore } from '@answer/stores'; -import { UploadImg } from '@answer/components'; - +} from '@/common/interface'; +import { interfaceStore } from '@/stores'; +import { UploadImg } from '@/components'; import { languages, uploadAvatar, diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 1a188512..e27a715c 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -11,11 +11,10 @@ import { BaseUserCard, Empty, QueryGroup, -} from '@answer/components'; -import { ADMIN_LIST_STATUS } from '@answer/common/constants'; -import { useEditStatusModal, useReportModal } from '@answer/hooks'; -import * as Type from '@answer/common/interface'; - +} from '@/components'; +import { ADMIN_LIST_STATUS } from '@/common/constants'; +import { useEditStatusModal, useReportModal } from '@/hooks'; +import * as Type from '@/common/interface'; import { useQuestionSearch, changeQuestionStatus, diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx index 17abd481..33de30ab 100644 --- a/ui/src/pages/Admin/Smtp/index.tsx +++ b/ui/src/pages/Admin/Smtp/index.tsx @@ -2,9 +2,8 @@ import React, { FC, useEffect, useState } from 'react'; import { Form, Button, Stack } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; - +import type * as Type from '@/common/interface'; +import { useToast } from '@/hooks'; import { useSmtpSetting, updateSmtpSetting } from '@/services'; import pattern from '@/common/pattern'; diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index de434084..a14f6b74 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -9,10 +9,9 @@ import { BaseUserCard, Empty, QueryGroup, -} from '@answer/components'; -import * as Type from '@answer/common/interface'; -import { useChangeModal } from '@answer/hooks'; - +} from '@/components'; +import * as Type from '@/common/interface'; +import { useChangeModal } from '@/hooks'; import { useQueryUsers } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx index 8d7b452e..27ef8565 100644 --- a/ui/src/pages/Admin/index.tsx +++ b/ui/src/pages/Admin/index.tsx @@ -3,8 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { AccordionNav, PageTitle } from '@answer/components'; -import { ADMIN_NAV_MENUS } from '@answer/common/constants'; +import { AccordionNav, PageTitle } from '@/components'; +import { ADMIN_NAV_MENUS } from '@/common/constants'; import './index.scss'; diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 7affe2f0..363c9b01 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -5,9 +5,8 @@ import { Helmet, HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { siteInfoStore, interfaceStore, toastStore } from '@answer/stores'; -import { Header, AdminHeader, Footer, Toast } from '@answer/components'; - +import { siteInfoStore, interfaceStore, toastStore } from '@/stores'; +import { Header, AdminHeader, Footer, Toast } from '@/components'; import { useSiteSettings } from '@/services'; import Storage from '@/utils/storage'; import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; diff --git a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx index 16388963..99a8026a 100644 --- a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx +++ b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import { Accordion, ListGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; +import { Icon } from '@/components'; import './index.scss'; diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index f233150e..fe4d3319 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -6,11 +6,8 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import classNames from 'classnames'; -import { Editor, EditorRef, TagSelector, PageTitle } from '@answer/components'; -import type * as Type from '@answer/common/interface'; - -import SearchQuestion from './components/SearchQuestion'; - +import { Editor, EditorRef, TagSelector, PageTitle } from '@/components'; +import type * as Type from '@/common/interface'; import { saveQuestion, questionDetail, @@ -20,6 +17,8 @@ import { useQueryQuestionByTitle, } from '@/services'; +import SearchQuestion from './components/SearchQuestion'; + interface FormDataItem { title: Type.FormValue; tags: Type.FormValue; diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index e97238cf..2fe87d1c 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -10,10 +10,9 @@ import { Comment, FormatTime, htmlRender, -} from '@answer/components'; -import { scrollTop } from '@answer/utils'; -import { AnswerItem } from '@answer/common/interface'; - +} from '@/components'; +import { scrollTop } from '@/utils'; +import { AnswerItem } from '@/common/interface'; import { acceptanceAnswer } from '@/services'; interface Props { diff --git a/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx b/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx index f2c7efe9..32abc17b 100644 --- a/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx +++ b/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx @@ -1,7 +1,7 @@ import { memo, FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@answer/components'; +import { QueryGroup } from '@/components'; interface Props { count: number; diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index aa3a38ec..1b46a869 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -11,9 +11,8 @@ import { Comment, FormatTime, htmlRender, -} from '@answer/components'; -import { formatCount } from '@answer/utils'; - +} from '@/components'; +import { formatCount } from '@/utils'; import { following } from '@/services'; interface Props { diff --git a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx index 8493408c..3dd91c7f 100644 --- a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx @@ -3,8 +3,7 @@ import { Card, ListGroup } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; - +import { Icon } from '@/components'; import { useSimilarQuestion } from '@/services'; import { loggedUserInfoStore } from '@/stores'; diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx index 9659b286..8d1ef1be 100644 --- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx @@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'; import { marked } from 'marked'; import classNames from 'classnames'; -import { Editor, Modal } from '@answer/components'; -import { FormDataType } from '@answer/common/interface'; - +import { Editor, Modal } from '@/components'; +import { FormDataType } from '@/common/interface'; import { postAnswer } from '@/services'; interface Props { diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 2f469415..cc19a719 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -2,15 +2,16 @@ import { useEffect, useState } from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; -import { Pagination, PageTitle } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import { scrollTop } from '@answer/utils'; -import { usePageUsers } from '@answer/hooks'; +import { Pagination, PageTitle } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { scrollTop } from '@/utils'; +import { usePageUsers } from '@/hooks'; import type { ListResult, QuestionDetailRes, AnswerItem, -} from '@answer/common/interface'; +} from '@/common/interface'; +import { questionDetail, getAnswers } from '@/services'; import { Question, @@ -21,8 +22,6 @@ import { Alert, } from './components'; -import { questionDetail, getAnswers } from '@/services'; - import './index.scss'; const Index = () => { diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index c87733f4..0fab103c 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -6,9 +6,8 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import classNames from 'classnames'; -import { Editor, EditorRef, Icon, PageTitle } from '@answer/components'; -import type * as Type from '@answer/common/interface'; - +import { Editor, EditorRef, Icon, PageTitle } from '@/components'; +import type * as Type from '@/common/interface'; import { useQueryAnswerInfo, modifyAnswer, diff --git a/ui/src/pages/Questions/index.tsx b/ui/src/pages/Questions/index.tsx index 9e7ed318..ea10ec15 100644 --- a/ui/src/pages/Questions/index.tsx +++ b/ui/src/pages/Questions/index.tsx @@ -3,8 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useMatch } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { PageTitle, FollowingTags } from '@answer/components'; - +import { PageTitle, FollowingTags } from '@/components'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; import { siteInfoStore } from '@/stores'; diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index 1558bc35..095796d2 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -4,7 +4,7 @@ import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { following } from '@/services'; -import { tryNormalLogged } from '@/utils/guards'; +import { tryNormalLogged } from '@/utils/guard'; interface Props { data; diff --git a/ui/src/pages/Search/components/SearchHead/index.tsx b/ui/src/pages/Search/components/SearchHead/index.tsx index 2d7549a4..fb185bc1 100644 --- a/ui/src/pages/Search/components/SearchHead/index.tsx +++ b/ui/src/pages/Search/components/SearchHead/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@answer/components'; +import { QueryGroup } from '@/components'; const sortBtns = ['relevance', 'newest', 'active', 'score']; diff --git a/ui/src/pages/Search/components/SearchItem/index.tsx b/ui/src/pages/Search/components/SearchItem/index.tsx index ec703277..09cecd32 100644 --- a/ui/src/pages/Search/components/SearchItem/index.tsx +++ b/ui/src/pages/Search/components/SearchItem/index.tsx @@ -2,8 +2,8 @@ import { memo, FC } from 'react'; import { ListGroupItem, Badge } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon, Tag, FormatTime, BaseUserCard } from '@answer/components'; -import type { SearchResItem } from '@answer/common/interface'; +import { Icon, Tag, FormatTime, BaseUserCard } from '@/components'; +import type { SearchResItem } from '@/common/interface'; interface Props { data: SearchResItem; diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index 914565ae..f22dc3ce 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -3,12 +3,11 @@ import { Container, Row, Col, ListGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; -import { Pagination, PageTitle } from '@answer/components'; +import { Pagination, PageTitle } from '@/components'; +import { useSearch } from '@/services'; import { Head, SearchHead, SearchItem, Tips, Empty } from './components'; -import { useSearch } from '@/services'; - const Index = () => { const { t } = useTranslation('translation'); const [searchParams] = useSearchParams(); diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index e37ab0ab..452ef37d 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -3,9 +3,8 @@ import { Container, Row, Col, Button } from 'react-bootstrap'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import * as Type from '@answer/common/interface'; -import { PageTitle, FollowingTags } from '@answer/components'; - +import * as Type from '@/common/interface'; +import { PageTitle, FollowingTags } from '@/components'; import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; diff --git a/ui/src/pages/Tags/Edit/index.tsx b/ui/src/pages/Tags/Edit/index.tsx index 58e7d59c..890bd32e 100644 --- a/ui/src/pages/Tags/Edit/index.tsx +++ b/ui/src/pages/Tags/Edit/index.tsx @@ -6,10 +6,9 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import classNames from 'classnames'; -import { Editor, EditorRef, PageTitle } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import type * as Type from '@answer/common/interface'; - +import { Editor, EditorRef, PageTitle } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import type * as Type from '@/common/interface'; import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; interface FormDataItem { diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index d8c4ad75..3b792adb 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -5,14 +5,7 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { - Tag, - TagSelector, - FormatTime, - Modal, - PageTitle, -} from '@answer/components'; - +import { Tag, TagSelector, FormatTime, Modal, PageTitle } from '@/components'; import { useTagInfo, useQuerySynonymsTags, diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index 80256d94..a01e3e56 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -3,9 +3,8 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components'; -import { formatCount } from '@answer/utils'; - +import { Tag, Pagination, PageTitle, QueryGroup } from '@/components'; +import { formatCount } from '@/utils'; import { useQueryTags, following } from '@/services'; const sortBtns = ['popular', 'name', 'newest']; diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index e95b1354..f919e560 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -6,8 +6,7 @@ import type { ImgCodeRes, PasswordResetReq, FormDataType, -} from '@answer/common/interface'; - +} from '@/common/interface'; import { resetPassword, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; diff --git a/ui/src/pages/Users/AccountForgot/index.tsx b/ui/src/pages/Users/AccountForgot/index.tsx index 5065e9b6..e7a77ea5 100644 --- a/ui/src/pages/Users/AccountForgot/index.tsx +++ b/ui/src/pages/Users/AccountForgot/index.tsx @@ -2,11 +2,11 @@ import React, { useState, useEffect } from 'react'; import { Container, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import SendEmail from './components/sendEmail'; - -import { tryNormalLogged } from '@/utils/guards'; +import { tryNormalLogged } from '@/utils/guard'; import { PageTitle } from '@/components'; +import SendEmail from './components/sendEmail'; + const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' }); const [step, setStep] = useState(1); diff --git a/ui/src/pages/Users/ActiveEmail/index.tsx b/ui/src/pages/Users/ActiveEmail/index.tsx index f50fbc3e..ac2223a7 100644 --- a/ui/src/pages/Users/ActiveEmail/index.tsx +++ b/ui/src/pages/Users/ActiveEmail/index.tsx @@ -1,9 +1,8 @@ import { FC, memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { loggedUserInfoStore } from '@answer/stores'; -import { getQueryString } from '@answer/utils'; - +import { loggedUserInfoStore } from '@/stores'; +import { getQueryString } from '@/utils'; import { activateAccount } from '@/services'; import { PageTitle } from '@/components'; diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx index 4d419091..f87a6adf 100644 --- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx +++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx @@ -7,9 +7,8 @@ import type { ImgCodeRes, PasswordResetReq, FormDataType, -} from '@answer/common/interface'; -import { loggedUserInfoStore } from '@answer/stores'; - +} from '@/common/interface'; +import { loggedUserInfoStore } from '@/stores'; import { changeEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; diff --git a/ui/src/pages/Users/ChangeEmail/index.tsx b/ui/src/pages/Users/ChangeEmail/index.tsx index cbb743a5..cabc5e5f 100644 --- a/ui/src/pages/Users/ChangeEmail/index.tsx +++ b/ui/src/pages/Users/ChangeEmail/index.tsx @@ -2,10 +2,10 @@ import { FC, memo } from 'react'; import { Container, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import SendEmail from './components/sendEmail'; - import { PageTitle } from '@/components'; +import SendEmail from './components/sendEmail'; + const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'change_email' }); diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index 6d9af5c2..423a6654 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -7,13 +7,11 @@ import type { LoginReqParams, ImgCodeRes, FormDataType, -} from '@answer/common/interface'; -import { PageTitle, Unactivate } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import { getQueryString } from '@answer/utils'; - +} from '@/common/interface'; +import { PageTitle, Unactivate } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { getQueryString, Guard, floppyNavigation } from '@/utils'; import { login, checkImgCode } from '@/services'; -import { deriveUserStat, tryNormalLogged } from '@/utils/guards'; import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; import { PicAuthCodeModal } from '@/components/Modal'; @@ -106,8 +104,8 @@ const Index: React.FC = () => { login(params) .then((res) => { updateUser(res); - const userStat = deriveUserStat(); - if (!userStat.isActivated) { + const userStat = Guard.deriveLoginState(); + if (userStat.isNotActivated) { // inactive setStep(2); setRefresh((pre) => pre + 1); @@ -115,7 +113,9 @@ const Index: React.FC = () => { const path = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home; Storage.remove(REDIRECT_PATH_STORAGE_KEY); - navigate(path, { replace: true }); + floppyNavigation.navigate(path, () => { + navigate(path, { replace: true }); + }); } setModalState(false); @@ -159,7 +159,7 @@ const Index: React.FC = () => { if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { setStep(2); } else { - tryNormalLogged(); + Guard.tryNormalLogged(); } }, []); diff --git a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx index 12cfe93b..54bd4cdb 100644 --- a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx +++ b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; -import { Empty } from '@answer/components'; +import { Empty } from '@/components'; import './index.scss'; diff --git a/ui/src/pages/Users/Notifications/components/Inbox/index.tsx b/ui/src/pages/Users/Notifications/components/Inbox/index.tsx index 27366b81..68739b0c 100644 --- a/ui/src/pages/Users/Notifications/components/Inbox/index.tsx +++ b/ui/src/pages/Users/Notifications/components/Inbox/index.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; -import { FormatTime, Empty } from '@answer/components'; +import { FormatTime, Empty } from '@/components'; const Inbox = ({ data, handleReadNotification }) => { if (!data) { diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index b24367b7..1ee7158c 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -3,11 +3,7 @@ import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useParams, useNavigate } from 'react-router-dom'; -import { PageTitle } from '@answer/components'; - -import Inbox from './components/Inbox'; -import Achievements from './components/Achievements'; - +import { PageTitle } from '@/components'; import { useQueryNotifications, clearUnreadNotification, @@ -15,6 +11,9 @@ import { readNotification, } from '@/services'; +import Inbox from './components/Inbox'; +import Achievements from './components/Achievements'; + const PAGE_SIZE = 10; const Notifications = () => { diff --git a/ui/src/pages/Users/PasswordReset/index.tsx b/ui/src/pages/Users/PasswordReset/index.tsx index af97001f..b97bd707 100644 --- a/ui/src/pages/Users/PasswordReset/index.tsx +++ b/ui/src/pages/Users/PasswordReset/index.tsx @@ -3,12 +3,11 @@ import { Container, Col, Form, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { loggedUserInfoStore } from '@answer/stores'; -import { getQueryString } from '@answer/utils'; -import type { FormDataType } from '@answer/common/interface'; - +import { loggedUserInfoStore } from '@/stores'; +import { getQueryString } from '@/utils'; +import type { FormDataType } from '@/common/interface'; import { replacementPassword } from '@/services'; -import { tryNormalLogged } from '@/utils/guards'; +import { tryNormalLogged } from '@/utils/guard'; import { PageTitle } from '@/components'; const Index: React.FC = () => { diff --git a/ui/src/pages/Users/Personal/components/Answers/index.tsx b/ui/src/pages/Users/Personal/components/Answers/index.tsx index 12ab0059..a7d8c48a 100644 --- a/ui/src/pages/Users/Personal/components/Answers/index.tsx +++ b/ui/src/pages/Users/Personal/components/Answers/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon, FormatTime, Tag } from '@answer/components'; +import { Icon, FormatTime, Tag } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/Comments/index.tsx b/ui/src/pages/Users/Personal/components/Comments/index.tsx index 483ce361..f53e468e 100644 --- a/ui/src/pages/Users/Personal/components/Comments/index.tsx +++ b/ui/src/pages/Users/Personal/components/Comments/index.tsx @@ -1,7 +1,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; -import { FormatTime } from '@answer/components'; +import { FormatTime } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx index 7eb67525..980a2df3 100644 --- a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx +++ b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon, FormatTime, Tag, BaseUserCard } from '@answer/components'; +import { Icon, FormatTime, Tag, BaseUserCard } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/ListHead/index.tsx b/ui/src/pages/Users/Personal/components/ListHead/index.tsx index e1ce494f..de550754 100644 --- a/ui/src/pages/Users/Personal/components/ListHead/index.tsx +++ b/ui/src/pages/Users/Personal/components/ListHead/index.tsx @@ -1,7 +1,7 @@ import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@answer/components'; +import { QueryGroup } from '@/components'; const sortBtns = ['newest', 'score']; diff --git a/ui/src/pages/Users/Personal/components/Reputation/index.tsx b/ui/src/pages/Users/Personal/components/Reputation/index.tsx index fa1cb3db..6458e921 100644 --- a/ui/src/pages/Users/Personal/components/Reputation/index.tsx +++ b/ui/src/pages/Users/Personal/components/Reputation/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { FormatTime } from '@answer/components'; +import { FormatTime } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/TopList/index.tsx b/ui/src/pages/Users/Personal/components/TopList/index.tsx index cdd7ba2e..86179305 100644 --- a/ui/src/pages/Users/Personal/components/TopList/index.tsx +++ b/ui/src/pages/Users/Personal/components/TopList/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; +import { Icon } from '@/components'; interface Props { data: any[]; diff --git a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx index 97caca3e..880b810f 100644 --- a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx +++ b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx @@ -3,8 +3,8 @@ import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { Avatar, Icon } from '@answer/components'; -import type { UserInfoRes } from '@answer/common/interface'; +import { Avatar, Icon } from '@/components'; +import type { UserInfoRes } from '@/common/interface'; interface Props { data: UserInfoRes; diff --git a/ui/src/pages/Users/Personal/components/Votes/index.tsx b/ui/src/pages/Users/Personal/components/Votes/index.tsx index 308af877..fa5023db 100644 --- a/ui/src/pages/Users/Personal/components/Votes/index.tsx +++ b/ui/src/pages/Users/Personal/components/Votes/index.tsx @@ -1,7 +1,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; -import { FormatTime } from '@answer/components'; +import { FormatTime } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index fcab49ff..4720db93 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -3,8 +3,13 @@ import { Container, Row, Col, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useParams, useSearchParams } from 'react-router-dom'; -import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; +import { Pagination, FormatTime, PageTitle, Empty } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { + usePersonalInfoByName, + usePersonalTop, + usePersonalListByTabName, +} from '@/services'; import { UserInfo, @@ -19,12 +24,6 @@ import { Votes, } from './components'; -import { - usePersonalInfoByName, - usePersonalTop, - usePersonalListByTabName, -} from '@/services'; - const Personal: FC = () => { const { tabName = 'overview', username = '' } = useParams(); const [searchParams] = useSearchParams(); diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx index 4f072fe7..0d92c5fa 100644 --- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx +++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx @@ -3,8 +3,7 @@ import { Form, Button, Col } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import type { FormDataType } from '@answer/common/interface'; - +import type { FormDataType } from '@/common/interface'; import { register } from '@/services'; import userStore from '@/stores/userInfo'; diff --git a/ui/src/pages/Users/Register/index.tsx b/ui/src/pages/Users/Register/index.tsx index 5f8370a0..e4d94b2d 100644 --- a/ui/src/pages/Users/Register/index.tsx +++ b/ui/src/pages/Users/Register/index.tsx @@ -2,12 +2,11 @@ import React, { useState, useEffect } from 'react'; import { Container } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { PageTitle, Unactivate } from '@answer/components'; +import { PageTitle, Unactivate } from '@/components'; +import { tryNormalLogged } from '@/utils/guard'; import SignUpForm from './components/SignUpForm'; -import { tryNormalLogged } from '@/utils/guards'; - const Index: React.FC = () => { const [showForm, setShowForm] = useState(true); const { t } = useTranslation('translation', { keyPrefix: 'login' }); diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index 4ed6b0ff..8bfa6dfa 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -2,9 +2,8 @@ import React, { FC, FormEvent, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; - +import type * as Type from '@/common/interface'; +import { useToast } from '@/hooks'; import { getLoggedUserInfo, changeEmail } from '@/services'; const reg = /(?<=.{2}).+(?=@)/gi; diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx index 2c7ed9ef..56d8f17b 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx @@ -2,9 +2,8 @@ import React, { FC, FormEvent, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { useToast } from '@answer/hooks'; -import type { FormDataType } from '@answer/common/interface'; - +import { useToast } from '@/hooks'; +import type { FormDataType } from '@/common/interface'; import { modifyPassword } from '@/services'; const Index: FC = () => { diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index f38f92e8..3a3941b9 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -6,9 +6,8 @@ import dayjs from 'dayjs'; import en from 'dayjs/locale/en'; import zh from 'dayjs/locale/zh-cn'; -import type { LangsType, FormDataType } from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; - +import type { LangsType, FormDataType } from '@/common/interface'; +import { useToast } from '@/hooks'; import { languages } from '@/services'; import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/pages/Users/Settings/Notification/index.tsx b/ui/src/pages/Users/Settings/Notification/index.tsx index 6d0cab61..e7310145 100644 --- a/ui/src/pages/Users/Settings/Notification/index.tsx +++ b/ui/src/pages/Users/Settings/Notification/index.tsx @@ -2,9 +2,8 @@ import React, { useState, FormEvent, useEffect } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type { FormDataType } from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; - +import type { FormDataType } from '@/common/interface'; +import { useToast } from '@/hooks'; import { setNotice, getLoggedUserInfo } from '@/services'; const Index = () => { diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 670bec91..a4442b08 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -5,11 +5,10 @@ import { Trans, useTranslation } from 'react-i18next'; import { marked } from 'marked'; import MD5 from 'md5'; -import type { FormDataType } from '@answer/common/interface'; -import { UploadImg, Avatar } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import { useToast } from '@answer/hooks'; - +import type { FormDataType } from '@/common/interface'; +import { UploadImg, Avatar } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { useToast } from '@/hooks'; import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services'; const Index: React.FC = () => { diff --git a/ui/src/pages/Users/Settings/index.tsx b/ui/src/pages/Users/Settings/index.tsx index 671707fe..dc591323 100644 --- a/ui/src/pages/Users/Settings/index.tsx +++ b/ui/src/pages/Users/Settings/index.tsx @@ -3,13 +3,12 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import type { FormDataType } from '@answer/common/interface'; - -import Nav from './components/Nav'; - +import type { FormDataType } from '@/common/interface'; import { getLoggedUserInfo } from '@/services'; import { PageTitle } from '@/components'; +import Nav from './components/Nav'; + const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.profile', diff --git a/ui/src/pages/Users/Suspended/index.tsx b/ui/src/pages/Users/Suspended/index.tsx index 4c381d44..403595a9 100644 --- a/ui/src/pages/Users/Suspended/index.tsx +++ b/ui/src/pages/Users/Suspended/index.tsx @@ -1,7 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { loggedUserInfoStore } from '@answer/stores'; - +import { loggedUserInfoStore } from '@/stores'; import { PageTitle } from '@/components'; const Suspended = () => { diff --git a/ui/src/router/guarder.ts b/ui/src/router/guarder.ts deleted file mode 100644 index e3b81598..00000000 --- a/ui/src/router/guarder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - pullLoggedUser, - isLoggedAndNormal, - isAdminLogged, - isNotLogged, - isNotLoggedOrNormal, - isLoggedAndInactive, - isLoggedAndSuspended, - isNotLoggedOrInactive, - isNotLoggedOrNotSuspend, -} from '@/utils/guards'; - -const RouteGuarder = { - base: async () => { - return isNotLoggedOrNotSuspend(); - }, - loggedAndNormal: async () => { - await pullLoggedUser(true); - return isLoggedAndNormal(); - }, - loggedAndInactive: async () => { - return isLoggedAndInactive(); - }, - loggedAndSuspended: async () => { - return isLoggedAndSuspended(); - }, - adminLogged: async () => { - await pullLoggedUser(true); - return isAdminLogged(); - }, - notLogged: async () => { - return isNotLogged(); - }, - notLoggedOrNormal: async () => { - return isNotLoggedOrNormal(); - }, - notLoggedOrInactive: async () => { - return isNotLoggedOrInactive(); - }, -}; - -export default RouteGuarder; diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 84eb37a6..e5aa2797 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -28,18 +28,19 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => { } root.push(rn); if (rn.guard) { - const { guard } = rn; - const loaderRef = rn.loader; + const refLoader = rn.loader; + const refGuard = rn.guard; rn.loader = async (args) => { - const gr = await guard(args); + const gr = await refGuard(); if (gr?.redirect && floppyNavigation.differentCurrent(gr.redirect)) { return redirect(gr.redirect); } - let ret; - if (typeof loaderRef === 'function') { - ret = await loaderRef(args); + + let lr; + if (typeof refLoader === 'function') { + lr = await refLoader(args); } - return ret; + return lr; }; } const children = Array.isArray(rn.children) ? rn.children : null; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 5b49fb1a..126dbac0 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -1,18 +1,28 @@ import { RouteObject } from 'react-router-dom'; -import RouteGuarder from '@/router/guarder'; +import { Guard } from '@/utils'; +import type { TGuardResult } from '@/utils/guard'; export interface RouteNode extends RouteObject { page: string; children?: RouteNode[]; - guard?: Function; + /** + * a method to auto guard route before route enter + * if the `ok` field in guard returned `TGuardResult` is true, + * it means the guard passed then enter the route. + * if guard returned the `TGuardResult` has `redirect` field, + * then auto redirect route to the `redirect` target. + */ + guard?: () => Promise; } const routes: RouteNode[] = [ { path: '/', page: 'pages/Layout', - guard: RouteGuarder.base, + guard: async () => { + return Guard.notForbidden(); + }, children: [ // question and answer { @@ -35,12 +45,16 @@ const routes: RouteNode[] = [ { path: 'questions/ask', page: 'pages/Questions/Ask', - guard: RouteGuarder.loggedAndNormal, + guard: async () => { + return Guard.activated(); + }, }, { path: 'posts/:qid/edit', page: 'pages/Questions/Ask', - guard: RouteGuarder.loggedAndNormal, + guard: async () => { + return Guard.activated(); + }, }, { path: 'posts/:qid/:aid/edit', @@ -66,6 +80,9 @@ const routes: RouteNode[] = [ { path: 'tags/:tagId/edit', page: 'pages/Tags/Edit', + guard: async () => { + return Guard.activated(); + }, }, // users { @@ -79,6 +96,9 @@ const routes: RouteNode[] = [ { path: 'users/settings', page: 'pages/Users/Settings', + guard: async () => { + return Guard.logged(); + }, children: [ { index: true, @@ -109,55 +129,85 @@ const routes: RouteNode[] = [ { path: 'users/login', page: 'pages/Users/Login', - guard: RouteGuarder.notLoggedOrInactive, + guard: async () => { + const notLogged = Guard.notLogged(); + if (notLogged.ok) { + return notLogged; + } + return Guard.notActivated(); + }, }, { path: 'users/register', page: 'pages/Users/Register', - guard: RouteGuarder.notLogged, + guard: async () => { + return Guard.notLogged(); + }, }, { path: 'users/account-recovery', page: 'pages/Users/AccountForgot', - guard: RouteGuarder.loggedAndNormal, + guard: async () => { + return Guard.activated(); + }, }, { path: 'users/change-email', page: 'pages/Users/ChangeEmail', + // TODO: guard this (change email when user not activated) ? }, { path: 'users/password-reset', page: 'pages/Users/PasswordReset', - guard: RouteGuarder.loggedAndNormal, + guard: async () => { + return Guard.activated(); + }, }, - // TODO: guard '/account-activation/*', '/users/confirm-new-email' { path: 'users/account-activation', page: 'pages/Users/ActiveEmail', - guard: RouteGuarder.loggedAndInactive, + guard: async () => { + const notActivated = Guard.notActivated(); + if (notActivated.ok) { + return notActivated; + } + return Guard.notLogged(); + }, }, { path: 'users/account-activation/success', page: 'pages/Users/ActivationResult', + guard: async () => { + return Guard.activated(); + }, }, { path: '/users/account-activation/failed', page: 'pages/Users/ActivationResult', + guard: async () => { + return Guard.notActivated(); + }, }, { path: '/users/confirm-new-email', page: 'pages/Users/ConfirmNewEmail', + // TODO: guard this }, { path: '/users/account-suspended', page: 'pages/Users/Suspended', - guard: RouteGuarder.loggedAndSuspended, + guard: async () => { + return Guard.forbidden(); + }, }, // for admin { path: 'admin', page: 'pages/Admin', - guard: RouteGuarder.adminLogged, + guard: async () => { + await Guard.pullLoggedUser(true); + return Guard.admin(); + }, children: [ { index: true, diff --git a/ui/src/services/admin/answer.ts b/ui/src/services/admin/answer.ts index 6fd0fbb6..c3340502 100644 --- a/ui/src/services/admin/answer.ts +++ b/ui/src/services/admin/answer.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useAnswerSearch = (params: Type.AdminContentsReq) => { const apiUrl = `/answer/admin/api/answer/page?${qs.stringify(params)}`; diff --git a/ui/src/services/admin/flag.ts b/ui/src/services/admin/flag.ts index 710cb447..64ea59f7 100644 --- a/ui/src/services/admin/flag.ts +++ b/ui/src/services/admin/flag.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const putReport = (params) => { return request.instance.put('/answer/admin/api/report', params); diff --git a/ui/src/services/admin/question.ts b/ui/src/services/admin/question.ts index a6308bf6..9e8d726a 100644 --- a/ui/src/services/admin/question.ts +++ b/ui/src/services/admin/question.ts @@ -1,8 +1,8 @@ import qs from 'qs'; import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const changeUserStatus = (params) => { return request.put('/answer/admin/api/user/status', params); diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index e1f486e7..274e24eb 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -1,7 +1,7 @@ import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useGeneralSetting = () => { const apiUrl = `/answer/admin/api/siteinfo/general`; diff --git a/ui/src/services/client/activity.ts b/ui/src/services/client/activity.ts index 7b7b539c..6f1a4fdb 100644 --- a/ui/src/services/client/activity.ts +++ b/ui/src/services/client/activity.ts @@ -1,7 +1,7 @@ import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useFollow = (params?: Type.FollowParams) => { const apiUrl = '/answer/api/v1/follow'; diff --git a/ui/src/services/client/notification.ts b/ui/src/services/client/notification.ts index dd9d880e..a849db0a 100644 --- a/ui/src/services/client/notification.ts +++ b/ui/src/services/client/notification.ts @@ -1,10 +1,9 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; - -import { tryNormalLogged } from '@/utils/guards'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; +import { tryNormalLogged } from '@/utils/guard'; export const useQueryNotifications = (params) => { const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, { diff --git a/ui/src/services/client/personal.ts b/ui/src/services/client/personal.ts index b84e260f..6b61aaba 100644 --- a/ui/src/services/client/personal.ts +++ b/ui/src/services/client/personal.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const usePersonalInfoByName = (username: string) => { const apiUrl = '/answer/api/v1/personal/user/info'; diff --git a/ui/src/services/client/question.ts b/ui/src/services/client/question.ts index c35fe544..c418f26c 100644 --- a/ui/src/services/client/question.ts +++ b/ui/src/services/client/question.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useQuestionList = (params: Type.QueryQuestionsReq) => { const apiUrl = `/answer/api/v1/question/page?${qs.stringify(params)}`; diff --git a/ui/src/services/client/search.ts b/ui/src/services/client/search.ts index f5fe86fe..8d380294 100644 --- a/ui/src/services/client/search.ts +++ b/ui/src/services/client/search.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useSearch = (params?: Type.SearchParams) => { const apiUrl = '/answer/api/v1/search'; diff --git a/ui/src/services/client/tag.ts b/ui/src/services/client/tag.ts index 634b4472..42b9f1ac 100644 --- a/ui/src/services/client/tag.ts +++ b/ui/src/services/client/tag.ts @@ -1,9 +1,8 @@ import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; - -import { tryNormalLogged } from '@/utils/guards'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; +import { tryNormalLogged } from '@/utils/guard'; export const deleteTag = (id) => { return request.delete('/answer/api/v1/tag', { diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 6f2cf83a..3e9e7872 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -1,8 +1,8 @@ import qs from 'qs'; import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const uploadImage = (file) => { const form = new FormData(); diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index 9aa540b5..017c3149 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -1,8 +1,7 @@ import create from 'zustand'; -import type { UserInfoRes } from '@answer/common/interface'; -import Storage from '@answer/utils/storage'; - +import type { UserInfoRes } from '@/common/interface'; +import Storage from '@/utils/storage'; import { LOGGED_USER_STORAGE_KEY, LOGGED_TOKEN_STORAGE_KEY, @@ -15,6 +14,7 @@ interface UserInfoStore { } const initUser: UserInfoRes = { + access_token: '', username: '', avatar: '', rank: 0, diff --git a/ui/src/utils/floppyNavigation.ts b/ui/src/utils/floppyNavigation.ts index 7edbbaa0..68e6f8a5 100644 --- a/ui/src/utils/floppyNavigation.ts +++ b/ui/src/utils/floppyNavigation.ts @@ -25,7 +25,8 @@ const navigate = (pathname: string, callback: Function) => { const navigateToLogin = () => { const { pathname } = window.location; if (pathname !== RouteAlias.login && pathname !== RouteAlias.register) { - const redirectUrl = window.location.href; + const loc = window.location; + const redirectUrl = loc.href.replace(loc.origin, ''); Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl); } navigate(RouteAlias.login, () => { diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts new file mode 100644 index 00000000..bc73ab9a --- /dev/null +++ b/ui/src/utils/guard.ts @@ -0,0 +1,182 @@ +import { getLoggedUserInfo } from '@/services'; +import { loggedUserInfoStore } from '@/stores'; +import { RouteAlias } from '@/router/alias'; +import Storage from '@/utils/storage'; +import { LOGGED_USER_STORAGE_KEY } from '@/common/constants'; + +import { floppyNavigation } from './floppyNavigation'; + +type TLoginState = { + isLogged: boolean; + isNotActivated: boolean; + isActivated: boolean; + isForbidden: boolean; + isNormal: boolean; + isAdmin: boolean; +}; + +export type TGuardResult = { + ok: boolean; + redirect?: string; +}; + +export const deriveLoginState = (): TLoginState => { + const ls: TLoginState = { + isLogged: false, + isNotActivated: false, + isActivated: false, + isForbidden: false, + isNormal: false, + isAdmin: false, + }; + const { user } = loggedUserInfoStore.getState(); + if (user.access_token) { + ls.isLogged = true; + } + if (ls.isLogged && user.mail_status === 1) { + ls.isActivated = true; + } + if (ls.isLogged && user.mail_status === 2) { + ls.isNotActivated = true; + } + if (ls.isLogged && user.status === 'forbidden') { + ls.isForbidden = true; + } + if (ls.isActivated && !ls.isForbidden) { + ls.isNormal = true; + } + if (ls.isNormal && user.is_admin === true) { + ls.isAdmin = true; + } + + return ls; +}; + +let pullLock = false; +let dedupeTimestamp = 0; +export const pullLoggedUser = async (forceRePull = false) => { + // only pull once if not force re-pull + if (pullLock && !forceRePull) { + return; + } + // dedupe pull requests in this time span in 10 seconds + if (Date.now() - dedupeTimestamp < 1000 * 10) { + return; + } + dedupeTimestamp = Date.now(); + const loggedUserInfo = await getLoggedUserInfo().catch((ex) => { + dedupeTimestamp = 0; + if (!deriveLoginState().isLogged) { + // load fallback userInfo from local storage + const storageLoggedUserInfo = Storage.get(LOGGED_USER_STORAGE_KEY); + if (storageLoggedUserInfo) { + loggedUserInfoStore.getState().update(storageLoggedUserInfo); + } + } + console.error(ex); + }); + if (loggedUserInfo) { + pullLock = true; + loggedUserInfoStore.getState().update(loggedUserInfo); + } +}; + +export const logged = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + if (!us.isLogged) { + gr.ok = false; + gr.redirect = RouteAlias.login; + } + return gr; +}; + +export const notLogged = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + if (us.isLogged) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +export const notActivated = () => { + const gr = logged(); + const us = deriveLoginState(); + if (us.isActivated) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +export const activated = () => { + const gr = logged(); + const us = deriveLoginState(); + if (us.isNotActivated) { + gr.ok = false; + gr.redirect = RouteAlias.activation; + } + return gr; +}; + +export const forbidden = () => { + const gr = logged(); + const us = deriveLoginState(); + if (gr.ok && !us.isForbidden) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +export const notForbidden = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + if (us.isForbidden) { + gr.ok = false; + gr.redirect = RouteAlias.suspended; + } + return gr; +}; + +export const admin = () => { + const gr = logged(); + const us = deriveLoginState(); + if (gr.ok && !us.isAdmin) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +/** + * try user was logged and all state ok + * @param autoLogin + */ +export const tryNormalLogged = (autoLogin: boolean = false) => { + const us = deriveLoginState(); + + if (us.isNormal) { + return true; + } + // must assert logged state first and return + if (!us.isLogged) { + if (autoLogin) { + floppyNavigation.navigateToLogin(); + } + return false; + } + if (us.isNotActivated) { + floppyNavigation.navigate(RouteAlias.activation, () => { + window.location.href = RouteAlias.activation; + }); + } else if (us.isForbidden) { + floppyNavigation.navigate(RouteAlias.suspended, () => { + window.location.replace(RouteAlias.suspended); + }); + } + + return false; +}; diff --git a/ui/src/utils/guards.ts b/ui/src/utils/guards.ts deleted file mode 100644 index 47f79605..00000000 --- a/ui/src/utils/guards.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { getLoggedUserInfo } from '@/services'; -import { loggedUserInfoStore } from '@/stores'; -import { RouteAlias } from '@/router/alias'; -import Storage from '@/utils/storage'; -import { LOGGED_USER_STORAGE_KEY } from '@/common/constants'; -import { floppyNavigation } from '@/utils/floppyNavigation'; - -type UserStat = { - isLogged: boolean; - isActivated: boolean; - isSuspended: boolean; - isNormal: boolean; - isAdmin: boolean; -}; -export const deriveUserStat = (): UserStat => { - const stat: UserStat = { - isLogged: false, - isActivated: false, - isSuspended: false, - isNormal: false, - isAdmin: false, - }; - const { user } = loggedUserInfoStore.getState(); - if (user.id && user.username) { - stat.isLogged = true; - } - if (stat.isLogged && user.mail_status === 1) { - stat.isActivated = true; - } - if (stat.isLogged && user.status === 'forbidden') { - stat.isSuspended = true; - } - if (stat.isLogged && stat.isActivated && !stat.isSuspended) { - stat.isNormal = true; - } - if (stat.isNormal && user.is_admin === true) { - stat.isAdmin = true; - } - - return stat; -}; - -type GuardResult = { - ok: boolean; - redirect?: string; -}; -let pullLock = false; -let dedupeTimestamp = 0; -export const pullLoggedUser = async (forceRePull = false) => { - // only pull once if not force re-pull - if (pullLock && !forceRePull) { - return; - } - // dedupe pull requests in this time span in 10 seconds - if (Date.now() - dedupeTimestamp < 1000 * 10) { - return; - } - dedupeTimestamp = Date.now(); - const loggedUserInfo = await getLoggedUserInfo().catch((ex) => { - dedupeTimestamp = 0; - if (!deriveUserStat().isLogged) { - // load fallback userInfo from local storage - const storageLoggedUserInfo = Storage.get(LOGGED_USER_STORAGE_KEY); - if (storageLoggedUserInfo) { - loggedUserInfoStore.getState().update(storageLoggedUserInfo); - } - } - console.error(ex); - }); - if (loggedUserInfo) { - pullLock = true; - loggedUserInfoStore.getState().update(loggedUserInfo); - } -}; - -export const isLogged = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (!userStat.isLogged) { - ret.ok = false; - ret.redirect = RouteAlias.login; - } - return ret; -}; - -export const isNotLogged = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (userStat.isLogged) { - ret.ok = false; - ret.redirect = RouteAlias.home; - } - return ret; -}; - -export const isLoggedAndInactive = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (userStat.isActivated) { - ret.ok = false; - ret.redirect = RouteAlias.home; - } - return ret; -}; - -export const isLoggedAndSuspended = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (!userStat.isSuspended) { - ret.ok = false; - ret.redirect = RouteAlias.home; - } - return ret; -}; - -export const isLoggedAndNormal = () => { - const ret: GuardResult = { ok: false, redirect: undefined }; - const userStat = deriveUserStat(); - if (userStat.isNormal) { - ret.ok = true; - } else if (!userStat.isActivated) { - ret.redirect = RouteAlias.activation; - } else if (userStat.isSuspended) { - ret.redirect = RouteAlias.suspended; - } else if (!userStat.isLogged) { - ret.redirect = RouteAlias.login; - } - return ret; -}; - -export const isNotLoggedOrNormal = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - const gr = isLoggedAndNormal(); - if (!gr.ok && userStat.isLogged) { - ret.ok = false; - ret.redirect = gr.redirect; - } - return ret; -}; - -export const isNotLoggedOrNotSuspend = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - const gr = isLoggedAndNormal(); - if (!gr.ok && userStat.isSuspended) { - ret.ok = false; - ret.redirect = gr.redirect; - } - return ret; -}; - -export const isNotLoggedOrInactive = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (userStat.isActivated) { - ret.ok = false; - ret.redirect = RouteAlias.home; - } else if (userStat.isSuspended) { - ret.ok = false; - ret.redirect = RouteAlias.suspended; - } - return ret; -}; - -export const isAdminLogged = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (!userStat.isAdmin) { - ret.redirect = RouteAlias.home; - ret.ok = false; - } - return ret; -}; - -/** - * try user was logged and all state ok - * @param autoLogin - */ -export const tryNormalLogged = (autoLogin: boolean = false) => { - const gr = isLoggedAndNormal(); - if (gr.ok) { - return true; - } - - if (gr.redirect === RouteAlias.login && autoLogin) { - floppyNavigation.navigateToLogin(); - } else if (gr.redirect) { - floppyNavigation.navigate(gr.redirect, () => { - // @ts-ignore - window.location.replace(gr.redirect); - }); - } - - return false; -}; diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts index a1eaf02c..69f70696 100644 --- a/ui/src/utils/index.ts +++ b/ui/src/utils/index.ts @@ -1,6 +1,6 @@ -export * from './common'; -export * as guards from './guards'; - export { default as request } from './request'; export { default as Storage } from './storage'; export { floppyNavigation } from './floppyNavigation'; + +export * as Guard from './guard'; +export * from './common'; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index d80cadf5..4e878cb3 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -1,12 +1,8 @@ import axios, { AxiosResponse } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; -import { Modal } from '@answer/components'; -import { loggedUserInfoStore, toastStore } from '@answer/stores'; - -import Storage from './storage'; -import { floppyNavigation } from './floppyNavigation'; - +import { Modal } from '@/components'; +import { loggedUserInfoStore, toastStore } from '@/stores'; import { LOGGED_TOKEN_STORAGE_KEY, CURRENT_LANG_STORAGE_KEY, @@ -14,6 +10,9 @@ import { } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; +import Storage from './storage'; +import { floppyNavigation } from './floppyNavigation'; + const API = { development: '', production: '', diff --git a/ui/tsconfig.json b/ui/tsconfig.json index d7c5decf..f270b720 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -20,18 +20,7 @@ "jsx": "react-jsx", "baseUrl": "./", "paths": { - "@/*": ["src/*"], - "@answer/pages/*": ["src/pages/*"], - "@answer/components": ["src/components/index.ts"], - "@answer/components/*": ["src/components/*"], - "@answer/stores": ["src/stores"], - "@answer/stores/*": ["src/stores/*"], - "@answer/services/*": ["src/services/*"], - "@answer/hooks": ["src/hooks"], - "@answer/common": ["src/common"], - "@answer/common/*": ["src/common/*"], - "@answer/utils": ["src/utils"], - "@answer/utils/*": ["src/utils/*"] + "@/*": ["src/*"] } }, "include": ["src", "node_modules/@testing-library/jest-dom"] From f196972fff1e87f678447b09229d4e44e659a7ae Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Wed, 2 Nov 2022 17:23:41 +0800 Subject: [PATCH 025/157] ci(admin/interface): fix interface merge issue --- ui/src/pages/Admin/Interface/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index ec8c770e..395b9616 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -8,11 +8,9 @@ import { FormDataType, AdminSettingsInterface, } from '@/common/interface'; - import { interfaceStore } from '@/stores'; import { UploadImg } from '@/components'; import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; - import { languages, uploadAvatar, From bf4e9824f59a1006d7c029163c9d5be84725a911 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 2 Nov 2022 17:26:55 +0800 Subject: [PATCH 026/157] fix: handle install page forms --- ui/src/common/interface.ts | 2 +- ui/src/components/Header/index.scss | 13 + ui/src/components/Header/index.tsx | 4 +- ui/src/i18n/locales/en.json | 31 ++- ui/src/index.scss | 1 + .../Install/components/FirstStep/index.tsx | 58 ++++- .../Install/components/FourthStep/index.tsx | 172 +++++++++++- .../Install/components/SecondStep/index.tsx | 245 ++++++++++++++++-- .../Install/components/ThirdStep/index.tsx | 5 +- ui/src/pages/Install/index.tsx | 135 +++++++++- ui/src/pages/Maintenance/index.tsx | 22 +- ui/src/pages/Upgrade/index.tsx | 64 ++--- .../Personal/components/DefaultList/index.tsx | 2 +- .../Personal/components/NavBar/index.tsx | 5 +- .../Personal/components/UserInfo/index.tsx | 6 +- .../Users/Personal/components/Votes/index.tsx | 2 +- ui/src/pages/Users/Personal/index.tsx | 6 +- 17 files changed, 651 insertions(+), 122 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 7516a2d7..732964d1 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -264,7 +264,7 @@ export interface AdminSettingsInterface { logo: string; language: string; theme: string; - time_zone: string; + time_zone?: string; } export interface AdminSettingsSmtp { diff --git a/ui/src/components/Header/index.scss b/ui/src/components/Header/index.scss index 348632a3..f3f67b78 100644 --- a/ui/src/components/Header/index.scss +++ b/ui/src/components/Header/index.scss @@ -50,6 +50,10 @@ @media (max-width: 992.9px) { #header { + .logo { + max-width: 93px; + max-height: auto; + } .nav-grow { flex-grow: 1!important; } @@ -65,3 +69,12 @@ } +@media (max-width: 576px) { + #header { + .logo { + max-width: 93px; + max-height: auto; + } + } +} + diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 748eb318..4de50cee 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -71,8 +71,8 @@ const Header: FC = () => { id="navBarToggle" /> -
- +
+ {interfaceInfo.logo ? ( void; + nextCallback: () => void; visible: boolean; } -const Index: FC = ({ visible }) => { +const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); + const [langs, setLangs] = useState(); + + const getLangs = async () => { + const res: LangsType[] = await languages(); + setLangs(res); + }; + + const handleSubmit = () => { + nextCallback(); + }; + + useEffect(() => { + getLangs(); + }, []); + if (!visible) return null; return ( -
- - {t('choose_lang.label')} - - + + + {t('lang.label')} + { + changeCallback({ + lang: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }}> + {langs?.map((item) => { + return ( + + ); + })}
- +
); diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index 25fb47ef..ccd44be2 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -1,50 +1,204 @@ -import { FC } from 'react'; +import { FC, FormEvent } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import type { FormDataType } from '@/common/interface'; import Progress from '../Progress'; interface Props { + data: FormDataType; + changeCallback: (value: FormDataType) => void; + nextCallback: () => void; visible: boolean; } -const Index: FC = ({ visible }) => { +const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); + const checkValidated = (): boolean => { + let bol = true; + const { + site_name, + contact_email, + admin_name, + admin_password, + admin_email, + } = data; + + if (!site_name.value) { + bol = false; + data.site_name = { + value: '', + isInvalid: true, + errorMsg: t('site_name.msg'), + }; + } + + if (!contact_email.value) { + bol = false; + data.contact_email = { + value: '', + isInvalid: true, + errorMsg: t('contact_email.msg'), + }; + } + + if (!admin_name.value) { + bol = false; + data.admin_name = { + value: '', + isInvalid: true, + errorMsg: t('admin_name.msg'), + }; + } + + if (!admin_password.value) { + bol = false; + data.admin_password = { + value: '', + isInvalid: true, + errorMsg: t('admin_password.msg'), + }; + } + + if (!admin_email.value) { + bol = false; + data.admin_email = { + value: '', + isInvalid: true, + errorMsg: t('admin_email.msg'), + }; + } + + changeCallback({ + ...data, + }); + return bol; + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!checkValidated()) { + return; + } + nextCallback(); + }; + if (!visible) return null; return ( -
+
{t('site_information')}
{t('site_name.label')} - + { + changeCallback({ + site_name: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.site_name.errorMsg} + {t('contact_email.label')} - + { + changeCallback({ + contact_email: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> {t('contact_email.text')} + + {data.contact_email.errorMsg} +
{t('admin_account')}
{t('admin_name.label')} - + { + changeCallback({ + admin_name: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.admin_name.errorMsg} + {t('admin_password.label')} - + { + changeCallback({ + admin_password: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> {t('admin_password.text')} + + {data.admin_password.errorMsg} + {t('admin_email.label')} - + { + changeCallback({ + admin_email: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> {t('admin_email.text')} + + {data.admin_email.errorMsg} +
- +
); diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx index 7b21ab73..6c97ec61 100644 --- a/ui/src/pages/Install/components/SecondStep/index.tsx +++ b/ui/src/pages/Install/components/SecondStep/index.tsx @@ -1,54 +1,243 @@ -import { FC } from 'react'; +import { FC, FormEvent } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import Progress from '../Progress'; +import type { FormDataType } from '@/common/interface'; interface Props { + data: FormDataType; + changeCallback: (value: FormDataType) => void; + nextCallback: () => void; visible: boolean; } -const Index: FC = ({ visible }) => { +const sqlData = [ + { + value: 'mysql', + label: 'MariaDB/MySQL', + }, + { + value: 'sqlite3', + label: 'SQLite', + }, + { + value: 'postgres', + label: 'PostgreSQL', + }, +]; + +const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); + const checkValidated = (): boolean => { + let bol = true; + const { db_type, db_username, db_password, db_host, db_name, db_file } = + data; + + if (db_type.value !== 'sqllite3') { + if (!db_username.value) { + bol = false; + data.db_username = { + value: '', + isInvalid: true, + errorMsg: t('db_username.msg'), + }; + } + + if (!db_password.value) { + bol = false; + data.db_password = { + value: '', + isInvalid: true, + errorMsg: t('db_password.msg'), + }; + } + + if (!db_host.value) { + bol = false; + data.db_host = { + value: '', + isInvalid: true, + errorMsg: t('db_host.msg'), + }; + } + + if (!db_name.value) { + bol = false; + data.db_name = { + value: '', + isInvalid: true, + errorMsg: t('db_name.msg'), + }; + } + } else if (!db_file.value) { + bol = false; + data.db_file = { + value: '', + isInvalid: true, + errorMsg: t('db_file.msg'), + }; + } + changeCallback({ + ...data, + }); + return bol; + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!checkValidated()) { + return; + } + nextCallback(); + }; + if (!visible) return null; return ( -
+ - {t('database_engine.label')} - - + {t('db_type.label')} + { + changeCallback({ + db_type: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }}> + {sqlData.map((item) => { + return ( + + ); + })} + {data.db_type.value !== 'sqlite3' ? ( + <> + + {t('db_username.label')} + { + changeCallback({ + db_username: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_username.errorMsg} + + - - {t('username.label')} - - + + {t('db_password.label')} + { + changeCallback({ + db_password: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> - - {t('password.label')} - - + + {data.db_password.errorMsg} + + - - {t('database_host.label')} - - + + {t('db_host.label')} + { + changeCallback({ + db_host: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_host.errorMsg} + + - - {t('database_name.label')} - - - - - {t('table_prefix.label')} - - + + {t('db_name.label')} + { + changeCallback({ + db_name: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_name.errorMsg} + + + + ) : ( + + {t('db_file.label')} + { + changeCallback({ + db_file: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_file.errorMsg} + + + )}
- +
); diff --git a/ui/src/pages/Install/components/ThirdStep/index.tsx b/ui/src/pages/Install/components/ThirdStep/index.tsx index 4d3f702f..3b5ca08c 100644 --- a/ui/src/pages/Install/components/ThirdStep/index.tsx +++ b/ui/src/pages/Install/components/ThirdStep/index.tsx @@ -6,9 +6,10 @@ import Progress from '../Progress'; interface Props { visible: boolean; + nextCallback: () => void; } -const Index: FC = ({ visible }) => { +const Index: FC = ({ visible, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); if (!visible) return null; @@ -30,7 +31,7 @@ const Index: FC = ({ visible }) => {
{t('config_yaml.info')}
- +
); diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 8de7cb11..e8c40d5a 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -1,7 +1,11 @@ -import { FC, useState } from 'react'; +import { FC, useState, useEffect } from 'react'; import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; import { useTranslation, Trans } from 'react-i18next'; +import type { FormDataType } from '@/common/interface'; +import { Storage } from '@/utils'; +import { PageTitle } from '@/components'; + import { FirstStep, SecondStep, @@ -10,14 +14,109 @@ import { Fifth, } from './components'; -import { PageTitle } from '@/components'; - const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step] = useState(7); + const [step, setStep] = useState(1); + const [showError] = useState(false); + + const [formData, setFormData] = useState({ + lang: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_type: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_username: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_password: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_host: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_file: { + value: '', + isInvalid: false, + errorMsg: '', + }, + + site_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + contact_email: { + value: '', + isInvalid: false, + errorMsg: '', + }, + admin_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + admin_password: { + value: '', + isInvalid: false, + errorMsg: '', + }, + admin_email: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }); + + const handleChange = (params: FormDataType) => { + console.log(params); + setFormData({ ...formData, ...params }); + }; + + const handleStep = () => { + setStep((pre) => pre + 1); + }; + + // const handleSubmit = () => { + // const params = { + // lang: formData.lang.value, + // db_type: formData.db_type.value, + // db_username: formData.db_username.value, + // db_password: formData.db_password.value, + // db_host: formData.db_host.value, + // db_name: formData.db_name.value, + // db_file: formData.db_file.value, + // site_name: formData.site_name.value, + // contact_email: formData.contact_email.value, + // admin_name: formData.admin_name.value, + // admin_password: formData.admin_password.value, + // admin_email: formData.admin_email.value, + // }; + + // console.log(params); + // }; + + useEffect(() => { + console.log('step===', Storage.get('INSTALL_STEP')); + }, []); return ( -
+
@@ -25,14 +124,30 @@ const Index: FC = () => {

{t('title')}

- show error msg - + {showError && show error msg } - + - + - + + + {step === 6 && ( diff --git a/ui/src/pages/Maintenance/index.tsx b/ui/src/pages/Maintenance/index.tsx index 3a2c2d86..560108bf 100644 --- a/ui/src/pages/Maintenance/index.tsx +++ b/ui/src/pages/Maintenance/index.tsx @@ -8,15 +8,19 @@ const Index = () => { keyPrefix: 'page_maintenance', }); return ( - - -
- (=‘_‘=) -
-
{t('description')}
-
+
+ + +
+ (=‘_‘=) +
+
{t('description')}
+
+
); }; diff --git a/ui/src/pages/Upgrade/index.tsx b/ui/src/pages/Upgrade/index.tsx index aee2a37a..c65c9098 100644 --- a/ui/src/pages/Upgrade/index.tsx +++ b/ui/src/pages/Upgrade/index.tsx @@ -14,38 +14,40 @@ const Index = () => { setStep(2); }; return ( - - - -
{t('post')}{t('votes')}{t('created')}{t('status')}{t('action')}{t('votes')}{t('created')}{t('status')}{t('action')}
-
- - - + + + - - {curFilter !== 'deleted' && } + + {curFilter !== 'deleted' && ( + + )} @@ -177,7 +179,10 @@ const Questions: FC = () => { {curFilter !== 'deleted' && ( diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index ae8942b4..58b849d1 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -86,20 +86,22 @@ const Users: FC = () => {
{t('post')}{t('votes')}{t('answers')}{t('post')}{t('votes')}{t('answers')} {t('created')}{t('status')}{t('action')}{t('status')}{t('action')}
-
- - + + - {(curFilter === 'deleted' || curFilter === 'suspended') && ( - )} - - {curFilter !== 'deleted' ? : null} + + {curFilter !== 'deleted' ? ( + + ) : null} @@ -136,7 +138,7 @@ const Users: FC = () => { diff --git a/ui/src/pages/Admin/Flags/index.tsx b/ui/src/pages/Admin/Flags/index.tsx index 353bd382..ff85ea1e 100644 --- a/ui/src/pages/Admin/Flags/index.tsx +++ b/ui/src/pages/Admin/Flags/index.tsx @@ -13,6 +13,7 @@ import { import { useReportModal } from '@/hooks'; import * as Type from '@/common/interface'; import { useFlagSearch } from '@/services'; +import { escapeRemove } from '@/utils'; import '../index.scss'; @@ -107,7 +108,7 @@ const Flags: FC = () => { {li.title} - {li.excerpt} + {escapeRemove(li.excerpt)} diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index 095796d2..f8d07e7a 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { following } from '@/services'; import { tryNormalLogged } from '@/utils/guard'; +import { escapeRemove } from '@/utils'; interface Props { data; @@ -51,7 +52,7 @@ const Index: FC = ({ data }) => { <> {data.excerpt && (

- {data.excerpt} + {escapeRemove(data.excerpt)} [{t('more')}]

)} diff --git a/ui/src/pages/Search/components/SearchItem/index.tsx b/ui/src/pages/Search/components/SearchItem/index.tsx index 09cecd32..ca4df6e6 100644 --- a/ui/src/pages/Search/components/SearchItem/index.tsx +++ b/ui/src/pages/Search/components/SearchItem/index.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { Icon, Tag, FormatTime, BaseUserCard } from '@/components'; import type { SearchResItem } from '@/common/interface'; +import { escapeRemove } from '@/utils'; interface Props { data: SearchResItem; @@ -61,7 +62,7 @@ const Index: FC = ({ data }) => { {data.object?.excerpt && (

- {data.object.excerpt} + {escapeRemove(data.object.excerpt)}

)} diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index 452ef37d..26252e1e 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -8,6 +8,7 @@ import { PageTitle, FollowingTags } from '@/components'; import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; +import { escapeRemove } from '@/utils'; const Questions: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'tags' }); @@ -69,7 +70,7 @@ const Questions: FC = () => {

- {tagInfo.excerpt || t('no_description')} + {escapeRemove(tagInfo.excerpt) || t('no_description')} [{t('more')}]

diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index 31bb0a40..2f846382 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -86,6 +86,22 @@ function formatUptime(value) { return `< 1 ${t('dates.hour')}`; } +function escapeRemove(str) { + if (!str || typeof str !== 'string') return str; + const arrEntities = { + lt: '<', + gt: '>', + nbsp: ' ', + amp: '&', + quot: '"', + '#39': "'", + }; + + return str.replace(/&(lt|gt|nbsp|amp|quot|#39);/gi, function (all, t) { + return arrEntities[t]; + }); +} + export { getQueryString, thousandthDivision, @@ -94,4 +110,5 @@ export { matchedUsers, parseUserInfo, formatUptime, + escapeRemove, }; From cbe0bdb17a31ada0ad7209c7729159794e194964 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 10 Nov 2022 16:59:28 +0800 Subject: [PATCH 149/157] fix: install page add form error handle --- .../pages/Install/components/FourthStep/index.tsx | 2 +- ui/src/pages/Install/index.tsx | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index 3807ac69..412b1912 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -101,7 +101,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { if (admin_email.value && !admin_email.value.match(mailReg)) { bol = false; data.admin_email = { - value: '', + value: admin_email.value, isInvalid: true, errorMsg: t('admin_email.msg.incorrect'), }; diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index d4170ca2..8e9e22a3 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -76,7 +76,7 @@ const Index: FC = () => { errorMsg: '', }, site_url: { - value: '', + value: window.location.origin, isInvalid: false, errorMsg: '', }, @@ -104,6 +104,9 @@ const Index: FC = () => { const handleChange = (params: FormDataType) => { // console.log(params); + setErrorData({ + msg: '', + }); setFormData({ ...formData, ...params }); }; @@ -150,11 +153,9 @@ const Index: FC = () => { }; dbCheck(params) .then(() => { - // handleNext(); checkInstall(); }) .catch((err) => { - console.log(err); handleErr(err); }); }; @@ -174,7 +175,13 @@ const Index: FC = () => { handleNext(); }) .catch((err) => { - handleErr(err); + if (err.isError && err.key) { + formData[err.key].isInvalid = true; + formData[err.key].errorMsg = err.value; + setFormData({ ...formData }); + } else { + handleErr(err); + } }); }; From 6ce4fc8cb6cc11a0dda6eca9d1fa072ca6fe0f65 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 10 Nov 2022 17:02:52 +0800 Subject: [PATCH 150/157] fix: style adjustment --- ui/src/pages/Search/components/Head/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index f8d07e7a..45906e05 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -41,7 +41,7 @@ const Index: FC = ({ data }) => {
{options?.length && ( <> - {t('options')} + {t('options')} {options?.map((item) => { return {item} ; })} From a8e9bf90cad4a8573cb19f1593ab67dcf5f3ba97 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 11 Nov 2022 11:21:27 +0800 Subject: [PATCH 151/157] fix: dashbord adjustment --- .../components/HealthStatus/index.tsx | 11 ++-- .../Dashboard/components/Statistics/index.tsx | 9 +-- .../Install/components/FourthStep/index.tsx | 61 +++++++++---------- ui/src/pages/Install/index.tsx | 30 ++++----- 4 files changed, 56 insertions(+), 55 deletions(-) diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx index f36e02a5..fd511ca1 100644 --- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -49,10 +49,13 @@ const HealthStatus: FC = ({ data }) => {
{t('smtp')} - {data.smtp ? t('enabled') : t('disabled')} - - {t('config')} - + {data.smtp ? ( + {t('enabled')} + ) : ( + + {t('config')} + + )} {t('timezone')} diff --git a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx index af798c8e..c12bc320 100644 --- a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx @@ -38,10 +38,11 @@ const Statistics: FC = ({ data }) => { {t('flags')} - {data.report_count} - - {t('review')} - + + + {data.report_count} + + diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index 412b1912..0f7d7b88 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -16,14 +16,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const checkValidated = (): boolean => { let bol = true; - const { - site_name, - site_url, - contact_email, - admin_name, - admin_password, - admin_email, - } = data; + const { site_name, site_url, contact_email, name, password, email } = data; if (!site_name.value) { bol = false; @@ -71,37 +64,37 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { }; } - if (!admin_name.value) { + if (!name.value) { bol = false; - data.admin_name = { + data.name = { value: '', isInvalid: true, errorMsg: t('admin_name.msg'), }; } - if (!admin_password.value) { + if (!password.value) { bol = false; - data.admin_password = { + data.password = { value: '', isInvalid: true, errorMsg: t('admin_password.msg'), }; } - if (!admin_email.value) { + if (!email.value) { bol = false; - data.admin_email = { + data.email = { value: '', isInvalid: true, errorMsg: t('admin_email.msg.empty'), }; } - if (admin_email.value && !admin_email.value.match(mailReg)) { + if (email.value && !email.value.match(mailReg)) { bol = false; - data.admin_email = { - value: admin_email.value, + data.email = { + value: email.value, isInvalid: true, errorMsg: t('admin_email.msg.incorrect'), }; @@ -132,6 +125,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { required value={data.site_name.value} isInvalid={data.site_name.isInvalid} + maxLength={30} onChange={(e) => { changeCallback({ site_name: { @@ -152,6 +146,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { required value={data.site_url.value} isInvalid={data.site_url.isInvalid} + maxLength={512} onChange={(e) => { changeCallback({ site_url: { @@ -191,15 +186,16 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => {
{t('admin_account')}
- + {t('admin_name.label')} { changeCallback({ - admin_name: { + name: { value: e.target.value, isInvalid: false, errorMsg: '', @@ -208,20 +204,21 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { }} /> - {data.admin_name.errorMsg} + {data.name.errorMsg} - + {t('admin_password.label')} { changeCallback({ - admin_password: { + password: { value: e.target.value, isInvalid: false, errorMsg: '', @@ -231,19 +228,19 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { /> {t('admin_password.text')} - {data.admin_password.errorMsg} + {data.password.errorMsg} - + {t('admin_email.label')} { changeCallback({ - admin_email: { + email: { value: e.target.value, isInvalid: false, errorMsg: '', @@ -253,7 +250,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { /> {t('admin_email.text')} - {data.admin_email.errorMsg} + {data.email.errorMsg} diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 8e9e22a3..30e6f720 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -24,7 +24,7 @@ import { const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step, setStep] = useState(1); + const [step, setStep] = useState(4); const [loading, setLoading] = useState(true); const [errorData, setErrorData] = useState<{ [propName: string]: any }>({ msg: '', @@ -36,7 +36,7 @@ const Index: FC = () => { const [formData, setFormData] = useState({ lang: { - value: '', + value: 'en_US', isInvalid: false, errorMsg: '', }, @@ -85,17 +85,17 @@ const Index: FC = () => { isInvalid: false, errorMsg: '', }, - admin_name: { + name: { value: '', isInvalid: false, errorMsg: '', }, - admin_password: { + password: { value: '', isInvalid: false, errorMsg: '', }, - admin_email: { + email: { value: '', isInvalid: false, errorMsg: '', @@ -166,9 +166,9 @@ const Index: FC = () => { site_name: formData.site_name.value, site_url: formData.site_url.value, contact_email: formData.contact_email.value, - admin_name: formData.admin_name.value, - admin_password: formData.admin_password.value, - admin_email: formData.admin_email.value, + name: formData.name.value, + password: formData.password.value, + email: formData.email.value, }; installBaseInfo(params) .then(() => { @@ -225,11 +225,11 @@ const Index: FC = () => { db_connection_success: res.db_connection_success, }); if (res && res.config_file_exist) { - if (res.db_connection_success) { - setStep(6) - } else { - setStep(7); - } + // if (res.db_connection_success) { + // setStep(6) + // } else { + // setStep(7); + // } } }) .finally(() => { @@ -246,9 +246,9 @@ const Index: FC = () => { } return ( -
+
- +

{t('title')}

From ca317c9e48920481a28c5f0080d4d43f225ed640 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 11 Nov 2022 11:22:44 +0800 Subject: [PATCH 152/157] fix: install page handle form error --- ui/src/pages/Install/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 30e6f720..52dca0ec 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -24,7 +24,7 @@ import { const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step, setStep] = useState(4); + const [step, setStep] = useState(1); const [loading, setLoading] = useState(true); const [errorData, setErrorData] = useState<{ [propName: string]: any }>({ msg: '', @@ -225,11 +225,11 @@ const Index: FC = () => { db_connection_success: res.db_connection_success, }); if (res && res.config_file_exist) { - // if (res.db_connection_success) { - // setStep(6) - // } else { - // setStep(7); - // } + if (res.db_connection_success) { + setStep(6) + } else { + setStep(7); + } } }) .finally(() => { From 9707e1b223e10a994931bf163d7cdbf4af39cf9b Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 11 Nov 2022 11:23:56 +0800 Subject: [PATCH 153/157] git dir size --- cmd/answer/wire_gen.go | 2 +- .../service/dashboard/dashboard_service.go | 49 +++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index e7ade4d0..2aa85b92 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -152,7 +152,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, dataData) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index f1ce90ad..3cef6286 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -6,6 +6,8 @@ import ( "fmt" "io/ioutil" "net/http" + "os" + "path/filepath" "time" "github.com/answerdev/answer/internal/base/constant" @@ -19,6 +21,7 @@ import ( "github.com/answerdev/answer/internal/service/export" questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/report_common" + "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" "github.com/segmentfault/pacman/errors" @@ -34,7 +37,9 @@ type DashboardService struct { reportRepo report_common.ReportRepo configRepo config.ConfigRepo siteInfoService *siteinfo_common.SiteInfoCommonService - data *data.Data + serviceConfig *service_config.ServiceConfig + + data *data.Data } func NewDashboardService( @@ -46,6 +51,8 @@ func NewDashboardService( reportRepo report_common.ReportRepo, configRepo config.ConfigRepo, siteInfoService *siteinfo_common.SiteInfoCommonService, + serviceConfig *service_config.ServiceConfig, + data *data.Data, ) *DashboardService { return &DashboardService{ @@ -57,7 +64,9 @@ func NewDashboardService( reportRepo: reportRepo, configRepo: configRepo, siteInfoService: siteInfoService, - data: data, + serviceConfig: serviceConfig, + + data: data, } } @@ -159,7 +168,12 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.SMTP = true } dashboardInfo.HTTPS = true - dashboardInfo.OccupyingStorageSpace = "1MB" + dirSize, err := ds.DirSize(ds.serviceConfig.UploadPath) + if err != nil { + return dashboardInfo, err + } + size := ds.formatFileSize(dirSize) + dashboardInfo.OccupyingStorageSpace = size startTime := time.Now().Unix() - schema.AppStartTime.Unix() dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) dashboardInfo.TimeZone = siteInfoInterface.TimeZone @@ -205,3 +219,32 @@ func (ds *DashboardService) GetEmailConfig() (ec *export.EmailConfig, err error) } return ec, nil } + +func (ds *DashboardService) DirSize(path string) (int64, error) { + var size int64 + err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if !info.IsDir() { + size += info.Size() + } + return err + }) + return size, err +} + +func (ds *DashboardService) formatFileSize(fileSize int64) (size string) { + if fileSize < 1024 { + //return strconv.FormatInt(fileSize, 10) + "B" + return fmt.Sprintf("%.2f B", float64(fileSize)/float64(1)) + } else if fileSize < (1024 * 1024) { + return fmt.Sprintf("%.2f KB", float64(fileSize)/float64(1024)) + } else if fileSize < (1024 * 1024 * 1024) { + return fmt.Sprintf("%.2f MB", float64(fileSize)/float64(1024*1024)) + } else if fileSize < (1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2f GB", float64(fileSize)/float64(1024*1024*1024)) + } else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2f TB", float64(fileSize)/float64(1024*1024*1024*1024)) + } else { //if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024) + return fmt.Sprintf("%.2f EB", float64(fileSize)/float64(1024*1024*1024*1024*1024)) + } + +} From 0c4eeda3835147d5230a510c5e89c4e66372e3ce Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 11 Nov 2022 11:24:36 +0800 Subject: [PATCH 154/157] feat: validation returns the original tag for the error field --- internal/base/validator/validator.go | 34 +++++++++++++++++++++++++++- internal/install/install_req.go | 6 ++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/internal/base/validator/validator.go b/internal/base/validator/validator.go index 4a0f22b1..b089ccf0 100644 --- a/internal/base/validator/validator.go +++ b/internal/base/validator/validator.go @@ -3,6 +3,7 @@ package validator import ( "errors" "reflect" + "strings" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/translator" @@ -97,9 +98,19 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error) for _, fieldError := range valErrors { errField = &ErrorField{ - Key: translator.GlobalTrans.Tr(m.Lang, fieldError.Field()), + Key: fieldError.Field(), Value: fieldError.Translate(m.Tran), } + + // get original tag name from value for set err field key. + structNamespace := fieldError.StructNamespace() + _, fieldName, found := strings.Cut(structNamespace, ".") + if found { + originalTag := getObjectTagByFieldName(value, fieldName) + if len(originalTag) > 0 { + errField.Key = originalTag + } + } return errField, myErrors.BadRequest(reason.RequestFormatError).WithMsg(fieldError.Translate(m.Tran)) } } @@ -117,3 +128,24 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error) type Checker interface { Check() (errField *ErrorField, err error) } + +func getObjectTagByFieldName(obj interface{}, fieldName string) (tag string) { + defer func() { + if err := recover(); err != nil { + log.Error(err) + } + }() + + objT := reflect.TypeOf(obj) + objT = objT.Elem() + + structField, exists := objT.FieldByName(fieldName) + if !exists { + return "" + } + tag = structField.Tag.Get("json") + if len(tag) == 0 { + return structField.Tag.Get("form") + } + return tag +} diff --git a/internal/install/install_req.go b/internal/install/install_req.go index 534a0fa6..a80476d3 100644 --- a/internal/install/install_req.go +++ b/internal/install/install_req.go @@ -77,9 +77,9 @@ type InitBaseInfoReq struct { SiteName string `validate:"required,gt=0,lte=30" json:"site_name"` SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"` ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"` - AdminName string `validate:"required,gt=4,lte=30" json:"admin_name"` - AdminPassword string `validate:"required,gte=8,lte=32" json:"admin_password"` - AdminEmail string `validate:"required,email,gt=0,lte=500" json:"admin_email"` + AdminName string `validate:"required,gt=4,lte=30" json:"name"` + AdminPassword string `validate:"required,gte=8,lte=32" json:"password"` + AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"` } func (r *InitBaseInfoReq) FormatSiteUrl() { From ffec3a0b9ccab79f4eb632a73d8c2d3d9bbb3476 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 11 Nov 2022 11:53:44 +0800 Subject: [PATCH 155/157] update dashboard cache --- docs/docs.go | 24 ++++++- docs/swagger.json | 24 ++++++- docs/swagger.yaml | 16 +++++ internal/schema/dashboard_schema.go | 2 +- internal/schema/siteinfo_schema.go | 2 + .../service/dashboard/dashboard_service.go | 69 ++++++++----------- pkg/dir/dir.go | 35 +++++++++- 7 files changed, 125 insertions(+), 47 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 5c31c2fb..dc079d1e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5342,11 +5342,17 @@ const docTemplate = `{ "schema.SiteGeneralReq": { "type": "object", "required": [ + "contact_email", "description", "name", - "short_description" + "short_description", + "site_url" ], "properties": { + "contact_email": { + "type": "string", + "maxLength": 512 + }, "description": { "type": "string", "maxLength": 2000 @@ -5358,17 +5364,27 @@ const docTemplate = `{ "short_description": { "type": "string", "maxLength": 255 + }, + "site_url": { + "type": "string", + "maxLength": 512 } } }, "schema.SiteGeneralResp": { "type": "object", "required": [ + "contact_email", "description", "name", - "short_description" + "short_description", + "site_url" ], "properties": { + "contact_email": { + "type": "string", + "maxLength": 512 + }, "description": { "type": "string", "maxLength": 2000 @@ -5380,6 +5396,10 @@ const docTemplate = `{ "short_description": { "type": "string", "maxLength": 255 + }, + "site_url": { + "type": "string", + "maxLength": 512 } } }, diff --git a/docs/swagger.json b/docs/swagger.json index f7545616..8f58a20a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5330,11 +5330,17 @@ "schema.SiteGeneralReq": { "type": "object", "required": [ + "contact_email", "description", "name", - "short_description" + "short_description", + "site_url" ], "properties": { + "contact_email": { + "type": "string", + "maxLength": 512 + }, "description": { "type": "string", "maxLength": 2000 @@ -5346,17 +5352,27 @@ "short_description": { "type": "string", "maxLength": 255 + }, + "site_url": { + "type": "string", + "maxLength": 512 } } }, "schema.SiteGeneralResp": { "type": "object", "required": [ + "contact_email", "description", "name", - "short_description" + "short_description", + "site_url" ], "properties": { + "contact_email": { + "type": "string", + "maxLength": 512 + }, "description": { "type": "string", "maxLength": 2000 @@ -5368,6 +5384,10 @@ "short_description": { "type": "string", "maxLength": 255 + }, + "site_url": { + "type": "string", + "maxLength": 512 } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8fab9053..9576a81f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -989,6 +989,9 @@ definitions: type: object schema.SiteGeneralReq: properties: + contact_email: + maxLength: 512 + type: string description: maxLength: 2000 type: string @@ -998,13 +1001,21 @@ definitions: short_description: maxLength: 255 type: string + site_url: + maxLength: 512 + type: string required: + - contact_email - description - name - short_description + - site_url type: object schema.SiteGeneralResp: properties: + contact_email: + maxLength: 512 + type: string description: maxLength: 2000 type: string @@ -1014,10 +1025,15 @@ definitions: short_description: maxLength: 255 type: string + site_url: + maxLength: 512 + type: string required: + - contact_email - description - name - short_description + - site_url type: object schema.SiteInterfaceReq: properties: diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index d08ee2b2..1ed09010 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -6,7 +6,7 @@ var AppStartTime time.Time const ( DashBoardCachekey = "answer@dashboard" - DashBoardCacheTime = 31 * time.Minute + DashBoardCacheTime = 60 * time.Minute ) type DashboardInfo struct { diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 446b986d..9a0a4839 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -5,6 +5,8 @@ type SiteGeneralReq struct { Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` + SiteUrl string `validate:"required,gt=1,lte=512,url" form:"site_url" json:"site_url"` + ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` } // SiteInterfaceReq site interface request diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index 3cef6286..af462823 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -6,8 +6,7 @@ import ( "fmt" "io/ioutil" "net/http" - "os" - "path/filepath" + "net/url" "time" "github.com/answerdev/answer/internal/base/constant" @@ -24,6 +23,7 @@ import ( "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/answerdev/answer/pkg/dir" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -71,24 +71,29 @@ func NewDashboardService( } func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.DashboardInfo, error) { - ds.SetCache(ctx) dashboardInfo := &schema.DashboardInfo{} infoStr, err := ds.data.Cache.GetString(ctx, schema.DashBoardCachekey) if err != nil { - return dashboardInfo, err + info, statisticalErr := ds.Statistical(ctx) + if statisticalErr != nil { + return dashboardInfo, err + } + setCacheErr := ds.SetCache(ctx, info) + if setCacheErr != nil { + log.Error("ds.SetCache", setCacheErr) + } + return info, err } err = json.Unmarshal([]byte(infoStr), dashboardInfo) if err != nil { return dashboardInfo, err } + startTime := time.Now().Unix() - schema.AppStartTime.Unix() + dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) return dashboardInfo, nil } -func (ds *DashboardService) SetCache(ctx context.Context) error { - info, err := ds.Statistical(ctx) - if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() - } +func (ds *DashboardService) SetCache(ctx context.Context, info *schema.DashboardInfo) error { infoStr, err := json.Marshal(info) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() @@ -167,12 +172,23 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI if emailconfig.SMTPHost != "" { dashboardInfo.SMTP = true } - dashboardInfo.HTTPS = true - dirSize, err := ds.DirSize(ds.serviceConfig.UploadPath) + siteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx) if err != nil { return dashboardInfo, err } - size := ds.formatFileSize(dirSize) + siteUrl, err := url.Parse(siteGeneral.SiteUrl) + if err != nil { + return dashboardInfo, err + } + if siteUrl.Scheme == "https" { + dashboardInfo.HTTPS = true + } + + dirSize, err := dir.DirSize(ds.serviceConfig.UploadPath) + if err != nil { + return dashboardInfo, err + } + size := dir.FormatFileSize(dirSize) dashboardInfo.OccupyingStorageSpace = size startTime := time.Now().Unix() - schema.AppStartTime.Unix() dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) @@ -219,32 +235,3 @@ func (ds *DashboardService) GetEmailConfig() (ec *export.EmailConfig, err error) } return ec, nil } - -func (ds *DashboardService) DirSize(path string) (int64, error) { - var size int64 - err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { - if !info.IsDir() { - size += info.Size() - } - return err - }) - return size, err -} - -func (ds *DashboardService) formatFileSize(fileSize int64) (size string) { - if fileSize < 1024 { - //return strconv.FormatInt(fileSize, 10) + "B" - return fmt.Sprintf("%.2f B", float64(fileSize)/float64(1)) - } else if fileSize < (1024 * 1024) { - return fmt.Sprintf("%.2f KB", float64(fileSize)/float64(1024)) - } else if fileSize < (1024 * 1024 * 1024) { - return fmt.Sprintf("%.2f MB", float64(fileSize)/float64(1024*1024)) - } else if fileSize < (1024 * 1024 * 1024 * 1024) { - return fmt.Sprintf("%.2f GB", float64(fileSize)/float64(1024*1024*1024)) - } else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) { - return fmt.Sprintf("%.2f TB", float64(fileSize)/float64(1024*1024*1024*1024)) - } else { //if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024) - return fmt.Sprintf("%.2f EB", float64(fileSize)/float64(1024*1024*1024*1024*1024)) - } - -} diff --git a/pkg/dir/dir.go b/pkg/dir/dir.go index 559f0d1c..808bef6e 100644 --- a/pkg/dir/dir.go +++ b/pkg/dir/dir.go @@ -1,6 +1,10 @@ package dir -import "os" +import ( + "fmt" + "os" + "path/filepath" +) func CreateDirIfNotExist(path string) error { return os.MkdirAll(path, os.ModePerm) @@ -15,3 +19,32 @@ func CheckFileExist(path string) bool { f, err := os.Stat(path) return err == nil && !f.IsDir() } + +func DirSize(path string) (int64, error) { + var size int64 + err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if !info.IsDir() { + size += info.Size() + } + return err + }) + return size, err +} + +func FormatFileSize(fileSize int64) (size string) { + if fileSize < 1024 { + //return strconv.FormatInt(fileSize, 10) + "B" + return fmt.Sprintf("%.2f B", float64(fileSize)/float64(1)) + } else if fileSize < (1024 * 1024) { + return fmt.Sprintf("%.2f KB", float64(fileSize)/float64(1024)) + } else if fileSize < (1024 * 1024 * 1024) { + return fmt.Sprintf("%.2f MB", float64(fileSize)/float64(1024*1024)) + } else if fileSize < (1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2f GB", float64(fileSize)/float64(1024*1024*1024)) + } else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2f TB", float64(fileSize)/float64(1024*1024*1024*1024)) + } else { //if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024) + return fmt.Sprintf("%.2f EB", float64(fileSize)/float64(1024*1024*1024*1024*1024)) + } + +} From e00a03f2dda5abeb5dfcda78e61091cc737620df Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 11 Nov 2022 12:12:09 +0800 Subject: [PATCH 156/157] feat: modify the error information about the failure to create a table --- cmd/answer/main.go | 5 ++--- i18n/en_US.yaml | 2 ++ internal/base/reason/reason.go | 1 + internal/install/install_controller.go | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/answer/main.go b/cmd/answer/main.go index 37daf478..44ee2d10 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -32,13 +32,12 @@ var ( // @in header // @name Authorization func main() { + log.SetLogger(zap.NewLogger( + log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) Execute() } func runApp() { - log.SetLogger(zap.NewLogger( - log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) - c, err := conf.ReadConfig(cli.GetConfigFilePath()) if err != nil { panic(err) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 0ae83bf3..3c114988 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -97,6 +97,8 @@ backend: database: connection_failed: other: "Database connection failed" + create_table_failed: + other: "Create table failed" install: create_config_failed: other: "Can’t create the config.yaml file." diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 74961d86..6765a883 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -40,6 +40,7 @@ const ( ReportNotFound = "error.report.not_found" ReadConfigFailed = "error.config.read_config_failed" DatabaseConnectionFailed = "error.database.connection_failed" + InstallCreateTableFailed = "error.database.create_table_failed" InstallConfigFailed = "error.install.create_config_failed" SiteInfoNotFound = "error.site_info.not_found" ) diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index 93ab5c08..d7f8ebac 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -170,7 +170,7 @@ func InitBaseInfo(ctx *gin.Context) { if err := migrations.InitDB(c.Data.Database); err != nil { log.Error("init database error: ", err.Error()) - handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) + handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), schema.ErrTypeAlert) return } From e5c2c04cb738f4e029c640781e538e6dbecbf433 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 11 Nov 2022 12:14:52 +0800 Subject: [PATCH 157/157] feat: update initialize the log location --- cmd/answer/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/answer/main.go b/cmd/answer/main.go index 44ee2d10..bf1d944f 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -32,12 +32,12 @@ var ( // @in header // @name Authorization func main() { - log.SetLogger(zap.NewLogger( - log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) Execute() } func runApp() { + log.SetLogger(zap.NewLogger( + log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) c, err := conf.ReadConfig(cli.GetConfigFilePath()) if err != nil { panic(err)
{t('name')}{t('reputation')}{t('name')}{t('reputation')} {t('email')} + {t('created_at')} + {curFilter === 'deleted' ? t('delete_at') : t('suspend_at')} {t('status')}{t('action')}{t('status')}{t('action')}
{user.status !== 'deleted' && ( {li.vote_count}