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 );