From e13563bad0f31ee9726ec44e10a93cde28f22713 Mon Sep 17 00:00:00 2001
From: Hilary Liu <2788370451@qq.com>
Date: Sun, 10 Sep 2023 22:08:13 +0800
Subject: [PATCH] feat: add strategy setting for post slug generation (#4551)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#### What type of PR is this?
/kind improvement
#### What this PR does / why we need it:
添加文章别名自动生成策略
#### Which issue(s) this PR fixes:
Fixes https://github.com/halo-dev/halo/issues/1790
#### Special notes for your reviewer:
需要后端提供支持在globalInfo里面添加`gSlugMode`字段。它的类型为(后续可能会支持更多的模式)
#### Does this PR introduce a user-facing change?
```release-note
文章支持多别名生成策略。
```
---
.../run/halo/app/infra/SystemSetting.java | 1 +
.../halo/app/actuator/GlobalInfoEndpoint.java | 10 ++++
.../system-configurable-configmap.yaml | 5 +-
.../resources/extensions/system-setting.yaml | 13 +++++
console/package.json | 1 +
console/pnpm-lock.yaml | 23 +++++++++
console/src/composables/use-slugify.ts | 51 ++++++++++++++++---
.../components/SinglePageSettingModal.vue | 6 ++-
.../components/CategoryEditingModal.vue | 6 ++-
.../posts/components/PostSettingModal.vue | 6 ++-
.../posts/tags/components/TagEditingModal.vue | 6 ++-
.../modules/system/settings/tabs/Setting.vue | 3 +-
console/src/setup/setupModules.ts | 1 -
console/src/types/actuator.ts | 3 ++
console/src/types/slug.ts | 7 +++
15 files changed, 123 insertions(+), 19 deletions(-)
create mode 100644 console/src/types/slug.ts
diff --git a/api/src/main/java/run/halo/app/infra/SystemSetting.java b/api/src/main/java/run/halo/app/infra/SystemSetting.java
index 27f8488207..14b3cd51ae 100644
--- a/api/src/main/java/run/halo/app/infra/SystemSetting.java
+++ b/api/src/main/java/run/halo/app/infra/SystemSetting.java
@@ -79,6 +79,7 @@ public static class Post {
Integer categoryPageSize;
Integer tagPageSize;
Boolean review;
+ String slugGenerationStrategy;
}
@Data
diff --git a/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java b/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java
index ab09f3d88a..667a5e7a11 100644
--- a/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java
+++ b/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java
@@ -53,6 +53,7 @@ public GlobalInfo globalInfo() {
handleCommentSetting(info, configMap);
handleUserSetting(info, configMap);
handleBasicSetting(info, configMap);
+ handlePostSlugGenerationStrategy(info, configMap);
}));
return info;
@@ -81,6 +82,8 @@ public static class GlobalInfo {
private boolean dataInitialized;
+ private String postSlugGenerationStrategy;
+
private List socialAuthProviders;
}
@@ -123,6 +126,13 @@ private void handleUserSetting(GlobalInfo info, ConfigMap configMap) {
}
}
+ private void handlePostSlugGenerationStrategy(GlobalInfo info, ConfigMap configMap) {
+ var post = SystemSetting.get(configMap, SystemSetting.Post.GROUP, SystemSetting.Post.class);
+ if (post != null) {
+ info.setPostSlugGenerationStrategy(post.getSlugGenerationStrategy());
+ }
+ }
+
private void handleBasicSetting(GlobalInfo info, ConfigMap configMap) {
var basic = SystemSetting.get(configMap, Basic.GROUP, Basic.class);
if (basic != null) {
diff --git a/application/src/main/resources/extensions/system-configurable-configmap.yaml b/application/src/main/resources/extensions/system-configurable-configmap.yaml
index 045948f895..808329854a 100644
--- a/application/src/main/resources/extensions/system-configurable-configmap.yaml
+++ b/application/src/main/resources/extensions/system-configurable-configmap.yaml
@@ -31,7 +31,8 @@ data:
"postPageSize": 10,
"archivePageSize": 10,
"categoryPageSize": 10,
- "tagPageSize": 10
+ "tagPageSize": 10,
+ "slugGenerationStrategy": "generateByTitle"
}
comment: |
{
@@ -42,4 +43,4 @@ data:
menu: |
{
"primary": "primary"
- }
\ No newline at end of file
+ }
diff --git a/application/src/main/resources/extensions/system-setting.yaml b/application/src/main/resources/extensions/system-setting.yaml
index f4cd28bb19..b7fee0474f 100644
--- a/application/src/main/resources/extensions/system-setting.yaml
+++ b/application/src/main/resources/extensions/system-setting.yaml
@@ -55,6 +55,19 @@ spec:
min: 1
max: 100
validation: required
+ - $formkit: select
+ label: "别名生成策略"
+ name: slugGenerationStrategy
+ value: 'generateByTitle'
+ options:
+ - label: '根据标题'
+ value: 'generateByTitle'
+ - label: '时间戳'
+ value: 'timestamp'
+ - label: 'Short UUID'
+ value: 'shortUUID'
+ - label: 'UUID'
+ value: 'UUID'
- group: seo
label: SEO 设置
formSchema:
diff --git a/console/package.json b/console/package.json
index 2c1a03280a..ef3f951b39 100644
--- a/console/package.json
+++ b/console/package.json
@@ -92,6 +92,7 @@
"pinia": "^2.1.6",
"pretty-bytes": "^6.0.0",
"qs": "^6.11.1",
+ "short-unique-id": "^5.0.2",
"transliteration": "^2.3.5",
"vue": "^3.3.4",
"vue-demi": "^0.14.5",
diff --git a/console/pnpm-lock.yaml b/console/pnpm-lock.yaml
index 36517a6e24..a1218c4585 100644
--- a/console/pnpm-lock.yaml
+++ b/console/pnpm-lock.yaml
@@ -182,6 +182,9 @@ importers:
qs:
specifier: ^6.11.1
version: 6.11.1
+ short-unique-id:
+ specifier: ^5.0.2
+ version: 5.0.2
transliteration:
specifier: ^2.3.5
version: 2.3.5
@@ -782,6 +785,7 @@ packages:
/@babel/plugin-proposal-async-generator-functions@7.19.1(@babel/core@7.20.12):
resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -797,6 +801,7 @@ packages:
/@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -810,6 +815,7 @@ packages:
/@babel/plugin-proposal-class-static-block@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.
peerDependencies:
'@babel/core': ^7.12.0
dependencies:
@@ -824,6 +830,7 @@ packages:
/@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -835,6 +842,7 @@ packages:
/@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.20.12):
resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -846,6 +854,7 @@ packages:
/@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -857,6 +866,7 @@ packages:
/@babel/plugin-proposal-logical-assignment-operators@7.18.9(@babel/core@7.20.12):
resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -868,6 +878,7 @@ packages:
/@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -879,6 +890,7 @@ packages:
/@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -890,6 +902,7 @@ packages:
/@babel/plugin-proposal-object-rest-spread@7.18.9(@babel/core@7.20.12):
resolution: {integrity: sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -904,6 +917,7 @@ packages:
/@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -915,6 +929,7 @@ packages:
/@babel/plugin-proposal-optional-chaining@7.18.9(@babel/core@7.20.12):
resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -927,6 +942,7 @@ packages:
/@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -940,6 +956,7 @@ packages:
/@babel/plugin-proposal-private-property-in-object@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==}
engines: {node: '>=6.9.0'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -955,6 +972,7 @@ packages:
/@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.20.12):
resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==}
engines: {node: '>=4'}
+ deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
@@ -10168,6 +10186,11 @@ packages:
vscode-textmate: 5.2.0
dev: true
+ /short-unique-id@5.0.2:
+ resolution: {integrity: sha512-4wZq1VLV4hsEx8guP5bN7XnY8UDsVXtdUDWFMP1gvEieAXolq5fWGKpuua21PRXaLn3OybTKFQNm7JGcHSWu/Q==}
+ hasBin: true
+ dev: false
+
/side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
diff --git a/console/src/composables/use-slugify.ts b/console/src/composables/use-slugify.ts
index fb727da1e2..d77a4b8d35 100644
--- a/console/src/composables/use-slugify.ts
+++ b/console/src/composables/use-slugify.ts
@@ -1,26 +1,63 @@
import { slugify } from "transliteration";
import { watch, type Ref } from "vue";
+import ShortUniqueId from "short-unique-id";
+import { FormType } from "@/types/slug";
+import { useGlobalInfoStore } from "@/stores/global-info";
+import { randomUUID } from "@/utils/id";
+const uid = new ShortUniqueId();
+const Strategy = {
+ generateByTitle: (value: string) => {
+ if (!value) return "";
+ return slugify(value, { trim: true });
+ },
+ shortUUID: (value: string) => {
+ if (!value) return "";
+ return uid.randomUUID(8);
+ },
+ UUID: (value: string) => {
+ if (!value) return "";
+ return randomUUID();
+ },
+ timestamp: (value: string) => {
+ if (!value) return "";
+ return new Date().getTime().toString();
+ },
+};
+
+const onceList = ["shortUUID", "UUID", "timestamp"];
+
export default function useSlugify(
source: Ref,
target: Ref,
- auto: Ref
+ auto: Ref,
+ formType: FormType
) {
watch(
() => source.value,
() => {
if (auto.value) {
- handleGenerateSlug();
+ handleGenerateSlug(false, formType);
}
}
);
- const handleGenerateSlug = () => {
- if (!source.value) {
+ const handleGenerateSlug = (forceUpdate = false, formType: FormType) => {
+ const globalInfoStore = useGlobalInfoStore();
+ const mode = globalInfoStore.globalInfo?.postSlugGenerationStrategy;
+
+ if (!mode) {
+ return;
+ }
+ if (formType != FormType.POST) {
+ target.value = Strategy["generateByTitle"](source.value);
+ return;
+ }
+ if (forceUpdate) {
+ target.value = Strategy[mode](source.value);
return;
}
- target.value = slugify(source.value, {
- trim: true,
- });
+ if (onceList.includes(mode) && target.value) return;
+ target.value = Strategy[mode](source.value);
};
return {
diff --git a/console/src/modules/contents/pages/components/SinglePageSettingModal.vue b/console/src/modules/contents/pages/components/SinglePageSettingModal.vue
index 354ee0ee6e..e742bfddf8 100644
--- a/console/src/modules/contents/pages/components/SinglePageSettingModal.vue
+++ b/console/src/modules/contents/pages/components/SinglePageSettingModal.vue
@@ -19,6 +19,7 @@ import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
import useSlugify from "@/composables/use-slugify";
import { useI18n } from "vue-i18n";
import { usePageUpdateMutate } from "../composables/use-page-update-mutate";
+import { FormType } from "@/types/slug";
const initialFormState: SinglePage = {
spec: {
@@ -277,7 +278,8 @@ const { handleGenerateSlug } = useSlugify(
formState.value.spec.slug = value;
},
}),
- computed(() => !isUpdateMode.value)
+ computed(() => !isUpdateMode.value),
+ FormType.SINGLE_PAGE
);
@@ -331,7 +333,7 @@ const { handleGenerateSlug } = useSlugify(
$t('core.page.settings.fields.slug.refresh_message')
"
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
- @click="handleGenerateSlug"
+ @click="handleGenerateSlug(true, FormType.SINGLE_PAGE)"
>
!isUpdateMode.value)
+ computed(() => !isUpdateMode.value),
+ FormType.CATEGORY
);
@@ -260,7 +262,7 @@ const { handleGenerateSlug } = useSlugify(
)
"
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
- @click="handleGenerateSlug"
+ @click="handleGenerateSlug(true, FormType.CATEGORY)"
>
!isUpdateMode.value)
+ computed(() => !isUpdateMode.value),
+ FormType.POST
);
@@ -293,7 +295,7 @@ const { handleGenerateSlug } = useSlugify(
$t('core.post.settings.fields.slug.refresh_message')
"
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
- @click="handleGenerateSlug"
+ @click="handleGenerateSlug(true, FormType.POST)"
>
!isUpdateMode.value)
+ computed(() => !isUpdateMode.value),
+ FormType.TAG
);
@@ -220,7 +222,7 @@ const { handleGenerateSlug } = useSlugify(
)
"
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
- @click="handleGenerateSlug"
+ @click="handleGenerateSlug(true, FormType.TAG)"
>
{
Toast.success(t("core.common.toast.save_success"));
queryClient.invalidateQueries({ queryKey: ["system-configMap"] });
-
+ await useGlobalInfoStore().fetchGlobalInfo();
systemConfigMapStore.configMap = data;
saving.value = false;
diff --git a/console/src/setup/setupModules.ts b/console/src/setup/setupModules.ts
index c9b110d0d3..b38737815b 100644
--- a/console/src/setup/setupModules.ts
+++ b/console/src/setup/setupModules.ts
@@ -30,7 +30,6 @@ export async function setupPluginModules(app: App) {
enabledPluginNames.forEach((name) => {
const module = window[name];
-
if (module) {
registerModule(app, module, false);
pluginModuleStore.registerPluginModule(name, module);
diff --git a/console/src/types/actuator.ts b/console/src/types/actuator.ts
index 5f97987d27..bdbc21b1e7 100644
--- a/console/src/types/actuator.ts
+++ b/console/src/types/actuator.ts
@@ -1,3 +1,5 @@
+import type { ModeType } from "./slug";
+
export interface GlobalInfo {
externalUrl: string;
timeZone: string;
@@ -10,6 +12,7 @@ export interface GlobalInfo {
userInitialized: boolean;
dataInitialized: boolean;
favicon?: string;
+ postSlugGenerationStrategy: ModeType;
}
export interface Info {
diff --git a/console/src/types/slug.ts b/console/src/types/slug.ts
new file mode 100644
index 0000000000..af9bd9174d
--- /dev/null
+++ b/console/src/types/slug.ts
@@ -0,0 +1,7 @@
+export type ModeType = "UUID" | "shortUUID" | "timestamp" | "generateByTitle";
+export enum FormType {
+ TAG = "Tag",
+ CATEGORY = "Category",
+ POST = "Post",
+ SINGLE_PAGE = "SinglePage",
+}