This commit is contained in:
1 parent
c3858c92ba
commit
4a4d07ddae
5 files changed
+212
-23
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">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import type { PageData, ActionData } from './$types';
|
import type { PageData, ActionData } from './$types';
|
||||||
|
import { createSlugAutoHandler } from '$lib/utils/slugify';
|
||||||
|
|
||||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||||
|
|
||||||
let name = $state('');
|
let name = $state('');
|
||||||
let slug = $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() {
|
function handleNameInput() {
|
||||||
if (!slug) {
|
slugHandler.onNameInput(name);
|
||||||
slug = name.toLowerCase().replace(/\s+/g, '-');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSlugInput() {
|
||||||
|
slugHandler.onSlugInput(slug);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -43,7 +54,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="slug">别名</label>
|
<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>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">创建</button>
|
<button type="submit" class="btn btn-primary">创建</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,21 +4,29 @@
|
|||||||
import type { ActionData } from './$types';
|
import type { ActionData } from './$types';
|
||||||
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
||||||
|
|
||||||
|
import { createSlugAutoHandler } from '$lib/utils/slugify';
|
||||||
|
|
||||||
let { form }: { form: ActionData } = $props();
|
let { form }: { form: ActionData } = $props();
|
||||||
|
|
||||||
let title = $state('');
|
let title = $state('');
|
||||||
let slug = $state('');
|
let slug = $state('');
|
||||||
let content = $state('');
|
let content = $state('');
|
||||||
let mode = $state<'edit' | 'preview'>('edit');
|
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() {
|
function handleTitleInput() {
|
||||||
if (!slug) {
|
slugHandler.onNameInput(title);
|
||||||
slug = title
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
|
|
||||||
.replace(/\s+/g, '-');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSlugInput() {
|
||||||
|
slugHandler.onSlugInput(slug);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -92,13 +100,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-box">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="slug"
|
id="slug"
|
||||||
name="slug"
|
name="slug"
|
||||||
class="sidebar-input"
|
class="sidebar-input"
|
||||||
bind:value={slug}
|
bind:value={slug}
|
||||||
|
oninput={handleSlugInput}
|
||||||
placeholder="about"
|
placeholder="about"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
import { parseHugoMarkdown } from '$lib/hugoParser';
|
import { parseHugoMarkdown } from '$lib/hugoParser';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
|
|
||||||
|
import { createSlugAutoHandler } from '$lib/utils/slugify';
|
||||||
|
|
||||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||||
const initialData = $state.snapshot(untrack(() => data));
|
const initialData = $state.snapshot(untrack(() => data));
|
||||||
|
|
||||||
@@ -29,14 +31,21 @@
|
|||||||
let tagsList = $state(initialData.tags || []);
|
let tagsList = $state(initialData.tags || []);
|
||||||
let seriesList = $state(initialData.series || []);
|
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() {
|
function handleTitleInput() {
|
||||||
if (!slug) {
|
slugHandler.onNameInput(title);
|
||||||
slug = title
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
|
|
||||||
.replace(/\s+/g, '-');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSlugInput() {
|
||||||
|
slugHandler.onSlugInput(slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedTags = $state<number[]>([]);
|
let selectedTags = $state<number[]>([]);
|
||||||
@@ -501,13 +510,25 @@
|
|||||||
<FeaturedImagePicker bind:cover adaptiveHeight previewable />
|
<FeaturedImagePicker bind:cover adaptiveHeight previewable />
|
||||||
|
|
||||||
<div class="sidebar-box">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="slug"
|
id="slug"
|
||||||
name="slug"
|
name="slug"
|
||||||
class="sidebar-input"
|
class="sidebar-input"
|
||||||
bind:value={slug}
|
bind:value={slug}
|
||||||
|
oninput={handleSlugInput}
|
||||||
placeholder="hello-world"
|
placeholder="hello-world"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,18 +2,26 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import type { ActionData, PageData } from './$types';
|
import type { ActionData, PageData } from './$types';
|
||||||
|
import { createSlugAutoHandler } from '$lib/utils/slugify';
|
||||||
|
|
||||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||||
let name = $state('');
|
let name = $state('');
|
||||||
let slug = $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() {
|
function handleNameInput() {
|
||||||
if (!slug) {
|
slugHandler.onNameInput(name);
|
||||||
slug = name
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
|
|
||||||
.replace(/\s+/g, '-');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSlugInput() {
|
||||||
|
slugHandler.onSlugInput(slug);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -40,7 +48,26 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<label for="slug">URL 别名 *</label>
|
<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>
|
<label for="description">描述</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
Reference in new issue
Block a user