feat: 在管理后台表单中集成 Slug 自动生成与防抖处理功能
continuous-integration/drone/push Build is failing

This commit is contained in:
seahi committed 2026-08-07 14:21:25 +08:00
1 parent c3858c92ba
commit 4a4d07ddae
5 files changed
+216 -27

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
/**
* 发送请求至后台 /api/slugify 接口生成英文 Slug
*/
export async function fetchSlug(text: string): Promise<string> {
if (!text || !text.trim()) return '';
try {
const res = await fetch('/api/slugify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text })
});
if (res.ok) {
const data = (await res.json()) as { slug?: string };
return data.slug || '';
}
} catch (err) {
console.error('获取 Slug 错误:', err);
}
return '';
}
/**
* 带有防抖功能的前端 Slug 自动同步处理器
*/
export function createSlugAutoHandler(opts: {
getSlug: () => string;
setSlug: (val: string) => void;
getIsUserModified: () => boolean;
setIsUserModified: (val: boolean) => void;
delayMs?: number;
}) {
const delay = opts.delayMs ?? 400;
let timer: ReturnType<typeof setTimeout> | null = null;
let isGenerating = false;
return {
/**
* 当名称/标题 Input 事件触发时调用
*/
onNameInput(nameValue: string) {
// 若用户已经手动更改过 slug,则不自动强行覆盖
if (opts.getIsUserModified()) return;
if (timer) clearTimeout(timer);
if (!nameValue.trim()) {
opts.setSlug('');
return;
}
timer = setTimeout(async () => {
isGenerating = true;
const generated = await fetchSlug(nameValue);
// 在异步返回后,再次确认用户期间没有手动修改 slug
if (!opts.getIsUserModified() && generated) {
opts.setSlug(generated);
}
isGenerating = false;
}, delay);
},
/**
* 当用户手动编辑 Slug 输入框时调用
*/
onSlugInput(slugValue: string) {
if (slugValue.trim()) {
opts.setIsUserModified(true);
} else {
// 如果把 slug 清空了,则恢复自动生成能力
opts.setIsUserModified(false);
}
},
/**
* 用户点击 🔄 重新生成按钮时强制生成
*/
async forceGenerate(nameValue: string) {
if (!nameValue.trim()) return;
const generated = await fetchSlug(nameValue);
if (generated) {
opts.setSlug(generated);
// 标记为重置/由系统填充
opts.setIsUserModified(false);
}
}
};
}
+34 -4
View File
@@ -1,16 +1,27 @@
<script lang="ts">
import { page } from '$app/stores';
import type { PageData, ActionData } from './$types';
import { createSlugAutoHandler } from '$lib/utils/slugify';
let { data, form }: { data: PageData; form: ActionData } = $props();
let name = $state('');
let slug = $state('');
let isSlugUserModified = $state(false);
const slugHandler = createSlugAutoHandler({
getSlug: () => slug,
setSlug: (val) => (slug = val),
getIsUserModified: () => isSlugUserModified,
setIsUserModified: (val) => (isSlugUserModified = val)
});
function handleNameInput() {
if (!slug) {
slug = name.toLowerCase().replace(/\s+/g, '-');
}
slugHandler.onNameInput(name);
}
function handleSlugInput() {
slugHandler.onSlugInput(slug);
}
</script>
@@ -43,7 +54,26 @@
</div>
<div class="form-group">
<label for="slug">别名</label>
<input type="text" id="slug" name="slug" bind:value={slug} placeholder="例如: tech" />
<div style="display: flex; gap: 0.5rem; align-items: center;">
<input
type="text"
id="slug"
name="slug"
bind:value={slug}
oninput={handleSlugInput}
placeholder="例如: tech-sharing"
style="flex: 1;"
/>
<button
type="button"
class="btn btn-secondary"
style="padding: 0.4rem 0.6rem; cursor: pointer;"
title="生成英文 Slug"
onclick={() => slugHandler.forceGenerate(name)}
>
🔄
</button>
</div>
</div>
<button type="submit" class="btn btn-primary">创建</button>
</form>
+28 -8
View File
@@ -4,21 +4,29 @@
import type { ActionData } from './$types';
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
import { createSlugAutoHandler } from '$lib/utils/slugify';
let { form }: { form: ActionData } = $props();
let title = $state('');
let slug = $state('');
let content = $state('');
let mode = $state<'edit' | 'preview'>('edit');
let isSlugUserModified = $state(false);
const slugHandler = createSlugAutoHandler({
getSlug: () => slug,
setSlug: (val) => (slug = val),
getIsUserModified: () => isSlugUserModified,
setIsUserModified: (val) => (isSlugUserModified = val)
});
// 自动从标题生成 Slug 简易逻辑
function handleTitleInput() {
if (!slug) {
slug = title
.toLowerCase()
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
.replace(/\s+/g, '-');
}
slugHandler.onNameInput(title);
}
function handleSlugInput() {
slugHandler.onSlugInput(slug);
}
</script>
@@ -92,13 +100,25 @@
</div>
<div class="sidebar-box">
<h3>别名 *</h3>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<h3 style="margin: 0;">别名 *</h3>
<button
type="button"
class="btn btn-secondary"
style="padding: 0.2rem 0.5rem; font-size: 0.8rem; cursor: pointer;"
title="生成英文 Slug"
onclick={() => slugHandler.forceGenerate(title)}
>
🔄 生成
</button>
</div>
<input
type="text"
id="slug"
name="slug"
class="sidebar-input"
bind:value={slug}
oninput={handleSlugInput}
placeholder="about"
required
/>
+29 -8
View File
@@ -11,6 +11,8 @@
import { parseHugoMarkdown } from '$lib/hugoParser';
import { untrack } from 'svelte';
import { createSlugAutoHandler } from '$lib/utils/slugify';
let { data, form }: { data: PageData; form: ActionData } = $props();
const initialData = $state.snapshot(untrack(() => data));
@@ -29,14 +31,21 @@
let tagsList = $state(initialData.tags || []);
let seriesList = $state(initialData.series || []);
// 自动从标题生成 Slug 简易逻辑
let isSlugUserModified = $state(false);
const slugHandler = createSlugAutoHandler({
getSlug: () => slug,
setSlug: (val) => (slug = val),
getIsUserModified: () => isSlugUserModified,
setIsUserModified: (val) => (isSlugUserModified = val)
});
function handleTitleInput() {
if (!slug) {
slug = title
.toLowerCase()
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
.replace(/\s+/g, '-');
}
slugHandler.onNameInput(title);
}
function handleSlugInput() {
slugHandler.onSlugInput(slug);
}
let selectedTags = $state<number[]>([]);
@@ -501,13 +510,25 @@
<FeaturedImagePicker bind:cover adaptiveHeight previewable />
<div class="sidebar-box">
<h3>别名 *</h3>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<h3 style="margin: 0;">别名 *</h3>
<button
type="button"
class="btn btn-secondary"
style="padding: 0.2rem 0.5rem; font-size: 0.8rem; cursor: pointer;"
title="生成英文 Slug"
onclick={() => slugHandler.forceGenerate(title)}
>
🔄 生成
</button>
</div>
<input
type="text"
id="slug"
name="slug"
class="sidebar-input"
bind:value={slug}
oninput={handleSlugInput}
placeholder="hello-world"
required
/>
+34 -7
View File
@@ -2,18 +2,26 @@
import { page } from '$app/stores';
import { resolve } from '$app/paths';
import type { ActionData, PageData } from './$types';
import { createSlugAutoHandler } from '$lib/utils/slugify';
let { data, form }: { data: PageData; form: ActionData } = $props();
let name = $state('');
let slug = $state('');
let isSlugUserModified = $state(false);
const slugHandler = createSlugAutoHandler({
getSlug: () => slug,
setSlug: (val) => (slug = val),
getIsUserModified: () => isSlugUserModified,
setIsUserModified: (val) => (isSlugUserModified = val)
});
function handleNameInput() {
if (!slug) {
slug = name
.toLowerCase()
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
.replace(/\s+/g, '-');
}
slugHandler.onNameInput(name);
}
function handleSlugInput() {
slugHandler.onSlugInput(slug);
}
</script>
@@ -40,7 +48,26 @@
/>
<label for="slug">URL 别名 *</label>
<input id="slug" name="slug" type="text" bind:value={slug} required />
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem;">
<input
id="slug"
name="slug"
type="text"
bind:value={slug}
oninput={handleSlugInput}
required
style="flex: 1;"
/>
<button
type="button"
class="btn btn-secondary"
style="padding: 0.4rem 0.6rem; cursor: pointer;"
title="生成英文 Slug"
onclick={() => slugHandler.forceGenerate(name)}
>
🔄
</button>
</div>
<label for="description">描述</label>
<textarea