This commit is contained in:
1 parent
c3858c92ba
commit
4a4d07ddae
5 files changed
+216
-27
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user