mirror of https://github.com/halo-dev/halo
feat: add supports for automatic slug generation (halo-dev/console#831)
#### What type of PR is this? /kind feature /milestone 2.2.x #### What this PR does / why we need it: 文章、独立页面、分类、标签支持自动生成别名。 策略: 1. 仅在创建时会自动根据标题或者名称自动生成别名,编辑时如果需要重新生成,可以点击输入框右侧的按钮。 2. 中文会被转为拼音并用 - 隔开,需要注意多音字的情况,目前无法保证多音字是否符合预期。 3. 使用了 https://www.npmjs.com/package/transliteration 库。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/3171 #### Screenshots: <img width="782" alt="image" src="https://user-images.githubusercontent.com/21301288/213849446-18d70974-7b2c-420c-bd50-93d2c3193895.png"> #### Special notes for your reviewer: 测试方式: 1. 需要 `pnpm install` 4. 测试文章、独立页面、分类、标签创建时,是否可以自动设置别名。 5. 测试别名输入框右侧的生成别名按钮是否正常工作。 6. 测试分类和标签选择器创建新分类或标签时,是否正确设置了别名。 #### Does this PR introduce a user-facing change? ```release-note Console 端的文章、独立页面、分类、标签支持自动生成别名 ```pull/3445/head
parent
82db7d0afd
commit
241ad3cc2f
|
@ -74,6 +74,7 @@
|
||||||
"pinia": "^2.0.26",
|
"pinia": "^2.0.26",
|
||||||
"pretty-bytes": "^6.0.0",
|
"pretty-bytes": "^6.0.0",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
|
"transliteration": "^2.3.5",
|
||||||
"vue": "^3.2.45",
|
"vue": "^3.2.45",
|
||||||
"vue-grid-layout": "3.0.0-beta1",
|
"vue-grid-layout": "3.0.0-beta1",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.1.6",
|
||||||
|
|
|
@ -85,6 +85,7 @@ importers:
|
||||||
tailwindcss: ^3.2.4
|
tailwindcss: ^3.2.4
|
||||||
tailwindcss-safe-area: ^0.2.2
|
tailwindcss-safe-area: ^0.2.2
|
||||||
tailwindcss-themer: ^2.0.2
|
tailwindcss-themer: ^2.0.2
|
||||||
|
transliteration: ^2.3.5
|
||||||
typescript: ~4.7.4
|
typescript: ~4.7.4
|
||||||
unplugin-icons: ^0.14.14
|
unplugin-icons: ^0.14.14
|
||||||
vite: ^4.0.4
|
vite: ^4.0.4
|
||||||
|
@ -141,6 +142,7 @@ importers:
|
||||||
pinia: 2.0.26_e7lp6ggkpgyi5vqd44m2kxvk6i
|
pinia: 2.0.26_e7lp6ggkpgyi5vqd44m2kxvk6i
|
||||||
pretty-bytes: 6.0.0
|
pretty-bytes: 6.0.0
|
||||||
qs: 6.11.0
|
qs: 6.11.0
|
||||||
|
transliteration: 2.3.5
|
||||||
vue: 3.2.45
|
vue: 3.2.45
|
||||||
vue-grid-layout: 3.0.0-beta1
|
vue-grid-layout: 3.0.0-beta1
|
||||||
vue-router: 4.1.6_vue@3.2.45
|
vue-router: 4.1.6_vue@3.2.45
|
||||||
|
@ -4267,7 +4269,6 @@ packages:
|
||||||
/ansi-regex/5.0.1:
|
/ansi-regex/5.0.1:
|
||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ansi-regex/6.0.1:
|
/ansi-regex/6.0.1:
|
||||||
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
|
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
|
||||||
|
@ -4286,7 +4287,6 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ansi-styles/6.2.1:
|
/ansi-styles/6.2.1:
|
||||||
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||||
|
@ -4783,7 +4783,6 @@ packages:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
wrap-ansi: 7.0.0
|
wrap-ansi: 7.0.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/clone/1.0.4:
|
/clone/1.0.4:
|
||||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||||
|
@ -4817,7 +4816,6 @@ packages:
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
dev: true
|
|
||||||
|
|
||||||
/color-name/1.1.3:
|
/color-name/1.1.3:
|
||||||
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||||
|
@ -5358,7 +5356,6 @@ packages:
|
||||||
|
|
||||||
/emoji-regex/8.0.0:
|
/emoji-regex/8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/emoji-regex/9.2.2:
|
/emoji-regex/9.2.2:
|
||||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
|
@ -5693,7 +5690,6 @@ packages:
|
||||||
/escalade/3.1.1:
|
/escalade/3.1.1:
|
||||||
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/escape-html/1.0.3:
|
/escape-html/1.0.3:
|
||||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
@ -6296,7 +6292,6 @@ packages:
|
||||||
/get-caller-file/2.0.5:
|
/get-caller-file/2.0.5:
|
||||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
engines: {node: 6.* || 8.* || >= 10.*}
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/get-func-name/2.0.0:
|
/get-func-name/2.0.0:
|
||||||
resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==}
|
resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==}
|
||||||
|
@ -6802,7 +6797,6 @@ packages:
|
||||||
/is-fullwidth-code-point/3.0.0:
|
/is-fullwidth-code-point/3.0.0:
|
||||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/is-fullwidth-code-point/4.0.0:
|
/is-fullwidth-code-point/4.0.0:
|
||||||
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
|
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
|
||||||
|
@ -8447,7 +8441,6 @@ packages:
|
||||||
/require-directory/2.1.1:
|
/require-directory/2.1.1:
|
||||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/require-from-string/2.0.2:
|
/require-from-string/2.0.2:
|
||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
|
@ -8894,7 +8887,6 @@ packages:
|
||||||
emoji-regex: 8.0.0
|
emoji-regex: 8.0.0
|
||||||
is-fullwidth-code-point: 3.0.0
|
is-fullwidth-code-point: 3.0.0
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/string-width/5.1.2:
|
/string-width/5.1.2:
|
||||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
|
@ -8954,7 +8946,6 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/strip-ansi/7.0.1:
|
/strip-ansi/7.0.1:
|
||||||
resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==}
|
resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==}
|
||||||
|
@ -9264,6 +9255,14 @@ packages:
|
||||||
punycode: 2.1.1
|
punycode: 2.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/transliteration/2.3.5:
|
||||||
|
resolution: {integrity: sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
yargs: 17.5.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/trim-newlines/3.0.1:
|
/trim-newlines/3.0.1:
|
||||||
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -10267,7 +10266,6 @@ packages:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/wrappy/1.0.2:
|
/wrappy/1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
@ -10306,7 +10304,6 @@ packages:
|
||||||
/y18n/5.0.8:
|
/y18n/5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/yallist/2.1.2:
|
/yallist/2.1.2:
|
||||||
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
|
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
|
||||||
|
@ -10345,7 +10342,6 @@ packages:
|
||||||
/yargs-parser/21.1.1:
|
/yargs-parser/21.1.1:
|
||||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/yargs/15.4.1:
|
/yargs/15.4.1:
|
||||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||||
|
@ -10388,7 +10384,6 @@ packages:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
y18n: 5.0.8
|
y18n: 5.0.8
|
||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/yauzl/2.10.0:
|
/yauzl/2.10.0:
|
||||||
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { slugify } from "transliteration";
|
||||||
|
import { watch, type Ref } from "vue";
|
||||||
|
export default function useSlugify(
|
||||||
|
source: Ref<string>,
|
||||||
|
target: Ref<string>,
|
||||||
|
auto: Ref<boolean>
|
||||||
|
) {
|
||||||
|
watch(
|
||||||
|
() => source.value,
|
||||||
|
() => {
|
||||||
|
if (auto.value) {
|
||||||
|
handleGenerateSlug();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGenerateSlug = () => {
|
||||||
|
target.value = slugify(source.value, {
|
||||||
|
trim: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleGenerateSlug,
|
||||||
|
};
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import CategoryTag from "./components/CategoryTag.vue";
|
||||||
import SearchResultListItem from "./components/SearchResultListItem.vue";
|
import SearchResultListItem from "./components/SearchResultListItem.vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
|
import { slugify } from "transliteration";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
@ -212,7 +213,7 @@ const handleCreateCategory = async () => {
|
||||||
category: {
|
category: {
|
||||||
spec: {
|
spec: {
|
||||||
displayName: text.value,
|
displayName: text.value,
|
||||||
slug: text.value,
|
slug: slugify(text.value, { trim: true }),
|
||||||
description: "",
|
description: "",
|
||||||
cover: "",
|
cover: "",
|
||||||
template: "",
|
template: "",
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import { onClickOutside } from "@vueuse/core";
|
import { onClickOutside } from "@vueuse/core";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
|
import { slugify } from "transliteration";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
@ -196,7 +197,7 @@ const handleCreateTag = async () => {
|
||||||
tag: {
|
tag: {
|
||||||
spec: {
|
spec: {
|
||||||
displayName: text.value,
|
displayName: text.value,
|
||||||
slug: text.value,
|
slug: slugify(text.value, { trim: true }),
|
||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
cover: "",
|
cover: "",
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
import {
|
||||||
|
IconRefreshLine,
|
||||||
|
Toast,
|
||||||
|
VButton,
|
||||||
|
VModal,
|
||||||
|
VSpace,
|
||||||
|
} from "@halo-dev/components";
|
||||||
import { computed, nextTick, ref, watchEffect } from "vue";
|
import { computed, nextTick, ref, watchEffect } from "vue";
|
||||||
import type { SinglePage } from "@halo-dev/api-client";
|
import type { SinglePage } from "@halo-dev/api-client";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
@ -10,6 +16,7 @@ import { randomUUID } from "@/utils/id";
|
||||||
import { toDatetimeLocal, toISOString } from "@/utils/date";
|
import { toDatetimeLocal, toISOString } from "@/utils/date";
|
||||||
import { submitForm } from "@formkit/core";
|
import { submitForm } from "@formkit/core";
|
||||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||||
|
import useSlugify from "@/composables/use-slugify";
|
||||||
|
|
||||||
const initialFormState: SinglePage = {
|
const initialFormState: SinglePage = {
|
||||||
spec: {
|
spec: {
|
||||||
|
@ -258,6 +265,20 @@ const publishTime = computed(() => {
|
||||||
const onPublishTimeChange = (value: string) => {
|
const onPublishTimeChange = (value: string) => {
|
||||||
formState.value.spec.publishTime = value ? toISOString(value) : undefined;
|
formState.value.spec.publishTime = value ? toISOString(value) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// slug
|
||||||
|
const { handleGenerateSlug } = useSlugify(
|
||||||
|
computed(() => formState.value.spec.title),
|
||||||
|
computed({
|
||||||
|
get() {
|
||||||
|
return formState.value.spec.slug;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
formState.value.spec.slug = value;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
computed(() => !isUpdateMode.value)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -302,7 +323,20 @@ const onPublishTimeChange = (value: string) => {
|
||||||
name="slug"
|
name="slug"
|
||||||
type="text"
|
type="text"
|
||||||
validation="required|length:0,100"
|
validation="required|length:0,100"
|
||||||
></FormKit>
|
help="通常用于生成页面的固定链接"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<div
|
||||||
|
v-tooltip="'根据标题重新生成别名'"
|
||||||
|
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
|
||||||
|
@click="handleGenerateSlug"
|
||||||
|
>
|
||||||
|
<IconRefreshLine
|
||||||
|
class="h-4 w-4 text-gray-500 group-hover:text-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.excerpt.autoGenerate"
|
v-model="formState.spec.excerpt.autoGenerate"
|
||||||
:options="[
|
:options="[
|
||||||
|
|
|
@ -4,7 +4,13 @@ import { computed, nextTick, ref, watch } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
import {
|
||||||
|
IconRefreshLine,
|
||||||
|
Toast,
|
||||||
|
VButton,
|
||||||
|
VModal,
|
||||||
|
VSpace,
|
||||||
|
} from "@halo-dev/components";
|
||||||
import SubmitButton from "@/components/button/SubmitButton.vue";
|
import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
|
@ -16,6 +22,7 @@ import { reset } from "@formkit/core";
|
||||||
import { setFocus } from "@/formkit/utils/focus";
|
import { setFocus } from "@/formkit/utils/focus";
|
||||||
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
||||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||||
|
import useSlugify from "@/composables/use-slugify";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -138,6 +145,20 @@ watch(
|
||||||
|
|
||||||
// custom templates
|
// custom templates
|
||||||
const { templates } = useThemeCustomTemplates("category");
|
const { templates } = useThemeCustomTemplates("category");
|
||||||
|
|
||||||
|
// slug
|
||||||
|
const { handleGenerateSlug } = useSlugify(
|
||||||
|
computed(() => formState.value.spec.displayName),
|
||||||
|
computed({
|
||||||
|
get() {
|
||||||
|
return formState.value.spec.slug;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
formState.value.spec.slug = value;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
computed(() => !isUpdateMode.value)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
|
@ -171,12 +192,24 @@ const { templates } = useThemeCustomTemplates("category");
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.slug"
|
v-model="formState.spec.slug"
|
||||||
help="通常作为分类访问地址标识"
|
help="通常用于生成分类的固定链接"
|
||||||
name="slug"
|
name="slug"
|
||||||
label="别名"
|
label="别名"
|
||||||
type="text"
|
type="text"
|
||||||
validation="required|length:0,50"
|
validation="required|length:0,50"
|
||||||
></FormKit>
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<div
|
||||||
|
v-tooltip="'根据名称重新生成别名'"
|
||||||
|
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
|
||||||
|
@click="handleGenerateSlug"
|
||||||
|
>
|
||||||
|
<IconRefreshLine
|
||||||
|
class="h-4 w-4 text-gray-500 group-hover:text-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.template"
|
v-model="formState.spec.template"
|
||||||
:options="templates"
|
:options="templates"
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
import {
|
||||||
|
IconRefreshLine,
|
||||||
|
Toast,
|
||||||
|
VButton,
|
||||||
|
VModal,
|
||||||
|
VSpace,
|
||||||
|
} from "@halo-dev/components";
|
||||||
import { computed, nextTick, ref, watchEffect } from "vue";
|
import { computed, nextTick, ref, watchEffect } from "vue";
|
||||||
import type { Post } from "@halo-dev/api-client";
|
import type { Post } from "@halo-dev/api-client";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
@ -10,6 +16,7 @@ import { randomUUID } from "@/utils/id";
|
||||||
import { toDatetimeLocal, toISOString } from "@/utils/date";
|
import { toDatetimeLocal, toISOString } from "@/utils/date";
|
||||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||||
import { submitForm } from "@formkit/core";
|
import { submitForm } from "@formkit/core";
|
||||||
|
import useSlugify from "@/composables/use-slugify";
|
||||||
|
|
||||||
const initialFormState: Post = {
|
const initialFormState: Post = {
|
||||||
spec: {
|
spec: {
|
||||||
|
@ -222,6 +229,20 @@ const onPublishTimeChange = (value: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
||||||
|
|
||||||
|
// slug
|
||||||
|
const { handleGenerateSlug } = useSlugify(
|
||||||
|
computed(() => formState.value.spec.title),
|
||||||
|
computed({
|
||||||
|
get() {
|
||||||
|
return formState.value.spec.slug;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
formState.value.spec.slug = value;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
computed(() => !isUpdateMode.value)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
|
@ -265,7 +286,20 @@ const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
||||||
name="slug"
|
name="slug"
|
||||||
type="text"
|
type="text"
|
||||||
validation="required|length:0,100"
|
validation="required|length:0,100"
|
||||||
></FormKit>
|
help="通常用于生成文章的固定链接"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<div
|
||||||
|
v-tooltip="'根据标题重新生成别名'"
|
||||||
|
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
|
||||||
|
@click="handleGenerateSlug"
|
||||||
|
>
|
||||||
|
<IconRefreshLine
|
||||||
|
class="h-4 w-4 text-gray-500 group-hover:text-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.categories"
|
v-model="formState.spec.categories"
|
||||||
label="分类目录"
|
label="分类目录"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { apiClient } from "@/utils/api-client";
|
||||||
import {
|
import {
|
||||||
IconArrowLeft,
|
IconArrowLeft,
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
|
IconRefreshLine,
|
||||||
Toast,
|
Toast,
|
||||||
VButton,
|
VButton,
|
||||||
VModal,
|
VModal,
|
||||||
|
@ -22,6 +23,7 @@ import cloneDeep from "lodash.clonedeep";
|
||||||
import { reset } from "@formkit/core";
|
import { reset } from "@formkit/core";
|
||||||
import { setFocus } from "@/formkit/utils/focus";
|
import { setFocus } from "@/formkit/utils/focus";
|
||||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||||
|
import useSlugify from "@/composables/use-slugify";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -139,6 +141,20 @@ watch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// slug
|
||||||
|
const { handleGenerateSlug } = useSlugify(
|
||||||
|
computed(() => formState.value.spec.displayName),
|
||||||
|
computed({
|
||||||
|
get() {
|
||||||
|
return formState.value.spec.slug;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
formState.value.spec.slug = value;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
computed(() => !isUpdateMode.value)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
|
@ -181,12 +197,24 @@ watch(
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.slug"
|
v-model="formState.spec.slug"
|
||||||
help="通常作为标签访问地址标识"
|
help="通常用于生成标签的固定链接"
|
||||||
label="别名"
|
label="别名"
|
||||||
name="slug"
|
name="slug"
|
||||||
type="text"
|
type="text"
|
||||||
validation="required|length:0,50"
|
validation="required|length:0,50"
|
||||||
></FormKit>
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<div
|
||||||
|
v-tooltip="'根据名称重新生成别名'"
|
||||||
|
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
|
||||||
|
@click="handleGenerateSlug"
|
||||||
|
>
|
||||||
|
<IconRefreshLine
|
||||||
|
class="h-4 w-4 text-gray-500 group-hover:text-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.color"
|
v-model="formState.spec.color"
|
||||||
name="color"
|
name="color"
|
||||||
|
|
Loading…
Reference in New Issue