This commit is contained in:
1 parent
ff7eac407a
commit
827ceccdf0
4 files changed
+395
-285
No files matched your search
@@ -7,6 +7,8 @@
|
|||||||
import TextInput from '$lib/components/admin/TextInput.svelte';
|
import TextInput from '$lib/components/admin/TextInput.svelte';
|
||||||
import Button from '$lib/components/admin/Button.svelte';
|
import Button from '$lib/components/admin/Button.svelte';
|
||||||
import Alert from '$lib/components/admin/Alert.svelte';
|
import Alert from '$lib/components/admin/Alert.svelte';
|
||||||
|
import Tabs from '$lib/components/admin/Tabs.svelte';
|
||||||
|
import ArticleAttachments from '$lib/components/admin/ArticleAttachments.svelte';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
|
|
||||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||||
@@ -16,6 +18,9 @@
|
|||||||
let slug = $state(initialData.page.slug);
|
let slug = $state(initialData.page.slug);
|
||||||
let content = $state(initialData.page.content);
|
let content = $state(initialData.page.content);
|
||||||
let mode = $state<'edit' | 'preview'>('edit');
|
let mode = $state<'edit' | 'preview'>('edit');
|
||||||
|
|
||||||
|
let sidebarTab = $state('settings');
|
||||||
|
let sessionUploads = $state<{ url: string; key: string; filename: string }[]>([]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="admin-page-header">
|
<div class="admin-page-header">
|
||||||
@@ -71,7 +76,10 @@
|
|||||||
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if mode === 'edit'}
|
{#if mode === 'edit'}
|
||||||
<MarkdownEditor bind:value={content} />
|
<MarkdownEditor
|
||||||
|
bind:value={content}
|
||||||
|
onUpload={(img) => (sessionUploads = [...sessionUploads, img])}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="preview-area">
|
<div class="preview-area">
|
||||||
<MarkdownRenderer {content} />
|
<MarkdownRenderer {content} />
|
||||||
@@ -82,23 +90,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-sidebar">
|
<div class="form-sidebar">
|
||||||
<div class="sidebar-box">
|
<Tabs
|
||||||
<h3>更新</h3>
|
tabs={[
|
||||||
<Button type="submit" variant="primary" fullWidth class="submit-btn">保存修改</Button>
|
{ id: 'settings', label: '设置' },
|
||||||
</div>
|
{ id: 'media', label: '媒体附件' }
|
||||||
|
]}
|
||||||
|
bind:activeTab={sidebarTab}
|
||||||
|
fullWidth={true}
|
||||||
|
small={true}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="sidebar-box">
|
{#if sidebarTab === 'settings'}
|
||||||
<h3>别名 *</h3>
|
<div class="sidebar-box">
|
||||||
<TextInput
|
<h3>更新</h3>
|
||||||
type="text"
|
<Button type="submit" variant="primary" fullWidth class="submit-btn">保存修改</Button>
|
||||||
id="slug"
|
</div>
|
||||||
name="slug"
|
|
||||||
bind:value={slug}
|
<div class="sidebar-box">
|
||||||
placeholder="hello-world"
|
<h3>别名 *</h3>
|
||||||
required
|
<TextInput
|
||||||
variant="underline"
|
type="text"
|
||||||
/>
|
id="slug"
|
||||||
</div>
|
name="slug"
|
||||||
|
bind:value={slug}
|
||||||
|
placeholder="hello-world"
|
||||||
|
required
|
||||||
|
variant="underline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>当前页面附件</h3>
|
||||||
|
<ArticleAttachments bind:content bind:sessionUploads />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
import TextInput from '$lib/components/admin/TextInput.svelte';
|
import TextInput from '$lib/components/admin/TextInput.svelte';
|
||||||
import Button from '$lib/components/admin/Button.svelte';
|
import Button from '$lib/components/admin/Button.svelte';
|
||||||
import Alert from '$lib/components/admin/Alert.svelte';
|
import Alert from '$lib/components/admin/Alert.svelte';
|
||||||
|
import Tabs from '$lib/components/admin/Tabs.svelte';
|
||||||
|
import ArticleAttachments from '$lib/components/admin/ArticleAttachments.svelte';
|
||||||
|
|
||||||
import { createSlugAutoHandler } from '$lib/utils/slugify';
|
import { createSlugAutoHandler } from '$lib/utils/slugify';
|
||||||
|
|
||||||
@@ -19,6 +21,9 @@
|
|||||||
let isSlugUserModified = $state(false);
|
let isSlugUserModified = $state(false);
|
||||||
let isGenerating = $state(false);
|
let isGenerating = $state(false);
|
||||||
|
|
||||||
|
let sidebarTab = $state('settings');
|
||||||
|
let sessionUploads = $state<{ url: string; key: string; filename: string }[]>([]);
|
||||||
|
|
||||||
const slugHandler = createSlugAutoHandler({
|
const slugHandler = createSlugAutoHandler({
|
||||||
getSlug: () => slug,
|
getSlug: () => slug,
|
||||||
setSlug: (val) => (slug = val),
|
setSlug: (val) => (slug = val),
|
||||||
@@ -80,7 +85,10 @@
|
|||||||
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if mode === 'edit'}
|
{#if mode === 'edit'}
|
||||||
<MarkdownEditor bind:value={content} />
|
<MarkdownEditor
|
||||||
|
bind:value={content}
|
||||||
|
onUpload={(img) => (sessionUploads = [...sessionUploads, img])}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="preview-area">
|
<div class="preview-area">
|
||||||
<MarkdownRenderer {content} />
|
<MarkdownRenderer {content} />
|
||||||
@@ -91,34 +99,51 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-sidebar">
|
<div class="form-sidebar">
|
||||||
<div class="sidebar-box">
|
<Tabs
|
||||||
<h3>发布</h3>
|
tabs={[
|
||||||
<Button type="submit" variant="primary" fullWidth class="submit-btn">发布</Button>
|
{ id: 'settings', label: '设置' },
|
||||||
</div>
|
{ id: 'media', label: '媒体附件' }
|
||||||
|
]}
|
||||||
|
bind:activeTab={sidebarTab}
|
||||||
|
fullWidth={true}
|
||||||
|
small={true}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="sidebar-box">
|
{#if sidebarTab === 'settings'}
|
||||||
<h3 style="display: flex; justify-content: space-between; align-items: center;">
|
<div class="sidebar-box">
|
||||||
<span>别名 *</span>
|
<h3>发布</h3>
|
||||||
<button
|
<Button type="submit" variant="primary" fullWidth class="submit-btn">发布</Button>
|
||||||
type="button"
|
</div>
|
||||||
style="background: none; border: none; padding: 0; color: #666; font-size: 0.8rem; cursor: pointer; outline: none;"
|
|
||||||
onclick={() => slugHandler.forceGenerate(title)}
|
<div class="sidebar-box">
|
||||||
disabled={isGenerating}
|
<h3 style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
>
|
<span>别名 *</span>
|
||||||
{isGenerating ? '正在生成...' : '重新生成'}
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</h3>
|
style="background: none; border: none; padding: 0; color: #666; font-size: 0.8rem; cursor: pointer; outline: none;"
|
||||||
<TextInput
|
onclick={() => slugHandler.forceGenerate(title)}
|
||||||
type="text"
|
disabled={isGenerating}
|
||||||
id="slug"
|
>
|
||||||
name="slug"
|
{isGenerating ? '正在生成...' : '重新生成'}
|
||||||
bind:value={slug}
|
</button>
|
||||||
oninput={handleSlugInput}
|
</h3>
|
||||||
placeholder="about"
|
<TextInput
|
||||||
required
|
type="text"
|
||||||
variant="underline"
|
id="slug"
|
||||||
/>
|
name="slug"
|
||||||
</div>
|
bind:value={slug}
|
||||||
|
oninput={handleSlugInput}
|
||||||
|
placeholder="about"
|
||||||
|
required
|
||||||
|
variant="underline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>当前页面附件</h3>
|
||||||
|
<ArticleAttachments bind:content bind:sessionUploads />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
import TextInput from '$lib/components/admin/TextInput.svelte';
|
import TextInput from '$lib/components/admin/TextInput.svelte';
|
||||||
import Button from '$lib/components/admin/Button.svelte';
|
import Button from '$lib/components/admin/Button.svelte';
|
||||||
import Alert from '$lib/components/admin/Alert.svelte';
|
import Alert from '$lib/components/admin/Alert.svelte';
|
||||||
|
import Tabs from '$lib/components/admin/Tabs.svelte';
|
||||||
|
import ArticleAttachments from '$lib/components/admin/ArticleAttachments.svelte';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
|
|
||||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||||
@@ -30,6 +32,9 @@
|
|||||||
let tagsList = $state(initialData.tags || []);
|
let tagsList = $state(initialData.tags || []);
|
||||||
let seriesList = $state(initialData.series || []);
|
let seriesList = $state(initialData.series || []);
|
||||||
|
|
||||||
|
let sidebarTab = $state('settings');
|
||||||
|
let sessionUploads = $state<{ url: string; key: string; filename: string }[]>([]);
|
||||||
|
|
||||||
function toggleTag(tagId: number) {
|
function toggleTag(tagId: number) {
|
||||||
if (selectedTags.includes(tagId)) {
|
if (selectedTags.includes(tagId)) {
|
||||||
selectedTags = selectedTags.filter((id) => id !== tagId);
|
selectedTags = selectedTags.filter((id) => id !== tagId);
|
||||||
@@ -155,7 +160,10 @@
|
|||||||
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if mode === 'edit'}
|
{#if mode === 'edit'}
|
||||||
<MarkdownEditor bind:value={content} />
|
<MarkdownEditor
|
||||||
|
bind:value={content}
|
||||||
|
onUpload={(img) => (sessionUploads = [...sessionUploads, img])}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="preview-area">
|
<div class="preview-area">
|
||||||
<MarkdownRenderer {content} />
|
<MarkdownRenderer {content} />
|
||||||
@@ -166,126 +174,148 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-sidebar">
|
<div class="form-sidebar">
|
||||||
<div class="sidebar-box">
|
<Tabs
|
||||||
<h3>保存</h3>
|
tabs={[
|
||||||
<Button type="submit" variant="primary" fullWidth class="submit-btn">保存</Button>
|
{ id: 'settings', label: '设置' },
|
||||||
</div>
|
{ id: 'media', label: '媒体附件' }
|
||||||
|
]}
|
||||||
|
bind:activeTab={sidebarTab}
|
||||||
|
fullWidth={true}
|
||||||
|
small={true}
|
||||||
|
/>
|
||||||
|
|
||||||
<FeaturedImagePicker bind:cover adaptiveHeight previewable />
|
{#if sidebarTab === 'settings'}
|
||||||
|
<div class="sidebar-box">
|
||||||
<div class="sidebar-box">
|
<h3>保存</h3>
|
||||||
<h3>别名 *</h3>
|
<Button type="submit" variant="primary" fullWidth class="submit-btn">保存</Button>
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
id="slug"
|
|
||||||
name="slug"
|
|
||||||
bind:value={slug}
|
|
||||||
placeholder="hello-world"
|
|
||||||
required
|
|
||||||
variant="underline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-box">
|
|
||||||
<h3>分类</h3>
|
|
||||||
<SelectDropdown
|
|
||||||
name="category_id"
|
|
||||||
label="分类"
|
|
||||||
options={data.categories.map((category) => ({
|
|
||||||
value: category.id,
|
|
||||||
label: category.name
|
|
||||||
}))}
|
|
||||||
bind:value={selectedCategoryId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-box">
|
|
||||||
<h3>{$page.data.aliases?.series || '系列'}</h3>
|
|
||||||
<SelectDropdown
|
|
||||||
name="series_id"
|
|
||||||
label={$page.data.aliases?.series || '系列'}
|
|
||||||
options={seriesList.map((series) => ({ value: series.id, label: series.name }))}
|
|
||||||
bind:value={selectedSeriesId}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
type="number"
|
|
||||||
name="series_weight"
|
|
||||||
style="margin-top: 10px;"
|
|
||||||
bind:value={seriesWeight}
|
|
||||||
placeholder="顺序(留空自动追加)"
|
|
||||||
disabled={!selectedSeriesId}
|
|
||||||
step="1"
|
|
||||||
variant="underline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-box">
|
|
||||||
<h3>标签</h3>
|
|
||||||
<div class="selected-tags-vertical">
|
|
||||||
{#each selectedTags as tagId (tagId)}
|
|
||||||
{#if tagsList.find((t) => t.id === tagId)}
|
|
||||||
{@const tag = tagsList.find((t) => t.id === tagId)}
|
|
||||||
<div class="selected-tag-row">
|
|
||||||
<span>{tag?.name}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="remove-tag-btn"
|
|
||||||
onclick={() => toggleTag(tagId)}
|
|
||||||
title="移除标签"
|
|
||||||
><svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
fill="none"
|
|
||||||
><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"
|
|
||||||
></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<input type="hidden" name="tags" value={tagId} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tag-input-container">
|
<div class="sidebar-box">
|
||||||
|
<h3>别名 *</h3>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
class="tag-input-new"
|
id="slug"
|
||||||
placeholder="+ 添加标签..."
|
name="slug"
|
||||||
bind:value={newTagInput}
|
bind:value={slug}
|
||||||
onkeydown={handleAddTag}
|
placeholder="hello-world"
|
||||||
onfocus={() => (showTagDropdown = true)}
|
required
|
||||||
onblur={() => setTimeout(() => (showTagDropdown = false), 200)}
|
|
||||||
disabled={isAddingTag}
|
|
||||||
autocomplete="off"
|
|
||||||
variant="underline"
|
variant="underline"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if showTagDropdown && filteredTags.length > 0}
|
|
||||||
<div class="tag-autocomplete-dropdown">
|
|
||||||
{#each filteredTags as tag (tag.id || tag)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="tag-dropdown-item"
|
|
||||||
onclick={() => {
|
|
||||||
if (!selectedTags.includes(tag.id)) {
|
|
||||||
selectedTags = [...selectedTags, tag.id];
|
|
||||||
}
|
|
||||||
newTagInput = '';
|
|
||||||
showTagDropdown = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tag.name}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>分类</h3>
|
||||||
|
<SelectDropdown
|
||||||
|
name="category_id"
|
||||||
|
label="分类"
|
||||||
|
options={data.categories.map((category) => ({
|
||||||
|
value: category.id,
|
||||||
|
label: category.name
|
||||||
|
}))}
|
||||||
|
bind:value={selectedCategoryId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>{$page.data.aliases?.series || '系列'}</h3>
|
||||||
|
<SelectDropdown
|
||||||
|
name="series_id"
|
||||||
|
label={$page.data.aliases?.series || '系列'}
|
||||||
|
options={seriesList.map((series) => ({ value: series.id, label: series.name }))}
|
||||||
|
bind:value={selectedSeriesId}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
name="series_weight"
|
||||||
|
style="margin-top: 10px;"
|
||||||
|
bind:value={seriesWeight}
|
||||||
|
placeholder="顺序(留空自动追加)"
|
||||||
|
disabled={!selectedSeriesId}
|
||||||
|
step="1"
|
||||||
|
variant="underline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>标签</h3>
|
||||||
|
<div class="selected-tags-vertical">
|
||||||
|
{#each selectedTags as tagId (tagId)}
|
||||||
|
{#if tagsList.find((t) => t.id === tagId)}
|
||||||
|
{@const tag = tagsList.find((t) => t.id === tagId)}
|
||||||
|
<div class="selected-tag-row">
|
||||||
|
<span>{tag?.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="remove-tag-btn"
|
||||||
|
onclick={() => toggleTag(tagId)}
|
||||||
|
title="移除标签"
|
||||||
|
><svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
><line x1="18" y1="6" x2="6" y2="18"></line><line
|
||||||
|
x1="6"
|
||||||
|
y1="6"
|
||||||
|
x2="18"
|
||||||
|
y2="18"
|
||||||
|
></line></svg
|
||||||
|
></button
|
||||||
|
>
|
||||||
|
<input type="hidden" name="tags" value={tagId} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tag-input-container">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
class="tag-input-new"
|
||||||
|
placeholder="+ 添加标签..."
|
||||||
|
bind:value={newTagInput}
|
||||||
|
onkeydown={handleAddTag}
|
||||||
|
onfocus={() => (showTagDropdown = true)}
|
||||||
|
onblur={() => setTimeout(() => (showTagDropdown = false), 200)}
|
||||||
|
disabled={isAddingTag}
|
||||||
|
autocomplete="off"
|
||||||
|
variant="underline"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if showTagDropdown && filteredTags.length > 0}
|
||||||
|
<div class="tag-autocomplete-dropdown">
|
||||||
|
{#each filteredTags as tag (tag.id || tag)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tag-dropdown-item"
|
||||||
|
onclick={() => {
|
||||||
|
if (!selectedTags.includes(tag.id)) {
|
||||||
|
selectedTags = [...selectedTags, tag.id];
|
||||||
|
}
|
||||||
|
newTagInput = '';
|
||||||
|
showTagDropdown = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<FeaturedImagePicker bind:cover adaptiveHeight previewable />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>当前文章附件</h3>
|
||||||
|
<ArticleAttachments bind:content bind:sessionUploads />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
import TextInput from '$lib/components/admin/TextInput.svelte';
|
import TextInput from '$lib/components/admin/TextInput.svelte';
|
||||||
import Button from '$lib/components/admin/Button.svelte';
|
import Button from '$lib/components/admin/Button.svelte';
|
||||||
import Alert from '$lib/components/admin/Alert.svelte';
|
import Alert from '$lib/components/admin/Alert.svelte';
|
||||||
|
import Tabs from '$lib/components/admin/Tabs.svelte';
|
||||||
|
import ArticleAttachments from '$lib/components/admin/ArticleAttachments.svelte';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
|
|
||||||
import { createSlugAutoHandler } from '$lib/utils/slugify';
|
import { createSlugAutoHandler } from '$lib/utils/slugify';
|
||||||
@@ -44,6 +46,9 @@
|
|||||||
setIsGenerating: (val) => (isGenerating = val)
|
setIsGenerating: (val) => (isGenerating = val)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let sidebarTab = $state('settings');
|
||||||
|
let sessionUploads = $state<{ url: string; key: string; filename: string }[]>([]);
|
||||||
|
|
||||||
function handleTitleInput() {
|
function handleTitleInput() {
|
||||||
slugHandler.onNameInput(title);
|
slugHandler.onNameInput(title);
|
||||||
}
|
}
|
||||||
@@ -173,7 +178,10 @@
|
|||||||
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if mode === 'edit'}
|
{#if mode === 'edit'}
|
||||||
<MarkdownEditor bind:value={content} />
|
<MarkdownEditor
|
||||||
|
bind:value={content}
|
||||||
|
onUpload={(img) => (sessionUploads = [...sessionUploads, img])}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="preview-area">
|
<div class="preview-area">
|
||||||
<MarkdownRenderer {content} />
|
<MarkdownRenderer {content} />
|
||||||
@@ -184,140 +192,162 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-sidebar">
|
<div class="form-sidebar">
|
||||||
<div class="sidebar-box">
|
<Tabs
|
||||||
<h3>发布</h3>
|
tabs={[
|
||||||
<Button type="submit" variant="primary" fullWidth class="publish-btn">
|
{ id: 'settings', label: '设置' },
|
||||||
发布{$page.data.aliases?.post || '文章'}
|
{ id: 'media', label: '媒体附件' }
|
||||||
</Button>
|
]}
|
||||||
</div>
|
bind:activeTab={sidebarTab}
|
||||||
|
fullWidth={true}
|
||||||
|
small={true}
|
||||||
|
/>
|
||||||
|
|
||||||
<FeaturedImagePicker bind:cover adaptiveHeight previewable />
|
{#if sidebarTab === 'settings'}
|
||||||
|
<div class="sidebar-box">
|
||||||
<div class="sidebar-box">
|
<h3>发布</h3>
|
||||||
<h3 style="display: flex; justify-content: space-between; align-items: center;">
|
<Button type="submit" variant="primary" fullWidth class="publish-btn">
|
||||||
<span>别名 *</span>
|
发布{$page.data.aliases?.post || '文章'}
|
||||||
<button
|
</Button>
|
||||||
type="button"
|
|
||||||
style="background: none; border: none; padding: 0; color: #666; font-size: 0.8rem; cursor: pointer; outline: none;"
|
|
||||||
title="生成英文 Slug"
|
|
||||||
onclick={() => slugHandler.forceGenerate(title)}
|
|
||||||
disabled={isGenerating}
|
|
||||||
>
|
|
||||||
{isGenerating ? '正在生成...' : '重新生成'}
|
|
||||||
</button>
|
|
||||||
</h3>
|
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
id="slug"
|
|
||||||
name="slug"
|
|
||||||
bind:value={slug}
|
|
||||||
oninput={handleSlugInput}
|
|
||||||
placeholder="hello-world"
|
|
||||||
required
|
|
||||||
variant="underline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-box">
|
|
||||||
<h3>分类</h3>
|
|
||||||
<SelectDropdown
|
|
||||||
name="category_id"
|
|
||||||
label="分类"
|
|
||||||
options={data.categories.map((category) => ({
|
|
||||||
value: category.id,
|
|
||||||
label: category.name
|
|
||||||
}))}
|
|
||||||
bind:value={selectedCategoryId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-box">
|
|
||||||
<h3>{$page.data.aliases?.series || '系列'}</h3>
|
|
||||||
<SelectDropdown
|
|
||||||
name="series_id"
|
|
||||||
label={$page.data.aliases?.series || '系列'}
|
|
||||||
options={seriesList.map((series) => ({ value: series.id, label: series.name }))}
|
|
||||||
bind:value={selectedSeriesId}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
type="number"
|
|
||||||
name="series_weight"
|
|
||||||
style="margin-top: 10px;"
|
|
||||||
bind:value={seriesWeight}
|
|
||||||
placeholder="顺序(留空自动追加)"
|
|
||||||
disabled={!selectedSeriesId}
|
|
||||||
step="1"
|
|
||||||
variant="underline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-box">
|
|
||||||
<h3>标签</h3>
|
|
||||||
<div class="selected-tags-vertical">
|
|
||||||
{#each selectedTags as tagId (tagId)}
|
|
||||||
{#if tagsList.find((t) => t.id === tagId)}
|
|
||||||
{@const tag = tagsList.find((t) => t.id === tagId)}
|
|
||||||
<div class="selected-tag-row">
|
|
||||||
<span>{tag?.name}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="remove-tag-btn"
|
|
||||||
onclick={() => toggleTag(tagId)}
|
|
||||||
title="移除标签"
|
|
||||||
><svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
fill="none"
|
|
||||||
><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"
|
|
||||||
></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<input type="hidden" name="tags" value={tagId} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tag-input-container">
|
<div class="sidebar-box">
|
||||||
|
<h3 style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<span>别名 *</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style="background: none; border: none; padding: 0; color: #666; font-size: 0.8rem; cursor: pointer; outline: none;"
|
||||||
|
title="生成英文 Slug"
|
||||||
|
onclick={() => slugHandler.forceGenerate(title)}
|
||||||
|
disabled={isGenerating}
|
||||||
|
>
|
||||||
|
{isGenerating ? '正在生成...' : '重新生成'}
|
||||||
|
</button>
|
||||||
|
</h3>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
class="tag-input-new"
|
id="slug"
|
||||||
placeholder="+ 添加标签..."
|
name="slug"
|
||||||
bind:value={newTagInput}
|
bind:value={slug}
|
||||||
onkeydown={handleAddTag}
|
oninput={handleSlugInput}
|
||||||
onfocus={() => (showTagDropdown = true)}
|
placeholder="hello-world"
|
||||||
onblur={() => setTimeout(() => (showTagDropdown = false), 200)}
|
required
|
||||||
disabled={isAddingTag}
|
|
||||||
autocomplete="off"
|
|
||||||
variant="underline"
|
variant="underline"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if showTagDropdown && filteredTags.length > 0}
|
|
||||||
<div class="tag-autocomplete-dropdown">
|
|
||||||
{#each filteredTags as tag (tag.id || tag)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="tag-dropdown-item"
|
|
||||||
onclick={() => {
|
|
||||||
if (!selectedTags.includes(tag.id)) {
|
|
||||||
selectedTags = [...selectedTags, tag.id];
|
|
||||||
}
|
|
||||||
newTagInput = '';
|
|
||||||
showTagDropdown = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tag.name}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>分类</h3>
|
||||||
|
<SelectDropdown
|
||||||
|
name="category_id"
|
||||||
|
label="分类"
|
||||||
|
options={data.categories.map((category) => ({
|
||||||
|
value: category.id,
|
||||||
|
label: category.name
|
||||||
|
}))}
|
||||||
|
bind:value={selectedCategoryId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>{$page.data.aliases?.series || '系列'}</h3>
|
||||||
|
<SelectDropdown
|
||||||
|
name="series_id"
|
||||||
|
label={$page.data.aliases?.series || '系列'}
|
||||||
|
options={seriesList.map((series) => ({ value: series.id, label: series.name }))}
|
||||||
|
bind:value={selectedSeriesId}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
name="series_weight"
|
||||||
|
style="margin-top: 10px;"
|
||||||
|
bind:value={seriesWeight}
|
||||||
|
placeholder="顺序(留空自动追加)"
|
||||||
|
disabled={!selectedSeriesId}
|
||||||
|
step="1"
|
||||||
|
variant="underline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>标签</h3>
|
||||||
|
<div class="selected-tags-vertical">
|
||||||
|
{#each selectedTags as tagId (tagId)}
|
||||||
|
{#if tagsList.find((t) => t.id === tagId)}
|
||||||
|
{@const tag = tagsList.find((t) => t.id === tagId)}
|
||||||
|
<div class="selected-tag-row">
|
||||||
|
<span>{tag?.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="remove-tag-btn"
|
||||||
|
onclick={() => toggleTag(tagId)}
|
||||||
|
title="移除标签"
|
||||||
|
><svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
><line x1="18" y1="6" x2="6" y2="18"></line><line
|
||||||
|
x1="6"
|
||||||
|
y1="6"
|
||||||
|
x2="18"
|
||||||
|
y2="18"
|
||||||
|
></line></svg
|
||||||
|
></button
|
||||||
|
>
|
||||||
|
<input type="hidden" name="tags" value={tagId} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tag-input-container">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
class="tag-input-new"
|
||||||
|
placeholder="+ 添加标签..."
|
||||||
|
bind:value={newTagInput}
|
||||||
|
onkeydown={handleAddTag}
|
||||||
|
onfocus={() => (showTagDropdown = true)}
|
||||||
|
onblur={() => setTimeout(() => (showTagDropdown = false), 200)}
|
||||||
|
disabled={isAddingTag}
|
||||||
|
autocomplete="off"
|
||||||
|
variant="underline"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if showTagDropdown && filteredTags.length > 0}
|
||||||
|
<div class="tag-autocomplete-dropdown">
|
||||||
|
{#each filteredTags as tag (tag.id || tag)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tag-dropdown-item"
|
||||||
|
onclick={() => {
|
||||||
|
if (!selectedTags.includes(tag.id)) {
|
||||||
|
selectedTags = [...selectedTags, tag.id];
|
||||||
|
}
|
||||||
|
newTagInput = '';
|
||||||
|
showTagDropdown = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<FeaturedImagePicker bind:cover adaptiveHeight previewable />
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-box">
|
||||||
|
<h3>当前文章附件</h3>
|
||||||
|
<ArticleAttachments bind:content bind:sessionUploads />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in new issue
Block a user