mirror of https://gitee.com/answerdev/answer.git
feat(permalink): permalink factory method done
This commit is contained in:
parent
9ceb9e0a07
commit
ca576d9b12
|
@ -1104,6 +1104,9 @@ ui:
|
|||
msg: Contact email cannot be empty.
|
||||
validate: Contact email is not valid.
|
||||
text: Email address of key contact responsible for this site.
|
||||
permalink:
|
||||
label: Permalink
|
||||
text: Custom URL structures can improve the usability, and forward-compatibility of your links.
|
||||
interface:
|
||||
page_title: Interface
|
||||
logo:
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"copy-to-clipboard": "^3.3.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"diff": "^5.1.0",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"i18next": "^21.9.0",
|
||||
"katex": "^0.16.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"react-router-dom": "^6.4.0",
|
||||
"semver": "^7.3.8",
|
||||
"swr": "^1.3.0",
|
||||
"urlcat": "^3.0.0",
|
||||
"zustand": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -26,6 +26,7 @@ specifiers:
|
|||
customize-cra: ^1.0.0
|
||||
dayjs: ^1.11.5
|
||||
diff: ^5.1.0
|
||||
emoji-regex: ^10.2.1
|
||||
eslint: ^8.0.1
|
||||
eslint-config-airbnb: ^19.0.4
|
||||
eslint-config-airbnb-typescript: ^17.0.0
|
||||
|
@ -63,6 +64,7 @@ specifiers:
|
|||
semver: ^7.3.8
|
||||
swr: ^1.3.0
|
||||
typescript: ^4.8.3
|
||||
urlcat: ^3.0.0
|
||||
yaml-loader: ^0.8.0
|
||||
zustand: ^4.1.1
|
||||
|
||||
|
@ -75,6 +77,7 @@ dependencies:
|
|||
copy-to-clipboard: 3.3.2
|
||||
dayjs: 1.11.5
|
||||
diff: 5.1.0
|
||||
emoji-regex: 10.2.1
|
||||
i18next: 21.9.2
|
||||
katex: 0.16.2
|
||||
lodash: 4.17.21
|
||||
|
@ -91,6 +94,7 @@ dependencies:
|
|||
react-router-dom: 6.4.0_biqbaboplfbrettd7655fr4n2y
|
||||
semver: 7.3.8
|
||||
swr: 1.3.0_react@18.2.0
|
||||
urlcat: 3.0.0
|
||||
zustand: 4.1.1_react@18.2.0
|
||||
|
||||
devDependencies:
|
||||
|
@ -4903,6 +4907,10 @@ packages:
|
|||
resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
/emoji-regex/10.2.1:
|
||||
resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==}
|
||||
dev: false
|
||||
|
||||
/emoji-regex/8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
|
@ -10417,6 +10425,12 @@ packages:
|
|||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
|
||||
/urlcat/3.0.0:
|
||||
resolution: {integrity: sha512-SSXrIzInzKdWjBfm5iOrPfO6E5Nt0aFs5PTZCauxJTjJE3qhfePAWz8tjGm7dnWMYIAdPGjio51aakunyZHMXQ==}
|
||||
dependencies:
|
||||
qs: 6.11.0
|
||||
dev: false
|
||||
|
||||
/use-sync-external-store/1.2.0_react@18.2.0:
|
||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||
peerDependencies:
|
||||
|
|
|
@ -279,6 +279,7 @@ export interface AdminSettingsGeneral {
|
|||
description: string;
|
||||
site_url: string;
|
||||
contact_email: string;
|
||||
permalink: boolean;
|
||||
}
|
||||
|
||||
export interface HeadInfo {
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
const pattern = {
|
||||
emoji: emojiRegex(),
|
||||
email:
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/,
|
||||
};
|
||||
|
|
|
@ -48,6 +48,13 @@ const General: FC = () => {
|
|||
title: t('contact_email.label'),
|
||||
description: t('contact_email.text'),
|
||||
},
|
||||
permalink: {
|
||||
type: 'boolean',
|
||||
title: t('permalink.label'),
|
||||
description: t('permalink.text'),
|
||||
enum: [true, false],
|
||||
enumNames: ['/questions/123/post-title', '/questions/123'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
|
@ -63,7 +70,7 @@ const General: FC = () => {
|
|||
}
|
||||
if (
|
||||
!url ||
|
||||
/^https?:$/.test(url.protocol) === false ||
|
||||
!/^https?:$/.test(url.protocol) ||
|
||||
url.pathname !== '/' ||
|
||||
url.search !== '' ||
|
||||
url.hash !== ''
|
||||
|
@ -86,6 +93,9 @@ const General: FC = () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
permalink: {
|
||||
'ui:widget': 'select',
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState<Type.FormDataType>(
|
||||
initFormData(schema),
|
||||
|
@ -100,6 +110,7 @@ const General: FC = () => {
|
|||
short_description: formData.short_description.value,
|
||||
site_url: formData.site_url.value,
|
||||
contact_email: formData.contact_email.value,
|
||||
permalink: formData.permalink.value,
|
||||
};
|
||||
|
||||
updateGeneralSetting(reqParams)
|
||||
|
|
|
@ -28,7 +28,6 @@ const Index = () => {
|
|||
const navigate = useNavigate();
|
||||
const { qid = '', aid = '' } = useParams();
|
||||
const [urlSearch] = useSearchParams();
|
||||
|
||||
const page = Number(urlSearch.get('page') || 0);
|
||||
const order = urlSearch.get('order') || '';
|
||||
const [question, setQuestion] = useState<QuestionDetailRes | null>(null);
|
||||
|
|
|
@ -100,7 +100,7 @@ const Index: FC = () => {
|
|||
const editor = unreviewed_info?.user_info;
|
||||
const editTime = unreviewed_info?.create_at;
|
||||
if (type === 'question') {
|
||||
itemLink = pathFactory.questionLanding(info?.object_id);
|
||||
itemLink = pathFactory.questionLanding(info?.object_id, info?.title);
|
||||
itemTitle = info?.title;
|
||||
editBadge = t('question_edit');
|
||||
editSummary ||= t('edit_question');
|
||||
|
@ -108,6 +108,7 @@ const Index: FC = () => {
|
|||
itemLink = pathFactory.answerLanding(
|
||||
// @ts-ignore
|
||||
unreviewed_info.content.question_id,
|
||||
info.title,
|
||||
unreviewed_info.object_id,
|
||||
);
|
||||
itemTitle = info?.title;
|
||||
|
|
|
@ -1,22 +1,41 @@
|
|||
import urlcat from 'urlcat';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import Pattern from '@/common/pattern';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
|
||||
const tagLanding = (tag: Type.Tag) => {
|
||||
let slugName = tag.main_tag_slug_name || tag.slug_name || '';
|
||||
slugName = slugName.toLowerCase();
|
||||
return `/tags/${encodeURIComponent(slugName)}`;
|
||||
return urlcat('/tags/:slugName', { slugName });
|
||||
};
|
||||
const tagInfo = (slugName: string) => {
|
||||
slugName = slugName.toLowerCase();
|
||||
return `/tags/${encodeURIComponent(slugName)}/info`;
|
||||
return urlcat('/tags/:slugName/info', { slugName });
|
||||
};
|
||||
const tagEdit = (tagId: string) => {
|
||||
return `/tags/${tagId}/edit`;
|
||||
return urlcat('/tags/:tagId/edit', { tagId });
|
||||
};
|
||||
const questionLanding = (question_id: string) => {
|
||||
return `/questions/${question_id}`;
|
||||
const questionLanding = (questionId: string, title: string = '') => {
|
||||
const { siteInfo } = siteInfoStore.getState();
|
||||
if (siteInfo.permalink) {
|
||||
title = title.toLowerCase();
|
||||
title = title.trim().replace(/\s+/g, '-');
|
||||
title = title.replace(Pattern.emoji, '');
|
||||
return urlcat('/questions/:questionId/:title', { questionId, title });
|
||||
}
|
||||
|
||||
return urlcat('/questions/:questionId', { questionId });
|
||||
};
|
||||
const answerLanding = (question_id: string, answer_id: string) => {
|
||||
return `/questions/${question_id}/${answer_id}`;
|
||||
const answerLanding = (
|
||||
questionId: string,
|
||||
questionTitle: string = '',
|
||||
answerId: string,
|
||||
) => {
|
||||
const questionLandingUrl = questionLanding(questionId, questionTitle);
|
||||
return urlcat(`${questionLandingUrl}/:answerId`, {
|
||||
answerId,
|
||||
});
|
||||
};
|
||||
|
||||
export const pathFactory = {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { LoaderFunctionArgs, RouteObject } from 'react-router-dom';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { guard } from '@/utils';
|
||||
import type { TGuardResult } from '@/utils/guard';
|
||||
|
||||
|
@ -15,7 +16,7 @@ export interface RouteNode extends RouteObject {
|
|||
*/
|
||||
guard?: (args: LoaderFunctionArgs) => Promise<TGuardResult>;
|
||||
}
|
||||
|
||||
const { siteInfo } = siteInfoStore.getState();
|
||||
const routes: RouteNode[] = [
|
||||
{
|
||||
path: '/',
|
||||
|
@ -34,11 +35,13 @@ const routes: RouteNode[] = [
|
|||
page: 'pages/Questions',
|
||||
},
|
||||
{
|
||||
path: 'questions/:qid',
|
||||
path: siteInfo.permalink ? 'questions/:qid/:title' : 'questions/:qid',
|
||||
page: 'pages/Questions/Detail',
|
||||
},
|
||||
{
|
||||
path: 'questions/:qid/:aid',
|
||||
path: siteInfo.permalink
|
||||
? 'questions/:qid/:title/:aid'
|
||||
: 'questions/:qid/:aid',
|
||||
page: 'pages/Questions/Detail',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -14,11 +14,13 @@ const siteInfo = create<SiteInfoType>((set) => ({
|
|||
short_description: '',
|
||||
site_url: '',
|
||||
contact_email: '',
|
||||
permalink: true,
|
||||
},
|
||||
update: (params) =>
|
||||
set(() => {
|
||||
set((_) => {
|
||||
const o = { ..._.siteInfo, ...params };
|
||||
return {
|
||||
siteInfo: params,
|
||||
siteInfo: o,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
|
Loading…
Reference in New Issue