This commit is contained in:
1 parent
910dd99ae0
commit
c91ef5aac9
5 files changed
+427
-744
No files matched your search
@@ -0,0 +1,419 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { EditorView, basicSetup } from 'codemirror';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
|
import { languages } from '@codemirror/language-data';
|
||||||
|
import { placeholder as cmPlaceholder } from '@codemirror/view';
|
||||||
|
|
||||||
|
let { value = $bindable(''), placeholderText = '支持 Markdown 格式...' } = $props();
|
||||||
|
|
||||||
|
let editorContainer: HTMLDivElement;
|
||||||
|
let view: EditorView;
|
||||||
|
let isUpdating = false;
|
||||||
|
|
||||||
|
const customTheme = EditorView.theme({
|
||||||
|
'&': {
|
||||||
|
color: 'var(--text-color)',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.015)',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: '1.05rem',
|
||||||
|
border: '1px solid rgba(0, 0, 0, 0.12)',
|
||||||
|
transition: 'border-color 0.3s ease, box-shadow 0.3s ease',
|
||||||
|
minHeight: '400px',
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
'&.cm-focused': {
|
||||||
|
outline: 'none',
|
||||||
|
borderColor: 'var(--accent-color, #419b38)',
|
||||||
|
boxShadow: '0 0 0 3px rgba(65, 155, 56, 0.08)'
|
||||||
|
},
|
||||||
|
'.cm-content': {
|
||||||
|
minHeight: '400px',
|
||||||
|
padding: '12px 14px',
|
||||||
|
lineHeight: '1.8'
|
||||||
|
},
|
||||||
|
'.cm-scroller': {
|
||||||
|
overflow: 'auto',
|
||||||
|
minHeight: '400px'
|
||||||
|
},
|
||||||
|
'.cm-gutters': {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.03)',
|
||||||
|
color: 'rgba(0, 0, 0, 0.4)',
|
||||||
|
borderRight: '1px solid rgba(0, 0, 0, 0.12)',
|
||||||
|
borderTopLeftRadius: '0',
|
||||||
|
borderBottomLeftRadius: '0'
|
||||||
|
},
|
||||||
|
'.cm-activeLineGutter': {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.08)'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const state = EditorState.create({
|
||||||
|
doc: value,
|
||||||
|
extensions: [
|
||||||
|
basicSetup,
|
||||||
|
cmPlaceholder(placeholderText),
|
||||||
|
markdown({
|
||||||
|
base: markdownLanguage,
|
||||||
|
codeLanguages: languages
|
||||||
|
}),
|
||||||
|
customTheme,
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
isUpdating = true;
|
||||||
|
value = update.state.doc.toString();
|
||||||
|
isUpdating = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
view = new EditorView({
|
||||||
|
state,
|
||||||
|
parent: editorContainer
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (view) {
|
||||||
|
view.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (view && value !== view.state.doc.toString() && !isUpdating) {
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: 0, to: view.state.doc.length, insert: value }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function insertAtCursor(before: string, after: string = '', defaultText: string = '') {
|
||||||
|
if (!view) return;
|
||||||
|
|
||||||
|
const selection = view.state.selection.main;
|
||||||
|
const selectedText = view.state.sliceDoc(selection.from, selection.to);
|
||||||
|
const textToInsert = selectedText
|
||||||
|
? before + selectedText + after
|
||||||
|
: before + defaultText + after;
|
||||||
|
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: selection.from,
|
||||||
|
to: selection.to,
|
||||||
|
insert: textToInsert
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
anchor: selection.from + textToInsert.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view.focus();
|
||||||
|
|
||||||
|
if (!selectedText && defaultText) {
|
||||||
|
view.dispatch({
|
||||||
|
selection: {
|
||||||
|
anchor: selection.from + before.length,
|
||||||
|
head: selection.from + before.length + defaultText.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertMore() {
|
||||||
|
insertAtCursor('\n<!--more-->\n', '', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
let showAdmonitionMenu = $state(false);
|
||||||
|
const admonitionTypes = [
|
||||||
|
{ type: 'note', title: '注意', label: '注意 (Note)' },
|
||||||
|
{ type: 'abstract', title: '摘要', label: '摘要 (Abstract)' },
|
||||||
|
{ type: 'info', title: '信息', label: '信息 (Info)' },
|
||||||
|
{ type: 'tip', title: '技巧', label: '技巧 (Tip)' },
|
||||||
|
{ type: 'success', title: '成功', label: '成功 (Success)' },
|
||||||
|
{ type: 'question', title: '问题', label: '问题 (Question)' },
|
||||||
|
{ type: 'warning', title: '警告', label: '警告 (Warning)' },
|
||||||
|
{ type: 'failure', title: '失败', label: '失败 (Failure)' },
|
||||||
|
{ type: 'danger', title: '危险', label: '危险 (Danger)' },
|
||||||
|
{ type: 'bug', title: 'Bug', label: 'Bug' },
|
||||||
|
{ type: 'example', title: '示例', label: '示例 (Example)' },
|
||||||
|
{ type: 'quote', title: '引用', label: '引用 (Quote)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function insertAdmonition(type: string, title: string) {
|
||||||
|
const openTag = `{{< admonition ${type} "${title}" >}}\n`;
|
||||||
|
const closeTag = `\n{{< /admonition >}}`;
|
||||||
|
insertAtCursor('\n' + openTag, closeTag, '在这里输入提示内容...');
|
||||||
|
showAdmonitionMenu = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="editor-wrapper">
|
||||||
|
<div class="md-toolbar">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-btn"
|
||||||
|
onclick={() => insertAtCursor('**', '**', '粗体')}
|
||||||
|
title="加粗"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24"
|
||||||
|
><path d="M6 4h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path
|
||||||
|
d="M6 12h10a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-btn"
|
||||||
|
onclick={() => insertAtCursor('*', '*', '斜体')}
|
||||||
|
title="斜体"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24"
|
||||||
|
><line x1="19" y1="4" x2="10" y2="4"></line><line x1="14" y1="20" x2="5" y2="20"
|
||||||
|
></line><line x1="15" y1="4" x2="9" y2="20"></line></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-btn"
|
||||||
|
onclick={() => insertAtCursor('~~', '~~', '删除线')}
|
||||||
|
title="删除线"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24"
|
||||||
|
><path d="M16 4H9a3 3 0 0 0-2.83 4"></path><path d="M14 12a4 4 0 0 1 0 8H6"></path><line
|
||||||
|
x1="4"
|
||||||
|
y1="12"
|
||||||
|
x2="20"
|
||||||
|
y2="12"
|
||||||
|
></line></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-btn"
|
||||||
|
onclick={() => insertAtCursor('### ', '', '标题')}
|
||||||
|
title="标题"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M4 12h16M4 18V6M20 18V6"></path></svg>
|
||||||
|
</button>
|
||||||
|
<div class="md-divider"></div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-btn"
|
||||||
|
onclick={() => insertAtCursor('- ', '', '列表项')}
|
||||||
|
title="无序列表"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24"
|
||||||
|
><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line
|
||||||
|
x1="8"
|
||||||
|
y1="18"
|
||||||
|
x2="21"
|
||||||
|
y2="18"
|
||||||
|
></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"
|
||||||
|
></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-btn"
|
||||||
|
onclick={() => insertAtCursor('[', '](https://)', '链接文字')}
|
||||||
|
title="链接"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24"
|
||||||
|
><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path
|
||||||
|
d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-btn"
|
||||||
|
onclick={() => insertAtCursor('', '图片描述')}
|
||||||
|
title="图片"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24"
|
||||||
|
><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><polygon
|
||||||
|
points="6 21, 10 13, 13 16.5, 16.5 9.5, 19 21"
|
||||||
|
style="fill: currentColor; stroke: currentColor; stroke-linejoin: round; stroke-width: 1.5;"
|
||||||
|
></polygon></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-btn"
|
||||||
|
onclick={() => insertAtCursor('```\n', '\n```', 'code')}
|
||||||
|
title="代码块"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24"
|
||||||
|
><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"
|
||||||
|
></polyline></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<div class="md-divider"></div>
|
||||||
|
<div class="toolbar-dropdown-wrapper">
|
||||||
|
{#if showAdmonitionMenu}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
||||||
|
<div
|
||||||
|
class="dropdown-overlay"
|
||||||
|
onclick={() => (showAdmonitionMenu = false)}
|
||||||
|
role="button"
|
||||||
|
></div>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-btn"
|
||||||
|
onclick={() => (showAdmonitionMenu = !showAdmonitionMenu)}
|
||||||
|
title="提示框 (Admonition)"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24"
|
||||||
|
><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"
|
||||||
|
></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
{#if showAdmonitionMenu}
|
||||||
|
<div class="toolbar-dropdown">
|
||||||
|
{#each admonitionTypes as item (item.type)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item"
|
||||||
|
onclick={() => insertAdmonition(item.type, item.title)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="md-btn" onclick={insertMore} title="插入摘要分割线">
|
||||||
|
<svg viewBox="0 0 24 24"
|
||||||
|
><polyline points="5 5 5 9 19 9 19 5"></polyline><polyline points="5 19 5 15 19 15 19 19"
|
||||||
|
></polyline><line
|
||||||
|
x1="4"
|
||||||
|
y1="12"
|
||||||
|
x2="20"
|
||||||
|
y2="12"
|
||||||
|
stroke-dasharray="3 3"
|
||||||
|
stroke-linecap="butt"
|
||||||
|
></line></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- hidden textarea for form submission -->
|
||||||
|
<textarea name="content" style="display:none;" {value}></textarea>
|
||||||
|
<div bind:this={editorContainer} class="editor-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editor-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.65;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--accent-color, #419b38);
|
||||||
|
background-color: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-btn svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-divider {
|
||||||
|
width: 2px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--text-color);
|
||||||
|
opacity: 0.15;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-dropdown-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: var(--bg-color, #ffffff);
|
||||||
|
border: 1px solid var(--text-color);
|
||||||
|
z-index: 100;
|
||||||
|
width: 140px;
|
||||||
|
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: var(--text-color);
|
||||||
|
color: var(--bg-color, #ffffff);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import type { PageData, ActionData } from './$types';
|
import type { PageData, ActionData } from './$types';
|
||||||
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
||||||
|
import MarkdownEditor from '$lib/components/admin/MarkdownEditor.svelte';
|
||||||
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';
|
||||||
@@ -70,14 +71,7 @@
|
|||||||
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if mode === 'edit'}
|
{#if mode === 'edit'}
|
||||||
<textarea
|
<MarkdownEditor bind:value={content} />
|
||||||
id="content"
|
|
||||||
name="content"
|
|
||||||
bind:value={content}
|
|
||||||
rows="16"
|
|
||||||
style="min-height: 400px;"
|
|
||||||
placeholder="支持 Markdown 格式..."
|
|
||||||
required></textarea>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="preview-area">
|
<div class="preview-area">
|
||||||
<MarkdownRenderer {content} />
|
<MarkdownRenderer {content} />
|
||||||
@@ -220,33 +214,6 @@
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
||||||
border-radius: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.015);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition:
|
|
||||||
border-color 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
line-height: 1.8;
|
|
||||||
min-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-color, #419b38);
|
|
||||||
box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-box {
|
.sidebar-box {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
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 MarkdownEditor from '$lib/components/admin/MarkdownEditor.svelte';
|
||||||
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';
|
||||||
@@ -79,14 +80,7 @@
|
|||||||
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if mode === 'edit'}
|
{#if mode === 'edit'}
|
||||||
<textarea
|
<MarkdownEditor bind:value={content} />
|
||||||
id="content"
|
|
||||||
name="content"
|
|
||||||
bind:value={content}
|
|
||||||
rows="16"
|
|
||||||
style="min-height: 400px;"
|
|
||||||
placeholder="支持 Markdown 格式..."
|
|
||||||
required></textarea>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="preview-area">
|
<div class="preview-area">
|
||||||
<MarkdownRenderer {content} />
|
<MarkdownRenderer {content} />
|
||||||
@@ -235,33 +229,6 @@
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
||||||
border-radius: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.015);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition:
|
|
||||||
border-color 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
line-height: 1.8;
|
|
||||||
min-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-color, #419b38);
|
|
||||||
box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-box {
|
.sidebar-box {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import type { PageData, ActionData } from './$types';
|
import type { PageData, ActionData } from './$types';
|
||||||
import { deserialize } from '$app/forms';
|
import { deserialize } from '$app/forms';
|
||||||
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
||||||
|
import MarkdownEditor from '$lib/components/admin/MarkdownEditor.svelte';
|
||||||
import FeaturedImagePicker from '$lib/components/admin/FeaturedImagePicker.svelte';
|
import FeaturedImagePicker from '$lib/components/admin/FeaturedImagePicker.svelte';
|
||||||
import SelectDropdown from '$lib/components/admin/SelectDropdown.svelte';
|
import SelectDropdown from '$lib/components/admin/SelectDropdown.svelte';
|
||||||
import { parseHugoMarkdown } from '$lib/hugoParser';
|
import { parseHugoMarkdown } from '$lib/hugoParser';
|
||||||
@@ -105,60 +106,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markdown 工具栏功能
|
|
||||||
function insertAtCursor(before: string, after: string = '', defaultText: string = '') {
|
|
||||||
const textarea = document.getElementById('content') as HTMLTextAreaElement;
|
|
||||||
if (!textarea) return;
|
|
||||||
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const selectedText = textarea.value.substring(start, end);
|
|
||||||
|
|
||||||
const insertion = selectedText ? before + selectedText + after : before + defaultText + after;
|
|
||||||
|
|
||||||
content = textarea.value.substring(0, start) + insertion + textarea.value.substring(end);
|
|
||||||
|
|
||||||
// 恢复焦点并选中文字
|
|
||||||
setTimeout(() => {
|
|
||||||
textarea.focus();
|
|
||||||
if (selectedText) {
|
|
||||||
textarea.setSelectionRange(start, start + insertion.length);
|
|
||||||
} else {
|
|
||||||
textarea.setSelectionRange(
|
|
||||||
start + before.length,
|
|
||||||
start + before.length + defaultText.length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertMore() {
|
|
||||||
insertAtCursor('\n<!--more-->\n', '', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
let showAdmonitionMenu = $state(false);
|
|
||||||
const admonitionTypes = [
|
|
||||||
{ type: 'note', title: '注意', label: '注意 (Note)' },
|
|
||||||
{ type: 'abstract', title: '摘要', label: '摘要 (Abstract)' },
|
|
||||||
{ type: 'info', title: '信息', label: '信息 (Info)' },
|
|
||||||
{ type: 'tip', title: '技巧', label: '技巧 (Tip)' },
|
|
||||||
{ type: 'success', title: '成功', label: '成功 (Success)' },
|
|
||||||
{ type: 'question', title: '问题', label: '问题 (Question)' },
|
|
||||||
{ type: 'warning', title: '警告', label: '警告 (Warning)' },
|
|
||||||
{ type: 'failure', title: '失败', label: '失败 (Failure)' },
|
|
||||||
{ type: 'danger', title: '危险', label: '危险 (Danger)' },
|
|
||||||
{ type: 'bug', title: 'Bug', label: 'Bug' },
|
|
||||||
{ type: 'example', title: '示例', label: '示例 (Example)' },
|
|
||||||
{ type: 'quote', title: '引用', label: '引用 (Quote)' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function insertAdmonition(type: string, title: string) {
|
|
||||||
const openTag = `{{< admonition ${type} "${title}" >}}\n`;
|
|
||||||
const closeTag = `\n{{< /admonition >}}`;
|
|
||||||
insertAtCursor('\n' + openTag, closeTag, '在这里输入提示内容...');
|
|
||||||
showAdmonitionMenu = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleImportMarkdown(e: Event) {
|
async function handleImportMarkdown(e: Event) {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
const file = target.files?.[0];
|
const file = target.files?.[0];
|
||||||
@@ -306,151 +253,6 @@
|
|||||||
<label for="content">正文 *</label>
|
<label for="content">正文 *</label>
|
||||||
|
|
||||||
<div class="editor-header-right">
|
<div class="editor-header-right">
|
||||||
{#if mode === 'edit'}
|
|
||||||
<div class="md-toolbar">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('**', '**', '粗体')}
|
|
||||||
title="加粗"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><path d="M6 4h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path
|
|
||||||
d="M6 12h10a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"
|
|
||||||
></path></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('*', '*', '斜体')}
|
|
||||||
title="斜体"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><line x1="19" y1="4" x2="10" y2="4"></line><line x1="14" y1="20" x2="5" y2="20"
|
|
||||||
></line><line x1="15" y1="4" x2="9" y2="20"></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('~~', '~~', '删除线')}
|
|
||||||
title="删除线"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><path d="M16 4H9a3 3 0 0 0-2.83 4"></path><path d="M14 12a4 4 0 0 1 0 8H6"
|
|
||||||
></path><line x1="4" y1="12" x2="20" y2="12"></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('### ', '', '标题')}
|
|
||||||
title="标题"
|
|
||||||
><svg viewBox="0 0 24 24"><path d="M4 12h16M4 18V6M20 18V6"></path></svg></button
|
|
||||||
>
|
|
||||||
<div class="md-divider"></div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('- ', '', '列表项')}
|
|
||||||
title="无序列表"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"
|
|
||||||
></line><line x1="8" y1="18" x2="21" y2="18"></line><line
|
|
||||||
x1="3"
|
|
||||||
y1="6"
|
|
||||||
x2="3.01"
|
|
||||||
y2="6"
|
|
||||||
></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line
|
|
||||||
x1="3"
|
|
||||||
y1="18"
|
|
||||||
x2="3.01"
|
|
||||||
y2="18"
|
|
||||||
></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('[', '](https://)', '链接文字')}
|
|
||||||
title="链接"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"
|
|
||||||
></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
|
|
||||||
></path></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('', '图片描述')}
|
|
||||||
title="图片"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><polygon
|
|
||||||
points="6 21, 10 13, 13 16.5, 16.5 9.5, 19 21"
|
|
||||||
style="fill: currentColor; stroke: currentColor; stroke-linejoin: round; stroke-width: 1.5;"
|
|
||||||
></polygon></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('```\n', '\n```', 'code')}
|
|
||||||
title="代码块"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"
|
|
||||||
></polyline></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<div class="md-divider"></div>
|
|
||||||
<div class="toolbar-dropdown-wrapper">
|
|
||||||
{#if showAdmonitionMenu}
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
||||||
<div
|
|
||||||
class="dropdown-overlay"
|
|
||||||
onclick={() => (showAdmonitionMenu = false)}
|
|
||||||
role="button"
|
|
||||||
></div>
|
|
||||||
{/if}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => (showAdmonitionMenu = !showAdmonitionMenu)}
|
|
||||||
title="提示框 (Admonition)"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"
|
|
||||||
></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
{#if showAdmonitionMenu}
|
|
||||||
<div class="toolbar-dropdown">
|
|
||||||
{#each admonitionTypes as item (item.type)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item"
|
|
||||||
onclick={() => insertAdmonition(item.type, item.title)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<button type="button" class="md-btn" onclick={insertMore} title="插入摘要分割线"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><polyline points="5 5 5 9 19 9 19 5"></polyline><polyline
|
|
||||||
points="5 19 5 15 19 15 19 19"
|
|
||||||
></polyline><line
|
|
||||||
x1="4"
|
|
||||||
y1="12"
|
|
||||||
x2="20"
|
|
||||||
y2="12"
|
|
||||||
stroke-dasharray="3 3"
|
|
||||||
stroke-linecap="butt"
|
|
||||||
></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="editor-tabs">
|
<div class="editor-tabs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -468,14 +270,7 @@
|
|||||||
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if mode === 'edit'}
|
{#if mode === 'edit'}
|
||||||
<textarea
|
<MarkdownEditor bind:value={content} />
|
||||||
id="content"
|
|
||||||
name="content"
|
|
||||||
bind:value={content}
|
|
||||||
rows="16"
|
|
||||||
style="min-height: 400px;"
|
|
||||||
placeholder="支持 Markdown 格式..."
|
|
||||||
required></textarea>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="preview-area">
|
<div class="preview-area">
|
||||||
<MarkdownRenderer {content} />
|
<MarkdownRenderer {content} />
|
||||||
@@ -723,39 +518,6 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
||||||
border-radius: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.015);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition:
|
|
||||||
border-color 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
line-height: 1.8;
|
|
||||||
min-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-color, #419b38);
|
|
||||||
box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-color, #419b38);
|
|
||||||
box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-box {
|
.sidebar-box {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -854,101 +616,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 0.65;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-btn:hover {
|
|
||||||
opacity: 1;
|
|
||||||
color: var(--accent-color, #419b38);
|
|
||||||
background-color: rgba(0, 0, 0, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-btn svg {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
stroke: currentColor;
|
|
||||||
stroke-width: 1.5;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-divider {
|
|
||||||
width: 2px;
|
|
||||||
height: 16px;
|
|
||||||
background: var(--text-color);
|
|
||||||
opacity: 0.15;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-dropdown-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 99;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
margin-top: 4px;
|
|
||||||
background: var(--bg-color, #ffffff);
|
|
||||||
border: 1px solid var(--text-color);
|
|
||||||
z-index: 100;
|
|
||||||
width: 140px;
|
|
||||||
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.15);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 8px 12px;
|
|
||||||
text-align: left;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:hover {
|
|
||||||
background: var(--text-color);
|
|
||||||
color: var(--bg-color, #ffffff);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
import type { PageData, ActionData } from './$types';
|
import type { PageData, ActionData } from './$types';
|
||||||
import { deserialize } from '$app/forms';
|
import { deserialize } from '$app/forms';
|
||||||
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
||||||
|
import MarkdownEditor from '$lib/components/admin/MarkdownEditor.svelte';
|
||||||
import FeaturedImagePicker from '$lib/components/admin/FeaturedImagePicker.svelte';
|
import FeaturedImagePicker from '$lib/components/admin/FeaturedImagePicker.svelte';
|
||||||
import SelectDropdown from '$lib/components/admin/SelectDropdown.svelte';
|
import SelectDropdown from '$lib/components/admin/SelectDropdown.svelte';
|
||||||
import { parseHugoMarkdown } from '$lib/hugoParser';
|
import { parseHugoMarkdown } from '$lib/hugoParser';
|
||||||
@@ -122,60 +123,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markdown 工具栏功能
|
|
||||||
function insertAtCursor(before: string, after: string = '', defaultText: string = '') {
|
|
||||||
const textarea = document.getElementById('content') as HTMLTextAreaElement;
|
|
||||||
if (!textarea) return;
|
|
||||||
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const selectedText = textarea.value.substring(start, end);
|
|
||||||
|
|
||||||
const insertion = selectedText ? before + selectedText + after : before + defaultText + after;
|
|
||||||
|
|
||||||
content = textarea.value.substring(0, start) + insertion + textarea.value.substring(end);
|
|
||||||
|
|
||||||
// 恢复焦点并选中文字
|
|
||||||
setTimeout(() => {
|
|
||||||
textarea.focus();
|
|
||||||
if (selectedText) {
|
|
||||||
textarea.setSelectionRange(start, start + insertion.length);
|
|
||||||
} else {
|
|
||||||
textarea.setSelectionRange(
|
|
||||||
start + before.length,
|
|
||||||
start + before.length + defaultText.length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertMore() {
|
|
||||||
insertAtCursor('\n<!--more-->\n', '', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
let showAdmonitionMenu = $state(false);
|
|
||||||
const admonitionTypes = [
|
|
||||||
{ type: 'note', title: '注意', label: '注意 (Note)' },
|
|
||||||
{ type: 'abstract', title: '摘要', label: '摘要 (Abstract)' },
|
|
||||||
{ type: 'info', title: '信息', label: '信息 (Info)' },
|
|
||||||
{ type: 'tip', title: '技巧', label: '技巧 (Tip)' },
|
|
||||||
{ type: 'success', title: '成功', label: '成功 (Success)' },
|
|
||||||
{ type: 'question', title: '问题', label: '问题 (Question)' },
|
|
||||||
{ type: 'warning', title: '警告', label: '警告 (Warning)' },
|
|
||||||
{ type: 'failure', title: '失败', label: '失败 (Failure)' },
|
|
||||||
{ type: 'danger', title: '危险', label: '危险 (Danger)' },
|
|
||||||
{ type: 'bug', title: 'Bug', label: 'Bug' },
|
|
||||||
{ type: 'example', title: '示例', label: '示例 (Example)' },
|
|
||||||
{ type: 'quote', title: '引用', label: '引用 (Quote)' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function insertAdmonition(type: string, title: string) {
|
|
||||||
const openTag = `{{< admonition ${type} "${title}" >}}\n`;
|
|
||||||
const closeTag = `\n{{< /admonition >}}`;
|
|
||||||
insertAtCursor('\n' + openTag, closeTag, '在这里输入提示内容...');
|
|
||||||
showAdmonitionMenu = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleImportMarkdown(e: Event) {
|
async function handleImportMarkdown(e: Event) {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
const file = target.files?.[0];
|
const file = target.files?.[0];
|
||||||
@@ -323,151 +270,6 @@
|
|||||||
<label for="content">正文 *</label>
|
<label for="content">正文 *</label>
|
||||||
|
|
||||||
<div class="editor-header-right">
|
<div class="editor-header-right">
|
||||||
{#if mode === 'edit'}
|
|
||||||
<div class="md-toolbar">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('**', '**', '粗体')}
|
|
||||||
title="加粗"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><path d="M6 4h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path
|
|
||||||
d="M6 12h10a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"
|
|
||||||
></path></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('*', '*', '斜体')}
|
|
||||||
title="斜体"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><line x1="19" y1="4" x2="10" y2="4"></line><line x1="14" y1="20" x2="5" y2="20"
|
|
||||||
></line><line x1="15" y1="4" x2="9" y2="20"></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('~~', '~~', '删除线')}
|
|
||||||
title="删除线"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><path d="M16 4H9a3 3 0 0 0-2.83 4"></path><path d="M14 12a4 4 0 0 1 0 8H6"
|
|
||||||
></path><line x1="4" y1="12" x2="20" y2="12"></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('### ', '', '标题')}
|
|
||||||
title="标题"
|
|
||||||
><svg viewBox="0 0 24 24"><path d="M4 12h16M4 18V6M20 18V6"></path></svg></button
|
|
||||||
>
|
|
||||||
<div class="md-divider"></div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('- ', '', '列表项')}
|
|
||||||
title="无序列表"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"
|
|
||||||
></line><line x1="8" y1="18" x2="21" y2="18"></line><line
|
|
||||||
x1="3"
|
|
||||||
y1="6"
|
|
||||||
x2="3.01"
|
|
||||||
y2="6"
|
|
||||||
></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line
|
|
||||||
x1="3"
|
|
||||||
y1="18"
|
|
||||||
x2="3.01"
|
|
||||||
y2="18"
|
|
||||||
></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('[', '](https://)', '链接文字')}
|
|
||||||
title="链接"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"
|
|
||||||
></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
|
|
||||||
></path></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('', '图片描述')}
|
|
||||||
title="图片"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><polygon
|
|
||||||
points="6 21, 10 13, 13 16.5, 16.5 9.5, 19 21"
|
|
||||||
style="fill: currentColor; stroke: currentColor; stroke-linejoin: round; stroke-width: 1.5;"
|
|
||||||
></polygon></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => insertAtCursor('```\n', '\n```', 'code')}
|
|
||||||
title="代码块"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"
|
|
||||||
></polyline></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
<div class="md-divider"></div>
|
|
||||||
<div class="toolbar-dropdown-wrapper">
|
|
||||||
{#if showAdmonitionMenu}
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
||||||
<div
|
|
||||||
class="dropdown-overlay"
|
|
||||||
onclick={() => (showAdmonitionMenu = false)}
|
|
||||||
role="button"
|
|
||||||
></div>
|
|
||||||
{/if}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="md-btn"
|
|
||||||
onclick={() => (showAdmonitionMenu = !showAdmonitionMenu)}
|
|
||||||
title="提示框 (Admonition)"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"
|
|
||||||
></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
{#if showAdmonitionMenu}
|
|
||||||
<div class="toolbar-dropdown">
|
|
||||||
{#each admonitionTypes as item (item.type)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item"
|
|
||||||
onclick={() => insertAdmonition(item.type, item.title)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<button type="button" class="md-btn" onclick={insertMore} title="插入摘要分割线"
|
|
||||||
><svg viewBox="0 0 24 24"
|
|
||||||
><polyline points="5 5 5 9 19 9 19 5"></polyline><polyline
|
|
||||||
points="5 19 5 15 19 15 19 19"
|
|
||||||
></polyline><line
|
|
||||||
x1="4"
|
|
||||||
y1="12"
|
|
||||||
x2="20"
|
|
||||||
y2="12"
|
|
||||||
stroke-dasharray="3 3"
|
|
||||||
stroke-linecap="butt"
|
|
||||||
></line></svg
|
|
||||||
></button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="editor-tabs">
|
<div class="editor-tabs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -485,14 +287,7 @@
|
|||||||
|
|
||||||
<div class="editor-body">
|
<div class="editor-body">
|
||||||
{#if mode === 'edit'}
|
{#if mode === 'edit'}
|
||||||
<textarea
|
<MarkdownEditor bind:value={content} />
|
||||||
id="content"
|
|
||||||
name="content"
|
|
||||||
bind:value={content}
|
|
||||||
rows="16"
|
|
||||||
style="min-height: 400px;"
|
|
||||||
placeholder="支持 Markdown 格式..."
|
|
||||||
required></textarea>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="preview-area">
|
<div class="preview-area">
|
||||||
<MarkdownRenderer {content} />
|
<MarkdownRenderer {content} />
|
||||||
@@ -754,39 +549,6 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
||||||
border-radius: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.015);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition:
|
|
||||||
border-color 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
line-height: 1.8;
|
|
||||||
min-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-color, #419b38);
|
|
||||||
box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-color, #419b38);
|
|
||||||
box-shadow: 0 0 0 3px rgba(65, 155, 56, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-box {
|
.sidebar-box {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -885,101 +647,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 0.65;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-btn:hover {
|
|
||||||
opacity: 1;
|
|
||||||
color: var(--accent-color, #419b38);
|
|
||||||
background-color: rgba(0, 0, 0, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-btn svg {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
stroke: currentColor;
|
|
||||||
stroke-width: 1.5;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md-divider {
|
|
||||||
width: 2px;
|
|
||||||
height: 16px;
|
|
||||||
background: var(--text-color);
|
|
||||||
opacity: 0.15;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-dropdown-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 99;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
margin-top: 4px;
|
|
||||||
background: var(--bg-color, #ffffff);
|
|
||||||
border: 1px solid var(--text-color);
|
|
||||||
z-index: 100;
|
|
||||||
width: 140px;
|
|
||||||
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.15);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
padding: 8px 12px;
|
|
||||||
text-align: left;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:hover {
|
|
||||||
background: var(--text-color);
|
|
||||||
color: var(--bg-color, #ffffff);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user