feat: 完善内容管理通用富文本编辑器
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||||
html { scroll-behavior: smooth; background: var(--paper); }
|
html { scroll-behavior: smooth; background: var(--paper); }
|
||||||
body { margin: 0; color: var(--ink); font-family: var(--sans); font-size: 16px; line-height: 1.65; background: var(--paper); -webkit-font-smoothing: antialiased; }
|
body { margin: 0; color: var(--ink); font-family: var(--sans); font-size: 16px; line-height: 1.65; background: var(--paper); -webkit-font-smoothing: antialiased; }
|
||||||
body.nav-open { overflow: hidden; }
|
body.nav-open { overflow: hidden; }
|
||||||
@@ -40,9 +41,11 @@ img { max-width: 100%; }
|
|||||||
.section-heading.centered { max-width: 780px; margin: 0 auto 56px; text-align: center; }
|
.section-heading.centered { max-width: 780px; margin: 0 auto 56px; text-align: center; }
|
||||||
|
|
||||||
.site-header { position: absolute; z-index: 20; top: 0; left: 0; display: grid; grid-template-columns: 250px 1fr 160px; align-items: center; width: 100%; padding: 26px 4.2vw; }
|
.site-header { position: absolute; z-index: 20; top: 0; left: 0; display: grid; grid-template-columns: 250px 1fr 160px; align-items: center; width: 100%; padding: 26px 4.2vw; }
|
||||||
.main-nav { display: flex; justify-content: center; gap: clamp(24px, 3.2vw, 54px); }
|
.main-nav { display: flex; justify-content: center; gap: clamp(18px, 2.4vw, 42px); }
|
||||||
.main-nav a { position: relative; display: inline-flex; align-items: center; min-height: 44px; padding: 0 4px; color: #17364e; font-size: 14px; white-space: nowrap; }
|
.main-nav a { position: relative; display: inline-flex; flex-direction: column; align-items: center; justify-content: center; min-height: 54px; padding: 0 3px; color: #17364e; white-space: nowrap; }
|
||||||
.main-nav a::after { position: absolute; right: 50%; bottom: -12px; left: 50%; height: 1px; content: ""; background: var(--ink); transition: .28s ease; }
|
.nav-label { font-size: 14px; line-height: 1.2; }
|
||||||
|
.nav-english { margin-top: 5px; color: #71889b; font-size: 9px; font-weight: 500; line-height: 1; letter-spacing: .12em; }
|
||||||
|
.main-nav a::after { position: absolute; right: 50%; bottom: -8px; left: 50%; height: 1px; content: ""; background: var(--ink); transition: .28s ease; }
|
||||||
.main-nav a:hover::after, .main-nav a:focus-visible::after, .main-nav a[aria-current="page"]::after { right: 0; left: 0; }
|
.main-nav a:hover::after, .main-nav a:focus-visible::after, .main-nav a[aria-current="page"]::after { right: 0; left: 0; }
|
||||||
.header-cta, .primary-button { position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 11px; overflow: hidden; border-radius: 999px; color: white; background: linear-gradient(135deg, #2b5f8b, #0a3456); box-shadow: 0 12px 28px rgba(10, 52, 86, .2); isolation: isolate; }
|
.header-cta, .primary-button { position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 11px; overflow: hidden; border-radius: 999px; color: white; background: linear-gradient(135deg, #2b5f8b, #0a3456); box-shadow: 0 12px 28px rgba(10, 52, 86, .2); isolation: isolate; }
|
||||||
.header-cta::before, .primary-button::before { position: absolute; top: -80%; left: -45%; z-index: -1; width: 32%; height: 260%; content: ""; background: linear-gradient(90deg, transparent, rgba(255,255,255,.32), transparent); transform: rotate(20deg) translateX(-260%); transition: transform .65s cubic-bezier(.2,.7,.2,1); }
|
.header-cta::before, .primary-button::before { position: absolute; top: -80%; left: -45%; z-index: -1; width: 32%; height: 260%; content: ""; background: linear-gradient(90deg, transparent, rgba(255,255,255,.32), transparent); transform: rotate(20deg) translateX(-260%); transition: transform .65s cubic-bezier(.2,.7,.2,1); }
|
||||||
@@ -187,6 +190,20 @@ img { max-width: 100%; }
|
|||||||
.story-body .lead { margin-bottom: 30px; color: #315a7a; font: 400 22px/1.8 var(--serif); }
|
.story-body .lead { margin-bottom: 30px; color: #315a7a; font: 400 22px/1.8 var(--serif); }
|
||||||
.long-copy { color: #516b80; font-size: 16px; line-height: 2.15; white-space: pre-line; }
|
.long-copy { color: #516b80; font-size: 16px; line-height: 2.15; white-space: pre-line; }
|
||||||
.story-body .primary-button { min-height: 50px; margin-top: 38px; padding: 0 28px; }
|
.story-body .primary-button { min-height: 50px; margin-top: 38px; padding: 0 28px; }
|
||||||
|
.rich-text-content { color: #516b80; font-size: 16px; line-height: 2.15; overflow-wrap: anywhere; }
|
||||||
|
.rich-text-content > :first-child { margin-top: 0; }
|
||||||
|
.rich-text-content > :last-child { margin-bottom: 0; }
|
||||||
|
.rich-text-content p { margin: 0 0 1.2em; }
|
||||||
|
.rich-text-content h2, .rich-text-content h3 { color: var(--ink); font-family: var(--serif); font-weight: 500; line-height: 1.45; }
|
||||||
|
.rich-text-content h2 { margin: 1.7em 0 .7em; font-size: 30px; }
|
||||||
|
.rich-text-content h3 { margin: 1.5em 0 .6em; font-size: 23px; }
|
||||||
|
.rich-text-content ul, .rich-text-content ol { margin: 1.1em 0; padding-left: 1.7em; }
|
||||||
|
.rich-text-content li { margin: .45em 0; }
|
||||||
|
.rich-text-content blockquote { margin: 1.5em 0; padding: 15px 22px; border-left: 3px solid #7b9ab2; color: #426782; background: #f3f7fa; }
|
||||||
|
.rich-text-content a { color: #286895; text-decoration: underline; text-underline-offset: 3px; }
|
||||||
|
.rich-text-content hr { margin: 2em 0; border: 0; border-top: 1px solid var(--line); }
|
||||||
|
.rich-text-content img { display: block; max-width: 100%; height: auto; margin: 1.4em 0; border-radius: 10px; }
|
||||||
|
.rich-text-content pre { overflow-x: auto; padding: 16px 18px; border-radius: 10px; color: #edf4f8; background: #17364e; font: 13px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||||
.statement-page { max-width: 900px; }
|
.statement-page { max-width: 900px; }
|
||||||
.statement-page header { padding: 66px 30px; border-radius: 34px; background: rgba(255,255,255,.7); box-shadow: var(--shadow); }
|
.statement-page header { padding: 66px 30px; border-radius: 34px; background: rgba(255,255,255,.7); box-shadow: var(--shadow); }
|
||||||
.detail-hero { display: grid; grid-template-columns: 1.05fr .95fr; align-items: center; max-width: 1260px; min-height: 620px; margin: 0 auto; padding: 54px 32px 80px; }
|
.detail-hero { display: grid; grid-template-columns: 1.05fr .95fr; align-items: center; max-width: 1260px; min-height: 620px; margin: 0 auto; padding: 54px 32px 80px; }
|
||||||
@@ -306,6 +323,33 @@ img { max-width: 100%; }
|
|||||||
.content-list > button small { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
|
.content-list > button small { overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.content-list .add-content { display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 5px; border: 1px dashed #aac0d2; color: #35698f; }
|
.content-list .add-content { display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 5px; border: 1px dashed #aac0d2; color: #35698f; }
|
||||||
.content-editor { padding: 28px; }
|
.content-editor { padding: 28px; }
|
||||||
|
.field-group { display: grid; gap: 8px; color: #38566e; font-size: 13px; }
|
||||||
|
.form-grid .field-group.full { grid-column: 1 / 3; }
|
||||||
|
.rich-text-editor { border: 1px solid #d5dfe7; border-radius: 12px; background: #fbfcfd; }
|
||||||
|
.rich-text-editor:focus-within { border-color: #6d96b7; box-shadow: 0 0 0 3px rgba(78,128,168,.08); }
|
||||||
|
.rich-text-toolbar { position: sticky; z-index: 5; top: 95px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 10px; border-bottom: 1px solid #dbe4eb; border-radius: 11px 11px 0 0; background: #f4f7fa; }
|
||||||
|
.rich-text-toolbar button, .rich-text-toolbar select, .toolbar-color { display: inline-flex; align-items: center; justify-content: center; min-height: 34px; padding: 0 10px; border: 1px solid #cfdbe5; border-radius: 7px; color: #294d68; background: white; font-size: 12px; cursor: pointer; }
|
||||||
|
.rich-text-toolbar select { width: auto; max-width: 160px; }
|
||||||
|
.rich-text-toolbar button { min-width: 34px; }
|
||||||
|
.rich-text-toolbar button:hover, .rich-text-toolbar button.active, .toolbar-color:hover { border-color: #7396b3; color: #164d77; background: #e4eef7; }
|
||||||
|
.rich-text-toolbar button:disabled { opacity: .4; cursor: not-allowed; }
|
||||||
|
.toolbar-divider { width: 1px; height: 25px; margin: 0 2px; background: #cfdbe5; }
|
||||||
|
.toolbar-color { position: relative; width: 34px; padding: 0; border-bottom: 4px solid #17364e; font-weight: 700; }
|
||||||
|
.toolbar-color.highlight { border-bottom-color: #fff1c9; background: #fff9e8; }
|
||||||
|
.toolbar-color input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||||
|
.rich-text-editor .tiptap { min-height: 420px; padding: 24px; outline: 0; color: #26465f; font: 400 16px/1.85 var(--sans); }
|
||||||
|
.rich-text-editor .tiptap > :first-child { margin-top: 0; }
|
||||||
|
.rich-text-editor .tiptap h2, .rich-text-editor .tiptap h3 { font-family: var(--serif); font-weight: 500; }
|
||||||
|
.rich-text-editor .tiptap h2 { font-size: 28px; }
|
||||||
|
.rich-text-editor .tiptap h3 { font-size: 22px; }
|
||||||
|
.rich-text-editor .tiptap blockquote { margin: 18px 0; padding: 12px 18px; border-left: 3px solid #7b9ab2; background: #edf3f7; }
|
||||||
|
.rich-text-editor .tiptap a { color: #286895; text-decoration: underline; }
|
||||||
|
.rich-text-editor .tiptap img { display: block; max-width: 100%; height: auto; margin: 18px 0; border-radius: 10px; }
|
||||||
|
.rich-text-editor .tiptap pre { overflow-x: auto; margin: 18px 0; padding: 16px 18px; border-radius: 10px; color: #edf4f8; background: #17364e; font: 13px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||||
|
.rich-text-editor .tiptap hr { margin: 24px 0; border: 0; border-top: 1px solid #cbd8e2; }
|
||||||
|
.rich-text-editor .tiptap p.is-editor-empty:first-child::before { float: left; height: 0; color: #98aaba; content: "在此输入正文,可像 Word 一样选择文字后设置格式…"; pointer-events: none; }
|
||||||
|
.rich-text-message { margin: 0; padding: 8px 12px; color: #a84d43; background: #fff3f1; font-size: 12px; }
|
||||||
|
.rich-text-footer { display: flex; justify-content: space-between; gap: 20px; padding: 9px 12px; border-top: 1px solid #e2e9ef; color: #7890a3; font-size: 11px; }
|
||||||
.danger-link, .inline-add { display: inline-flex; align-items: center; gap: 6px; padding: 8px 11px; border: 0; border-radius: 8px; color: #9b4b43; background: #fff1ef; cursor: pointer; }
|
.danger-link, .inline-add { display: inline-flex; align-items: center; gap: 6px; padding: 8px 11px; border: 0; border-radius: 8px; color: #9b4b43; background: #fff1ef; cursor: pointer; }
|
||||||
.inline-add { color: #35698f; background: #e9f1f8; }
|
.inline-add { color: #35698f; background: #e9f1f8; }
|
||||||
.form-grid select { width: 100%; padding: 12px 13px; border: 1px solid #d5dfe7; border-radius: 9px; color: #1c3c56; background: #fbfcfd; }
|
.form-grid select { width: 100%; padding: 12px 13px; border: 1px solid #d5dfe7; border-radius: 9px; color: #1c3c56; background: #fbfcfd; }
|
||||||
@@ -380,12 +424,14 @@ img { max-width: 100%; }
|
|||||||
@keyframes panel-wipe { from { opacity: .4; clip-path: inset(0 100% 0 0 round 38px); transform: translateY(22px); } to { opacity: 1; clip-path: inset(0 0 0 0 round 38px); transform: none; } }
|
@keyframes panel-wipe { from { opacity: .4; clip-path: inset(0 100% 0 0 round 38px); transform: translateY(22px); } to { opacity: 1; clip-path: inset(0 0 0 0 round 38px); transform: none; } }
|
||||||
@keyframes float-cue { 0%,100% { transform: translate(-50%, 0); } 50% { transform: translate(-50%, 7px); } }
|
@keyframes float-cue { 0%,100% { transform: translate(-50%, 0); } 50% { transform: translate(-50%, 7px); } }
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1300px) {
|
||||||
.site-header { position: fixed; grid-template-columns: 1fr auto; min-height: 72px; padding: 12px 24px; border-bottom: 1px solid rgba(28,62,88,.08); background: rgba(248,250,252,.9); backdrop-filter: blur(18px); }
|
.site-header { position: fixed; grid-template-columns: 1fr auto; min-height: 72px; padding: 12px 24px; border-bottom: 1px solid rgba(28,62,88,.08); background: rgba(248,250,252,.9); backdrop-filter: blur(18px); }
|
||||||
.menu-toggle { z-index: 3; display: inline-flex; padding: 10px; cursor: pointer; }
|
.menu-toggle { z-index: 3; display: inline-flex; padding: 10px; cursor: pointer; }
|
||||||
.main-nav { position: fixed; top: 0; left: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100vw; height: 100svh; gap: 28px; visibility: hidden; opacity: 0; background: rgba(247,249,251,.97); transition: visibility .28s ease, opacity .28s ease; }
|
.main-nav { position: fixed; top: 0; left: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100vw; height: 100svh; gap: 20px; visibility: hidden; opacity: 0; background: rgba(247,249,251,.97); transition: visibility .28s ease, opacity .28s ease; }
|
||||||
.main-nav.open { visibility: visible; opacity: 1; }
|
.main-nav.open { visibility: visible; opacity: 1; }
|
||||||
.main-nav a { display: flex; min-width: 180px; min-height: 48px; justify-content: center; font: 500 24px var(--serif); }
|
.main-nav a { display: flex; min-width: 220px; min-height: 56px; justify-content: center; }
|
||||||
|
.main-nav .nav-label { font: 500 23px/1.15 var(--serif); }
|
||||||
|
.main-nav .nav-english { margin-top: 6px; font: 500 9px/1 var(--sans); letter-spacing: .18em; }
|
||||||
.header-cta { display: none; }
|
.header-cta { display: none; }
|
||||||
.teacher-card { grid-template-columns: .9fr 1.1fr; }
|
.teacher-card { grid-template-columns: .9fr 1.1fr; }
|
||||||
.official-grid { grid-template-columns: repeat(2, 1fr); }
|
.official-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
@@ -482,6 +528,10 @@ img { max-width: 100%; }
|
|||||||
.editor-panel { padding: 20px 16px; }
|
.editor-panel { padding: 20px 16px; }
|
||||||
.form-grid { grid-template-columns: 1fr; }
|
.form-grid { grid-template-columns: 1fr; }
|
||||||
.form-grid label.full { grid-column: 1; }
|
.form-grid label.full { grid-column: 1; }
|
||||||
|
.form-grid .field-group.full { grid-column: 1; }
|
||||||
|
.rich-text-toolbar { gap: 5px; }
|
||||||
|
.rich-text-toolbar select { max-width: 118px; }
|
||||||
|
.rich-text-editor .tiptap { min-height: 340px; padding: 18px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
|
|||||||
10
app/components/RichTextContent.vue
Normal file
10
app/components/RichTextContent.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { toDisplayHtml } from '~/utils/rich-text'
|
||||||
|
|
||||||
|
const props = defineProps<{ content: string }>()
|
||||||
|
const html = computed(() => toDisplayHtml(props.content))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rich-text-content" v-html="html" />
|
||||||
|
</template>
|
||||||
183
app/components/RichTextEditor.vue
Normal file
183
app/components/RichTextEditor.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { TextAlign } from '@tiptap/extension-text-align'
|
||||||
|
import { TextStyleKit } from '@tiptap/extension-text-style'
|
||||||
|
import Image from '@tiptap/extension-image'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { EditorContent, useEditor } from '@tiptap/vue-3'
|
||||||
|
import { toEditorHtml } from '~/utils/rich-text'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: string }>()
|
||||||
|
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||||
|
const linkMessage = ref('')
|
||||||
|
const imageInput = ref<HTMLInputElement>()
|
||||||
|
const imageMessage = ref('')
|
||||||
|
const imageBusy = ref(false)
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
content: toEditorHtml(props.modelValue),
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
heading: { levels: [2, 3] },
|
||||||
|
link: {
|
||||||
|
openOnClick: false,
|
||||||
|
autolink: true,
|
||||||
|
HTMLAttributes: { target: '_blank', rel: 'noopener noreferrer' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
||||||
|
TextStyleKit,
|
||||||
|
Image.configure({ allowBase64: false }),
|
||||||
|
],
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: 'rich-text-surface',
|
||||||
|
'aria-label': '正文编辑区域',
|
||||||
|
spellcheck: 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onUpdate: ({ editor: instance }) => {
|
||||||
|
emit('update:modelValue', instance.isEmpty ? '' : instance.getHTML())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (value) => {
|
||||||
|
if (!editor.value) return
|
||||||
|
const next = toEditorHtml(value)
|
||||||
|
if (editor.value.getHTML() !== next) editor.value.commands.setContent(next, { emitUpdate: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectBlock(event: Event) {
|
||||||
|
const value = (event.target as HTMLSelectElement).value
|
||||||
|
if (value === 'paragraph') editor.value?.chain().focus().setParagraph().run()
|
||||||
|
else editor.value?.chain().focus().toggleHeading({ level: Number(value) as 2 | 3 }).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFontFamily(event: Event) {
|
||||||
|
const value = (event.target as HTMLSelectElement).value
|
||||||
|
if (value) editor.value?.chain().focus().setFontFamily(value).run()
|
||||||
|
else editor.value?.chain().focus().unsetFontFamily().run()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectFontSize(event: Event) {
|
||||||
|
const value = (event.target as HTMLSelectElement).value
|
||||||
|
if (value) editor.value?.chain().focus().setFontSize(value).run()
|
||||||
|
else editor.value?.chain().focus().unsetFontSize().run()
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLineHeight(event: Event) {
|
||||||
|
const value = (event.target as HTMLSelectElement).value
|
||||||
|
if (value) editor.value?.chain().focus().setLineHeight(value).run()
|
||||||
|
else editor.value?.chain().focus().unsetLineHeight().run()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColor(event: Event, background = false) {
|
||||||
|
const value = (event.target as HTMLInputElement).value
|
||||||
|
const chain = editor.value?.chain().focus()
|
||||||
|
if (background) chain?.setBackgroundColor(value).run()
|
||||||
|
else chain?.setColor(value).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLink() {
|
||||||
|
const previous = editor.value?.getAttributes('link').href || ''
|
||||||
|
const href = window.prompt('请输入链接地址(http、https、mailto 或 tel)', previous)
|
||||||
|
if (href === null) return
|
||||||
|
if (!href.trim()) {
|
||||||
|
editor.value?.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!/^(https?:\/\/|mailto:|tel:)/i.test(href.trim())) {
|
||||||
|
linkMessage.value = '链接需以 http://、https://、mailto: 或 tel: 开头'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
linkMessage.value = ''
|
||||||
|
editor.value?.chain().focus().extendMarkRange('link').setLink({ href: href.trim(), target: '_blank', rel: 'noopener noreferrer' }).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertImageUrl() {
|
||||||
|
const src = window.prompt('请输入图片地址(建议使用素材库中的图片地址)')
|
||||||
|
if (!src?.trim()) return
|
||||||
|
if (!/^https?:\/\//i.test(src.trim()) && !src.trim().startsWith('/')) {
|
||||||
|
imageMessage.value = '图片地址需以 http://、https:// 或 / 开头'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imageMessage.value = ''
|
||||||
|
editor.value?.chain().focus().setImage({ src: src.trim() }).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(event: Event) {
|
||||||
|
const file = (event.target as HTMLInputElement).files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
imageBusy.value = true
|
||||||
|
imageMessage.value = ''
|
||||||
|
try {
|
||||||
|
const body = new FormData()
|
||||||
|
body.append('file', file)
|
||||||
|
const result = await $fetch<{ url: string }>('/api/admin/media', { method: 'POST', body })
|
||||||
|
editor.value?.chain().focus().setImage({ src: result.url, alt: file.name }).run()
|
||||||
|
} catch (error: any) {
|
||||||
|
imageMessage.value = error?.data?.statusMessage || '图片上传失败,请检查图片格式和大小'
|
||||||
|
} finally {
|
||||||
|
imageBusy.value = false
|
||||||
|
;(event.target as HTMLInputElement).value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rich-text-editor">
|
||||||
|
<div v-if="editor" class="rich-text-toolbar" role="toolbar" aria-label="正文排版工具栏">
|
||||||
|
<select aria-label="段落样式" title="段落样式" @change="selectBlock">
|
||||||
|
<option value="paragraph">正文</option>
|
||||||
|
<option value="2">二级标题</option>
|
||||||
|
<option value="3">三级标题</option>
|
||||||
|
</select>
|
||||||
|
<select aria-label="字体" title="字体" @change="selectFontFamily">
|
||||||
|
<option value="">默认字体</option>
|
||||||
|
<option value="Noto Serif SC, serif">宋体风格</option>
|
||||||
|
<option value="Noto Sans SC, sans-serif">黑体风格</option>
|
||||||
|
<option value="Arial, sans-serif">Arial</option>
|
||||||
|
</select>
|
||||||
|
<select aria-label="字号" title="字号" @change="selectFontSize">
|
||||||
|
<option value="">默认字号</option>
|
||||||
|
<option v-for="size in [14,16,18,20,24,28,32]" :key="size" :value="`${size}px`">{{ size }} px</option>
|
||||||
|
</select>
|
||||||
|
<select aria-label="行距" title="行距" @change="selectLineHeight">
|
||||||
|
<option value="">默认行距</option>
|
||||||
|
<option value="1.5">1.5 倍</option>
|
||||||
|
<option value="1.75">1.75 倍</option>
|
||||||
|
<option value="2">2 倍</option>
|
||||||
|
<option value="2.25">2.25 倍</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<span class="toolbar-divider" />
|
||||||
|
<button type="button" :class="{ active: editor.isActive('bold') }" title="加粗" aria-label="加粗" @click="editor.chain().focus().toggleBold().run()"><b>B</b></button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive('italic') }" title="斜体" aria-label="斜体" @click="editor.chain().focus().toggleItalic().run()"><i>I</i></button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive('underline') }" title="下划线" aria-label="下划线" @click="editor.chain().focus().toggleUnderline().run()"><u>U</u></button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive('strike') }" title="删除线" aria-label="删除线" @click="editor.chain().focus().toggleStrike().run()"><s>S</s></button>
|
||||||
|
<label class="toolbar-color" title="文字颜色">A<input type="color" value="#17364e" aria-label="文字颜色" @input="setColor($event)"></label>
|
||||||
|
<label class="toolbar-color highlight" title="背景颜色">A<input type="color" value="#fff1c9" aria-label="背景颜色" @input="setColor($event, true)"></label>
|
||||||
|
|
||||||
|
<span class="toolbar-divider" />
|
||||||
|
<button type="button" :class="{ active: editor.isActive({ textAlign: 'left' }) }" title="左对齐" aria-label="左对齐" @click="editor.chain().focus().setTextAlign('left').run()">左</button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive({ textAlign: 'center' }) }" title="居中对齐" aria-label="居中对齐" @click="editor.chain().focus().setTextAlign('center').run()">中</button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive({ textAlign: 'right' }) }" title="右对齐" aria-label="右对齐" @click="editor.chain().focus().setTextAlign('right').run()">右</button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive({ textAlign: 'justify' }) }" title="两端对齐" aria-label="两端对齐" @click="editor.chain().focus().setTextAlign('justify').run()">齐</button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive('bulletList') }" title="项目符号" aria-label="项目符号" @click="editor.chain().focus().toggleBulletList().run()">• 列表</button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive('orderedList') }" title="编号列表" aria-label="编号列表" @click="editor.chain().focus().toggleOrderedList().run()">1. 列表</button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive('blockquote') }" title="引用" aria-label="引用" @click="editor.chain().focus().toggleBlockquote().run()">引用</button>
|
||||||
|
<button type="button" :class="{ active: editor.isActive('link') }" title="添加或编辑链接" aria-label="添加或编辑链接" @click="setLink">链接</button>
|
||||||
|
<button type="button" title="插入图片地址" aria-label="插入图片地址" @click="insertImageUrl">图片地址</button>
|
||||||
|
<button type="button" title="上传图片" aria-label="上传图片" :disabled="imageBusy" @click="imageInput?.click()">{{ imageBusy ? '上传中…' : '上传图片' }}</button>
|
||||||
|
<input ref="imageInput" class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp,image/gif" @change="uploadImage">
|
||||||
|
<button type="button" :class="{ active: editor.isActive('codeBlock') }" title="代码块" aria-label="代码块" @click="editor.chain().focus().toggleCodeBlock().run()">代码</button>
|
||||||
|
<button type="button" title="分隔线" aria-label="分隔线" @click="editor.chain().focus().setHorizontalRule().run()">分隔线</button>
|
||||||
|
<button type="button" title="清除格式" aria-label="清除格式" @click="editor.chain().focus().clearNodes().unsetAllMarks().run()">清除格式</button>
|
||||||
|
<button type="button" title="撤销" aria-label="撤销" :disabled="!editor.can().undo()" @click="editor.chain().focus().undo().run()">↶</button>
|
||||||
|
<button type="button" title="重做" aria-label="重做" :disabled="!editor.can().redo()" @click="editor.chain().focus().redo().run()">↷</button>
|
||||||
|
</div>
|
||||||
|
<EditorContent :editor="editor" />
|
||||||
|
<p v-if="linkMessage" class="rich-text-message">{{ linkMessage }}</p>
|
||||||
|
<p v-if="imageMessage" class="rich-text-message">{{ imageMessage }}</p>
|
||||||
|
<div class="rich-text-footer"><span>支持从 Word 粘贴,发布时会自动清理不安全代码</span><span>{{ modelValue.length }} 字符</span></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -6,16 +6,17 @@ const toggle = ref<HTMLButtonElement>()
|
|||||||
const navigation = ref<HTMLElement>()
|
const navigation = ref<HTMLElement>()
|
||||||
const props = withDefaults(defineProps<{ logo?: string; brandName?: string; englishName?: string }>(), {
|
const props = withDefaults(defineProps<{ logo?: string; brandName?: string; englishName?: string }>(), {
|
||||||
brandName: '慧遇书院',
|
brandName: '慧遇书院',
|
||||||
|
englishName: 'HUIYU ACADEMY',
|
||||||
})
|
})
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const links = computed(() => [
|
const links = computed(() => [
|
||||||
{ label: '首页', href: '/#home' },
|
{ label: '首页', englishLabel: 'HOME', href: '/#home' },
|
||||||
{ label: `关于${props.brandName}`, href: '/#about' },
|
{ label: `关于${props.brandName}`, englishLabel: `ABOUT ${props.englishName}`, href: '/#about' },
|
||||||
{ label: '卢慧老师', href: '/luhui' },
|
{ label: '卢慧老师', englishLabel: 'LU HUI', href: '/luhui' },
|
||||||
{ label: '课程与产品', href: '/products' },
|
{ label: '课程与产品', englishLabel: 'COURSES & PRODUCTS', href: '/products' },
|
||||||
{ label: '官方信息', href: '/#official' },
|
{ label: '官方信息', englishLabel: 'OFFICIAL INFO', href: '/#official' },
|
||||||
{ label: '品牌动态', href: '/news' },
|
{ label: '品牌动态', englishLabel: 'BRAND NEWS', href: '/news' },
|
||||||
{ label: '联系我们', href: '/#contact' },
|
{ label: '联系我们', englishLabel: 'CONTACT US', href: '/#contact' },
|
||||||
])
|
])
|
||||||
|
|
||||||
function closeMenu(returnFocus = false) {
|
function closeMenu(returnFocus = false) {
|
||||||
@@ -47,7 +48,10 @@ onBeforeUnmount(() => {
|
|||||||
<List v-else :size="24" />
|
<List v-else :size="24" />
|
||||||
</button>
|
</button>
|
||||||
<nav id="site-navigation" ref="navigation" :class="['main-nav', { open }]" aria-label="主导航" @keydown.esc="closeMenu(true)">
|
<nav id="site-navigation" ref="navigation" :class="['main-nav', { open }]" aria-label="主导航" @keydown.esc="closeMenu(true)">
|
||||||
<a v-for="link in links" :key="link.href" :href="link.href" :aria-current="isActive(link.href) ? 'page' : undefined" @click="closeMenu()">{{ link.label }}</a>
|
<a v-for="link in links" :key="link.href" :href="link.href" :aria-current="isActive(link.href) ? 'page' : undefined" @click="closeMenu()">
|
||||||
|
<span class="nav-label">{{ link.label }}</span>
|
||||||
|
<span class="nav-english" lang="en">{{ link.englishLabel }}</span>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
<a class="header-cta" href="/#official">
|
<a class="header-cta" href="/#official">
|
||||||
官方渠道
|
官方渠道
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { apiErrorMessage } from '~/composables/useUnsavedChanges'
|
|||||||
import type { SiteDocument } from '../../../server/utils/site-schema'
|
import type { SiteDocument } from '../../../server/utils/site-schema'
|
||||||
|
|
||||||
definePageMeta({ layout: false })
|
definePageMeta({ layout: false })
|
||||||
useHead(() => ({ title: `内容管理|${document.value?.brand.name || '慧遇书院'}` }))
|
|
||||||
|
|
||||||
type ContentTab = 'products' | 'articles' | 'team'
|
type ContentTab = 'products' | 'articles' | 'team'
|
||||||
const tab = ref<ContentTab>('products')
|
const tab = ref<ContentTab>('products')
|
||||||
@@ -14,6 +13,7 @@ const loading = ref(true)
|
|||||||
const busy = ref(false)
|
const busy = ref(false)
|
||||||
const message = ref('')
|
const message = ref('')
|
||||||
const { dirty, markClean } = useUnsavedChanges(document)
|
const { dirty, markClean } = useUnsavedChanges(document)
|
||||||
|
useHead(() => ({ title: `内容管理|${document.value?.brand.name || '慧遇书院'}` }))
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -123,21 +123,21 @@ async function uploadImage(event: Event, apply: (url: string) => void) {
|
|||||||
<div v-if="tab === 'products' && currentProduct" class="form-grid">
|
<div v-if="tab === 'products' && currentProduct" class="form-grid">
|
||||||
<label>标题<input v-model="currentProduct.title" maxlength="60"></label><label>英文标识<input v-model="currentProduct.slug" placeholder="family-course"></label>
|
<label>标题<input v-model="currentProduct.title" maxlength="60"></label><label>英文标识<input v-model="currentProduct.slug" placeholder="family-course"></label>
|
||||||
<label>类型<select v-model="currentProduct.type"><option value="course">课程</option><option value="service">服务</option><option value="tool">工具</option></select></label><label>状态<select v-model="currentProduct.status"><option value="draft">草稿</option><option value="published">已发布</option><option value="offline">已下线</option></select></label>
|
<label>类型<select v-model="currentProduct.type"><option value="course">课程</option><option value="service">服务</option><option value="tool">工具</option></select></label><label>状态<select v-model="currentProduct.status"><option value="draft">草稿</option><option value="published">已发布</option><option value="offline">已下线</option></select></label>
|
||||||
<label class="full">副标题<input v-model="currentProduct.subtitle"></label><label class="full">摘要<textarea v-model="currentProduct.summary" rows="3" /></label><label class="full">正文<textarea v-model="currentProduct.content" rows="12" /></label>
|
<label class="full">副标题<input v-model="currentProduct.subtitle"></label><label class="full">摘要<textarea v-model="currentProduct.summary" rows="3" /></label><div class="full field-group"><span>正文</span><RichTextEditor v-model="currentProduct.content" /></div>
|
||||||
<label class="full">封面地址<input v-model="currentProduct.image"><span class="compact-upload"><input type="file" accept="image/png,image/jpeg,image/webp" @change="uploadImage($event, url => currentProduct!.image = url)">上传本地图片</span><MediaPicker v-model="currentProduct.image" /></label>
|
<label class="full">封面地址<input v-model="currentProduct.image"><span class="compact-upload"><input type="file" accept="image/png,image/jpeg,image/webp" @change="uploadImage($event, url => currentProduct!.image = url)">上传本地图片</span><MediaPicker v-model="currentProduct.image" /></label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'articles' && currentArticle" class="form-grid">
|
<div v-else-if="tab === 'articles' && currentArticle" class="form-grid">
|
||||||
<label>标题<input v-model="currentArticle.title"></label><label>英文标识<input v-model="currentArticle.slug"></label>
|
<label>标题<input v-model="currentArticle.title"></label><label>英文标识<input v-model="currentArticle.slug"></label>
|
||||||
<label>类型<select v-model="currentArticle.type"><option value="news">品牌动态</option><option value="statement">官方声明</option><option value="media">媒体报道</option></select></label><label>状态<select v-model="currentArticle.status"><option value="draft">草稿</option><option value="published">已发布</option><option value="offline">已下线</option></select></label>
|
<label>类型<select v-model="currentArticle.type"><option value="news">品牌动态</option><option value="statement">官方声明</option><option value="media">媒体报道</option></select></label><label>状态<select v-model="currentArticle.status"><option value="draft">草稿</option><option value="published">已发布</option><option value="offline">已下线</option></select></label>
|
||||||
<label>发布日期<input v-model="currentArticle.publishedAt" type="date"></label><label>声明编号<input v-model="currentArticle.statementNo"></label>
|
<label>发布日期<input v-model="currentArticle.publishedAt" type="date"></label><label>声明编号<input v-model="currentArticle.statementNo"></label>
|
||||||
<label class="full">摘要<textarea v-model="currentArticle.summary" rows="3" /></label><label class="full">正文<textarea v-model="currentArticle.content" rows="14" /></label>
|
<label class="full">摘要<textarea v-model="currentArticle.summary" rows="3" /></label><div class="full field-group"><span>正文</span><RichTextEditor v-model="currentArticle.content" /></div>
|
||||||
<label>来源名称<input v-model="currentArticle.sourceName"></label><label>来源链接<input v-model="currentArticle.sourceUrl"></label>
|
<label>来源名称<input v-model="currentArticle.sourceName"></label><label>来源链接<input v-model="currentArticle.sourceUrl"></label>
|
||||||
<label class="full">封面地址<input v-model="currentArticle.cover"><span class="compact-upload"><input type="file" accept="image/png,image/jpeg,image/webp" @change="uploadImage($event, url => currentArticle!.cover = url)">上传本地图片</span><MediaPicker v-model="currentArticle.cover" /></label>
|
<label class="full">封面地址<input v-model="currentArticle.cover"><span class="compact-upload"><input type="file" accept="image/png,image/jpeg,image/webp" @change="uploadImage($event, url => currentArticle!.cover = url)">上传本地图片</span><MediaPicker v-model="currentArticle.cover" /></label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'team' && currentMember" class="form-grid">
|
<div v-else-if="tab === 'team' && currentMember" class="form-grid">
|
||||||
<label>姓名<input v-model="currentMember.name"></label><label>英文标识<input v-model="currentMember.slug"></label>
|
<label>姓名<input v-model="currentMember.name"></label><label>英文标识<input v-model="currentMember.slug"></label>
|
||||||
<label>角色<input v-model="currentMember.role"></label><label>状态<select v-model="currentMember.status"><option value="draft">草稿</option><option value="published">已发布</option><option value="offline">已下线</option></select></label>
|
<label>角色<input v-model="currentMember.role"></label><label>状态<select v-model="currentMember.status"><option value="draft">草稿</option><option value="published">已发布</option><option value="offline">已下线</option></select></label>
|
||||||
<label class="full">简介<textarea v-model="currentMember.summary" rows="5" /></label><label class="full">专长(用中文逗号分隔)<input :value="currentMember.specialties.join(',')" @input="currentMember.specialties = ($event.target as HTMLInputElement).value.split(/[,,]/).map(v => v.trim()).filter(Boolean).slice(0, 8)"></label>
|
<div class="full field-group"><span>简介</span><RichTextEditor v-model="currentMember.summary" /></div><label class="full">专长(用中文逗号分隔)<input :value="currentMember.specialties.join(',')" @input="currentMember.specialties = ($event.target as HTMLInputElement).value.split(/[,,]/).map(v => v.trim()).filter(Boolean).slice(0, 8)"></label>
|
||||||
<label class="full">头像地址<input v-model="currentMember.avatar"><span class="compact-upload"><input type="file" accept="image/png,image/jpeg,image/webp" @change="uploadImage($event, url => currentMember!.avatar = url)">上传本地图片</span><MediaPicker v-model="currentMember.avatar" /></label>
|
<label class="full">头像地址<input v-model="currentMember.avatar"><span class="compact-upload"><input type="file" accept="image/png,image/jpeg,image/webp" @change="uploadImage($event, url => currentMember!.avatar = url)">上传本地图片</span><MediaPicker v-model="currentMember.avatar" /></label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-editor">当前分类暂无内容,点击“新建内容”开始编辑。</div>
|
<div v-else class="empty-editor">当前分类暂无内容,点击“新建内容”开始编辑。</div>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { apiErrorMessage } from '~/composables/useUnsavedChanges'
|
|||||||
import type { SiteDocument } from '../../../server/utils/site-schema'
|
import type { SiteDocument } from '../../../server/utils/site-schema'
|
||||||
|
|
||||||
definePageMeta({ layout: false })
|
definePageMeta({ layout: false })
|
||||||
useHead(() => ({ title: `首页管理|${document.value?.brand.name || '慧遇书院'}` }))
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
@@ -20,6 +19,7 @@ const document = ref<SiteDocument>()
|
|||||||
const user = ref<{ displayName: string }>()
|
const user = ref<{ displayName: string }>()
|
||||||
const uploadBusy = ref<string>()
|
const uploadBusy = ref<string>()
|
||||||
const { dirty, markClean } = useUnsavedChanges(document)
|
const { dirty, markClean } = useUnsavedChanges(document)
|
||||||
|
useHead(() => ({ title: `首页管理|${document.value?.brand.name || '慧遇书院'}` }))
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { apiErrorMessage } from '~/composables/useUnsavedChanges'
|
|||||||
import type { SiteDocument } from '../../../server/utils/site-schema'
|
import type { SiteDocument } from '../../../server/utils/site-schema'
|
||||||
|
|
||||||
definePageMeta({ layout: false })
|
definePageMeta({ layout: false })
|
||||||
useHead(() => ({ title: `网站设置|${document.value?.brand.name || '慧遇书院'}` }))
|
|
||||||
const document = ref<SiteDocument>()
|
const document = ref<SiteDocument>()
|
||||||
const loading = ref(true), busy = ref(false), message = ref('')
|
const loading = ref(true), busy = ref(false), message = ref('')
|
||||||
const revisions = ref<Array<{ version: number; action: string; comment: string | null; createdAt: string }>>([])
|
const revisions = ref<Array<{ version: number; action: string; comment: string | null; createdAt: string }>>([])
|
||||||
@@ -13,6 +12,7 @@ type HealthStatus = { status: string; version: string; checks: { database: strin
|
|||||||
const health = ref<HealthStatus>()
|
const health = ref<HealthStatus>()
|
||||||
const passwords = reactive({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
const passwords = reactive({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
||||||
const { dirty, markClean } = useUnsavedChanges(document)
|
const { dirty, markClean } = useUnsavedChanges(document)
|
||||||
|
useHead(() => ({ title: `网站设置|${document.value?.brand.name || '慧遇书院'}` }))
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await $fetch('/api/auth/me')
|
await $fetch('/api/auth/me')
|
||||||
|
|||||||
@@ -19,14 +19,24 @@ const absoluteUrl = useAbsoluteUrl()
|
|||||||
const publishedProducts = computed(() => site.value.products.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published'))
|
const publishedProducts = computed(() => site.value.products.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published'))
|
||||||
const news = computed(() => site.value.articles.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published').slice(0, 4))
|
const news = computed(() => site.value.articles.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published').slice(0, 4))
|
||||||
const team = computed(() => site.value.team.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published').slice(0, 4))
|
const team = computed(() => site.value.team.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published').slice(0, 4))
|
||||||
|
const defaultHeroImage = '/images/hero-bg-v3.webp'
|
||||||
|
const heroImageSource = ref(site.value.hero.backgroundImage || defaultHeroImage)
|
||||||
const hero = ref<HTMLElement>()
|
const hero = ref<HTMLElement>()
|
||||||
let heroFrame = 0
|
let heroFrame = 0
|
||||||
const absoluteHero = computed(() => absoluteUrl(site.value.hero.backgroundImage))
|
const absoluteHero = computed(() => absoluteUrl(heroImageSource.value))
|
||||||
const contactPhoneHref = computed(() => /[待演示]/.test(site.value.contact.phone) ? '' : `tel:${site.value.contact.phone.replace(/\s/g, '')}`)
|
const contactPhoneHref = computed(() => /[待演示]/.test(site.value.contact.phone) ? '' : `tel:${site.value.contact.phone.replace(/\s/g, '')}`)
|
||||||
const contactEmailHref = computed(() => site.value.contact.email.includes('@') && !site.value.contact.email.includes('演示') ? `mailto:${site.value.contact.email}` : '')
|
const contactEmailHref = computed(() => site.value.contact.email.includes('@') && !site.value.contact.email.includes('演示') ? `mailto:${site.value.contact.email}` : '')
|
||||||
const usesDefaultHero = computed(() => site.value.hero.backgroundImage === '/images/hero-bg-v3.webp')
|
const usesDefaultHero = computed(() => heroImageSource.value === defaultHeroImage)
|
||||||
const previewTo = (path: string) => isPreview.value ? { path, query: { preview: 'draft' } } : path
|
const previewTo = (path: string) => isPreview.value ? { path, query: { preview: 'draft' } } : path
|
||||||
|
|
||||||
|
watch(() => site.value.hero.backgroundImage, (value) => {
|
||||||
|
heroImageSource.value = value || defaultHeroImage
|
||||||
|
})
|
||||||
|
|
||||||
|
function useHeroFallback() {
|
||||||
|
if (heroImageSource.value !== defaultHeroImage) heroImageSource.value = defaultHeroImage
|
||||||
|
}
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: () => site.value?.seo.title,
|
title: () => site.value?.seo.title,
|
||||||
description: () => site.value?.seo.description,
|
description: () => site.value?.seo.description,
|
||||||
@@ -78,7 +88,7 @@ onBeforeUnmount(() => {
|
|||||||
<picture class="hero-media" aria-hidden="true">
|
<picture class="hero-media" aria-hidden="true">
|
||||||
<source v-if="usesDefaultHero" srcset="/images/hero-bg-v3.avif" type="image/avif">
|
<source v-if="usesDefaultHero" srcset="/images/hero-bg-v3.avif" type="image/avif">
|
||||||
<source v-if="usesDefaultHero" srcset="/images/hero-bg-v3.webp" type="image/webp">
|
<source v-if="usesDefaultHero" srcset="/images/hero-bg-v3.webp" type="image/webp">
|
||||||
<img :src="site.hero.backgroundImage" alt="" width="1672" height="941" fetchpriority="high" decoding="async">
|
<img :src="heroImageSource" alt="" width="1672" height="941" fetchpriority="high" decoding="async" @error="useHeroFallback">
|
||||||
</picture>
|
</picture>
|
||||||
<SiteHeader :logo="site.brand.logo" :brand-name="site.brand.name" :english-name="site.brand.englishName" />
|
<SiteHeader :logo="site.brand.logo" :brand-name="site.brand.name" :english-name="site.brand.englishName" />
|
||||||
<div class="hero-copy">
|
<div class="hero-copy">
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ if (!article.value) throw createError({ statusCode: 404, statusMessage: '未找
|
|||||||
useSeoMeta({ title: () => `${article.value!.title}|${site.value.brand.name}`, description: () => article.value!.summary, ogTitle: () => article.value!.title, ogDescription: () => article.value!.summary, ogImage: () => absoluteUrl(article.value!.cover), articlePublishedTime: () => article.value!.publishedAt })
|
useSeoMeta({ title: () => `${article.value!.title}|${site.value.brand.name}`, description: () => article.value!.summary, ogTitle: () => article.value!.title, ogDescription: () => article.value!.summary, ogImage: () => absoluteUrl(article.value!.cover), articlePublishedTime: () => article.value!.publishedAt })
|
||||||
useHead(() => ({ script: [{ type: 'application/ld+json', textContent: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', headline: article.value!.title, description: article.value!.summary, datePublished: article.value!.publishedAt, image: absoluteUrl(article.value!.cover), publisher: { '@type': 'Organization', name: site.value.brand.companyName, logo: { '@type': 'ImageObject', url: absoluteUrl(site.value.brand.logo) } } }) }] }))
|
useHead(() => ({ script: [{ type: 'application/ld+json', textContent: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Article', headline: article.value!.title, description: article.value!.summary, datePublished: article.value!.publishedAt, image: absoluteUrl(article.value!.cover), publisher: { '@type': 'Organization', name: site.value.brand.companyName, logo: { '@type': 'ImageObject', url: absoluteUrl(site.value.brand.logo) } } }) }] }))
|
||||||
</script>
|
</script>
|
||||||
<template><PublicPageLayout :site="site"><article v-if="article" class="story-page"><NuxtLink class="detail-back" :to="{ path: '/news', query: isPreview ? { preview: 'draft' } : {} }">← 返回品牌动态</NuxtLink><header><p class="eyebrow">BRAND NEWS</p><h1>{{ article.title }}</h1><p>{{ article.publishedAt }}<span v-if="article.sourceName"> · {{ article.sourceName }}</span></p><img :src="article.cover" :alt="article.title" width="1536" height="1024" decoding="async"></header><div class="story-body"><p class="lead">{{ article.summary }}</p><p class="long-copy">{{ article.content }}</p><a v-if="article.sourceUrl" :href="article.sourceUrl" target="_blank" rel="noopener noreferrer" class="text-link">查看原文 ↗</a></div><aside v-if="related.length" class="related-content"><p class="eyebrow">RELATED</p><h2>继续阅读</h2><NuxtLink v-for="item in related" :key="item.slug" :to="{ path: `/news/${item.slug}`, query: isPreview ? { preview: 'draft' } : {} }"><span>{{ item.title }}</span><time>{{ item.publishedAt }}</time></NuxtLink></aside></article></PublicPageLayout></template>
|
<template><PublicPageLayout :site="site"><article v-if="article" class="story-page"><NuxtLink class="detail-back" :to="{ path: '/news', query: isPreview ? { preview: 'draft' } : {} }">← 返回品牌动态</NuxtLink><header><p class="eyebrow">BRAND NEWS</p><h1>{{ article.title }}</h1><p>{{ article.publishedAt }}<span v-if="article.sourceName"> · {{ article.sourceName }}</span></p><img :src="article.cover" :alt="article.title" width="1536" height="1024" decoding="async"></header><div class="story-body"><p class="lead">{{ article.summary }}</p><RichTextContent :content="article.content" /><a v-if="article.sourceUrl" :href="article.sourceUrl" target="_blank" rel="noopener noreferrer" class="text-link">查看原文 ↗</a></div><aside v-if="related.length" class="related-content"><p class="eyebrow">RELATED</p><h2>继续阅读</h2><NuxtLink v-for="item in related" :key="item.slug" :to="{ path: `/news/${item.slug}`, query: isPreview ? { preview: 'draft' } : {} }"><span>{{ item.title }}</span><time>{{ item.publishedAt }}</time></NuxtLink></aside></article></PublicPageLayout></template>
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ if (!product.value) throw createError({ statusCode: 404, statusMessage: '未找
|
|||||||
useSeoMeta({ title: () => `${product.value!.title}|${site.value.brand.name}`, description: () => product.value!.summary, ogTitle: () => product.value!.title, ogDescription: () => product.value!.summary, ogImage: () => absoluteUrl(product.value!.image) })
|
useSeoMeta({ title: () => `${product.value!.title}|${site.value.brand.name}`, description: () => product.value!.summary, ogTitle: () => product.value!.title, ogDescription: () => product.value!.summary, ogImage: () => absoluteUrl(product.value!.image) })
|
||||||
useHead(() => ({ script: [{ type: 'application/ld+json', textContent: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Course', name: product.value!.title, description: product.value!.summary, provider: { '@type': 'Organization', name: site.value.brand.companyName, url: absoluteUrl('/') }, image: absoluteUrl(product.value!.image) }) }] }))
|
useHead(() => ({ script: [{ type: 'application/ld+json', textContent: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Course', name: product.value!.title, description: product.value!.summary, provider: { '@type': 'Organization', name: site.value.brand.companyName, url: absoluteUrl('/') }, image: absoluteUrl(product.value!.image) }) }] }))
|
||||||
</script>
|
</script>
|
||||||
<template><PublicPageLayout :site="site"><article v-if="product" class="story-page"><NuxtLink class="detail-back" :to="{ path: '/products', query: isPreview ? { preview: 'draft' } : {} }">← 返回课程与产品</NuxtLink><header><p class="eyebrow">{{ product.type === 'tool' ? 'TECH PRODUCT' : 'COURSE' }}</p><h1>{{ product.title }}</h1><p>{{ product.subtitle }}</p><img :src="product.image" :alt="product.title" width="1536" height="1024" loading="eager" decoding="async"></header><div class="story-body"><p class="lead">{{ product.summary }}</p><p class="long-copy">{{ product.content }}</p><a class="primary-button" href="/#contact">咨询了解</a></div></article></PublicPageLayout></template>
|
<template><PublicPageLayout :site="site"><article v-if="product" class="story-page"><NuxtLink class="detail-back" :to="{ path: '/products', query: isPreview ? { preview: 'draft' } : {} }">← 返回课程与产品</NuxtLink><header><p class="eyebrow">{{ product.type === 'tool' ? 'TECH PRODUCT' : 'COURSE' }}</p><h1>{{ product.title }}</h1><p>{{ product.subtitle }}</p><img :src="product.image" :alt="product.title" width="1536" height="1024" loading="eager" decoding="async"></header><div class="story-body"><p class="lead">{{ product.summary }}</p><RichTextContent :content="product.content" /><a class="primary-button article-consult" href="/#contact">咨询了解</a></div></article></PublicPageLayout></template>
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ const article = computed(() => site.value.articles.find(item => item.slug === ro
|
|||||||
if (!article.value) throw createError({ statusCode: 404, statusMessage: '未找到该声明' })
|
if (!article.value) throw createError({ statusCode: 404, statusMessage: '未找到该声明' })
|
||||||
useSeoMeta({ title: () => `${article.value!.title}|${site.value.brand.name}`, description: () => article.value!.summary })
|
useSeoMeta({ title: () => `${article.value!.title}|${site.value.brand.name}`, description: () => article.value!.summary })
|
||||||
</script>
|
</script>
|
||||||
<template><PublicPageLayout :site="site"><article v-if="article" class="story-page statement-page"><NuxtLink class="detail-back" :to="{ path: '/statements', query: isPreview ? { preview: 'draft' } : {} }">← 返回官方声明</NuxtLink><header><p class="eyebrow">OFFICIAL STATEMENT</p><small>{{ article.statementNo }}</small><h1>{{ article.title }}</h1><p>{{ article.publishedAt }}</p></header><div class="story-body"><p class="lead">{{ article.summary }}</p><p class="long-copy">{{ article.content }}</p></div></article></PublicPageLayout></template>
|
<template><PublicPageLayout :site="site"><article v-if="article" class="story-page statement-page"><NuxtLink class="detail-back" :to="{ path: '/statements', query: isPreview ? { preview: 'draft' } : {} }">← 返回官方声明</NuxtLink><header><p class="eyebrow">OFFICIAL STATEMENT</p><small>{{ article.statementNo }}</small><h1>{{ article.title }}</h1><p>{{ article.publishedAt }}</p></header><div class="story-body"><p class="lead">{{ article.summary }}</p><RichTextContent :content="article.content" /></div></article></PublicPageLayout></template>
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ const { site, isPreview } = await useSiteDocument()
|
|||||||
const members = computed(() => site.value.team.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published'))
|
const members = computed(() => site.value.team.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published'))
|
||||||
useSeoMeta({ title: () => `陪伴团队|${site.value.brand.name}`, description: () => `${site.value.brand.name}陪伴团队。` })
|
useSeoMeta({ title: () => `陪伴团队|${site.value.brand.name}`, description: () => `${site.value.brand.name}陪伴团队。` })
|
||||||
</script>
|
</script>
|
||||||
<template><PublicPageLayout :site="site"><section class="listing-hero"><p class="eyebrow">COMPANION TEAM</p><h1>陪伴团队</h1><p>以专业、温暖和清晰的协作,为每一段成长提供支持。</p></section><section class="team-grid"><article v-for="member in members" :key="member.slug"><img :src="member.avatar" :alt="member.name" width="1536" height="1024" loading="lazy" decoding="async"><div><h2>{{ member.name }}</h2><p class="role">{{ member.role }}</p><p>{{ member.summary }}</p><div class="tag-list"><span v-for="tag in member.specialties" :key="tag">{{ tag }}</span></div></div></article></section></PublicPageLayout></template>
|
<template><PublicPageLayout :site="site"><section class="listing-hero"><p class="eyebrow">COMPANION TEAM</p><h1>陪伴团队</h1><p>以专业、温暖和清晰的协作,为每一段成长提供支持。</p></section><section class="team-grid"><article v-for="member in members" :key="member.slug"><img :src="member.avatar" :alt="member.name" width="1536" height="1024" loading="lazy" decoding="async"><div><h2>{{ member.name }}</h2><p class="role">{{ member.role }}</p><RichTextContent :content="member.summary" /><div class="tag-list"><span v-for="tag in member.specialties" :key="tag">{{ tag }}</span></div></div></article></section></PublicPageLayout></template>
|
||||||
|
|||||||
24
app/utils/rich-text.ts
Normal file
24
app/utils/rich-text.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const RICH_TEXT_TAG = /<(?:p|h[1-6]|ul|ol|li|blockquote|strong|em|u|s|a|br|hr|span)\b/i
|
||||||
|
|
||||||
|
export function escapeRichText(value: string) {
|
||||||
|
return value
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toEditorHtml(value: string) {
|
||||||
|
if (!value.trim()) return '<p></p>'
|
||||||
|
if (RICH_TEXT_TAG.test(value)) return value
|
||||||
|
|
||||||
|
return value
|
||||||
|
.split(/\n{2,}/)
|
||||||
|
.map(paragraph => `<p>${escapeRichText(paragraph).replaceAll('\n', '<br>')}</p>`)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDisplayHtml(value: string) {
|
||||||
|
return toEditorHtml(value || '')
|
||||||
|
}
|
||||||
848
package-lock.json
generated
848
package-lock.json
generated
@@ -7,14 +7,21 @@
|
|||||||
"name": "huiyu-official-site",
|
"name": "huiyu-official-site",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phosphor-icons/vue": "^2.2.1",
|
"@phosphor-icons/vue": "^2.2.1",
|
||||||
|
"@tiptap/extension-image": "^3.30.5",
|
||||||
|
"@tiptap/extension-text-align": "^3.30.5",
|
||||||
|
"@tiptap/extension-text-style": "^3.30.5",
|
||||||
|
"@tiptap/starter-kit": "^3.30.5",
|
||||||
|
"@tiptap/vue-3": "^3.30.5",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"better-sqlite3": "^13.0.3",
|
"better-sqlite3": "^13.0.3",
|
||||||
"nuxt": "^4.5.2",
|
"nuxt": "^4.5.2",
|
||||||
|
"sanitize-html": "^2.17.7",
|
||||||
"sharp": "^0.35.3",
|
"sharp": "^0.35.3",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"@types/sanitize-html": "^2.16.1",
|
||||||
"sass": "^1.93.3",
|
"sass": "^1.93.3",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
@@ -941,6 +948,31 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.8.0",
|
||||||
|
"@floating-ui/utils": "^0.2.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||||
|
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@img/colour": {
|
"node_modules/@img/colour": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
@@ -3246,6 +3278,468 @@
|
|||||||
"integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
|
"integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-3O7N0FyKIfuLV+xrdWyDM3V5eUY/q2CgLjhhMwOAbM1Pu7VPp9VP+TpEYOdH8aRyB+h1vj5hX5A747D8ZrPfHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-8pf1ZDrl6XlVPUee/2YlWZPFSF7yqsJEX6WLW41x06MT1dapG3Qudcns0znj6kYsX8JXTJ+Mhegy4z19bvTtRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-MLZS+s/BJiJbv2C2M3G4FQGn43kPwYUUr2TiW3afSn+dRkEZlDxx5CwZYbe1PEt2+VX/SIifn945Oqr3s0axSA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bubble-menu": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-redcmBInVwmipjF/WEVcNwCUJgJygUv8leidfRb/jBSAwx7GgnQWPr2HVQDk9zeTt3ykWld2NRdrk94Crjymow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-66OGw4suO0Gr/4QAEiSvVi0eSvEqStvu1moHUSvUlwvEqnuB0ME2w9HuRogPElzWdrYLzbBA47mlCi9Veva0ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-0dCt8eBo4sMtw+LjUu+0GF0JrTlREROdWoy8TM5kFPxJ5ZU2LKDiROXDPwXStaaoFFR/aStLH9xSpdz7cHUiUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-SvkNoOio2xBy9AtSMyFKMp88MTUvyIXsGCnuz71INV2wHdH4VfRPoQLT6e077LpTCa9w6LHyTKtbZA4HYOp7wg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-4mKoD3bBr5W2AQ2Y/7amOqcVw0eqJbUwRtwvxypeo5Hi0PB9O5Ev8P05c3EUTo10bllqdKYXGeCU5AcgdQsHWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-gJxn9PMUee8zazQBYqbOZiI7zTFXPVJ15AzTnLfUu4eDtPieMwtpgcjhCt4bZlp37drLm+Li76wWLirb7iLxsw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-gTjGPWUpGn8IoW533TciZJZC/81LmBgU7zee+aRMRF3ix3K2z1pJjl6knDvrRQA6Kom+yWFaiueqGNXkrk38XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-h3m2ZA1XXLAfdHwdtG0MZnf0KWbNyP8xTI3RUlTDR5apmKmYVBaeocOJwTcFFZ92gPKqxYKyhteZqYtHOCZGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-PcL4Z8l/DlauwZUJg0jf6SXKk6/YOXJtd8yYzqHrmCLRoY4kAa7+TxUhc/lWhuitUiOZ8+nB8V/NNM+WR6mRaw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-x7e7+p1bWXvwz6vqONPP1SVB73V0gZP7LIDL/5KMnxWLjahMTabgVPgjV30kz7GsOEgUYtEf7eCyGVGNNWL5Dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-s5t2xM6wPYRJl2cqYplc7Ze8m8xddk4DP2v0aLFcUuD100D1B5eJbxsiwLK6wBAzNwiuVjd486RnmUpcswaXhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-image": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-fETBsbSTaf5MLCGZaffHRRAFYUHeDIeITbnm7Q5Il+/LlX7PEiBZGKVyD/M3bIMpxhkyB0UlryIYGhC5LjN1Aw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-Fu9EuSRlHQNGES7UhY4mQod3yUKnWwxsGlPuDJURfwju/Ug04Jz8iRuERZBPcRgLNkwKDmbDsWUZF72RRIL2Uw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-zsQ+q83HpCYOMTNzdjb12dggE7sZMp0aqQJhArdavSTl8hLbVY/u3qJN88t25IEwAKVK0pcTJfWWA2QFl8bjAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-CHThihH+7TA0TfwtmA+eTXP1MAsA/+p011olJHg8Rilj5xA+L+0IFkjhN16JnZkBgSA4MnfEv15UMNfRaNpuVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-N0fKUyQkPQBvVB48imjfIfdexU9VsAX1RydajYP2xAt+QOJ8KHVmXv1EGTp+v7pGdZMLdls1cSZ2f352vW3aWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-3pLR2yo29uhowaGMbD8v71XpgdNmqotBg1NXlyTMVFzxELj+VjjeLLTqSguR/iWDUbHXNzIsfMEgpRnNMO//8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-bUGUnSAgjZhoUWBtr+1wRJHF3NnNvuKQr6sQ8le1tJ2yvLq448ZsSZjZGvTNeLsy3GDKBoMJ0rqLUK34+nM3hg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-GrNNlAImfQhYRtGE95YNAjcTclUUMPHs9JeE9vMgfcoKcOmZ6GsbWUdN0hA55xqwGaUjnroOVtf77K9z1EbkJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-r8IbWUm4YXNCkmc6h6dzuREQIGbO0x9z9l3GfQPUR3LxFtGssDLngE4dFXHRt/ejLWKEauy6clklf7LawtvYzw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-pOgj4mIGFlw4NUdA6PCTFP/MHrYWnOYiZRYeu0I3/Yo3iN4Am/CgYBLl8PWsNh+YPO2hA2DWs7ZMke9D2j5R+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-align": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-nRiAXqfpnuYkxOTFmK09vKf0Obv+yYA6UvmQ2yhjaThf7/i+ncxAbmxWttAV8GEy2fGpuXxkJYtIgDXXN7tVPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-style": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-libXk+nSBOQmUiKneMegnipSVz7n/YfCuSVUxXn4LjAdD9zdpWgH2uBXbxo2q211dA7KxJyUhkPGoCpG0RmPQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-u12G/WW2uFRY95BzrSAU9RW002K+7lFpSCL6fb7Puh8yLhek3lddOtAx0P+NI/PyMKtcoVMtBxoi+0mwVM7NPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-5x3OiCYBvXz0G5OGM8f7ka++1dh9EXdNRZ8IcMcEd+QwW4RPwvmJ2EjheA1eRRyMlcbNU7T+4IRlBXURlTetEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-gufkLkW2tA6PZPjivYxDiGzTIIftwqhmYI6lvvKu2S4FbhcysJgMAe/GXVSywzCRVVex9SrvCe6RFYrqnwRitQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-changeset": "^2.4.1",
|
||||||
|
"prosemirror-commands": "^1.7.1",
|
||||||
|
"prosemirror-dropcursor": "^1.8.2",
|
||||||
|
"prosemirror-gapcursor": "^1.4.1",
|
||||||
|
"prosemirror-history": "^1.5.0",
|
||||||
|
"prosemirror-inputrules": "^1.5.1",
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.11",
|
||||||
|
"prosemirror-schema-list": "^1.5.1",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-tables": "^1.8.5",
|
||||||
|
"prosemirror-transform": "^1.12.0",
|
||||||
|
"prosemirror-view": "^1.41.9"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/starter-kit": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-cmDRvukpoqjQjhE96q+l3tCLNZZcJKehxK+00Sp9F4XbKecfM2QpZ2TRHh3vi8qhCeDQNyMoCqfNaqznIjLjIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/extension-blockquote": "3.30.5",
|
||||||
|
"@tiptap/extension-bold": "3.30.5",
|
||||||
|
"@tiptap/extension-bullet-list": "3.30.5",
|
||||||
|
"@tiptap/extension-code": "3.30.5",
|
||||||
|
"@tiptap/extension-code-block": "3.30.5",
|
||||||
|
"@tiptap/extension-document": "3.30.5",
|
||||||
|
"@tiptap/extension-dropcursor": "3.30.5",
|
||||||
|
"@tiptap/extension-gapcursor": "3.30.5",
|
||||||
|
"@tiptap/extension-hard-break": "3.30.5",
|
||||||
|
"@tiptap/extension-heading": "3.30.5",
|
||||||
|
"@tiptap/extension-horizontal-rule": "3.30.5",
|
||||||
|
"@tiptap/extension-italic": "3.30.5",
|
||||||
|
"@tiptap/extension-link": "3.30.5",
|
||||||
|
"@tiptap/extension-list": "3.30.5",
|
||||||
|
"@tiptap/extension-list-item": "3.30.5",
|
||||||
|
"@tiptap/extension-list-keymap": "3.30.5",
|
||||||
|
"@tiptap/extension-ordered-list": "3.30.5",
|
||||||
|
"@tiptap/extension-paragraph": "3.30.5",
|
||||||
|
"@tiptap/extension-strike": "3.30.5",
|
||||||
|
"@tiptap/extension-text": "3.30.5",
|
||||||
|
"@tiptap/extension-underline": "3.30.5",
|
||||||
|
"@tiptap/extensions": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/vue-3": {
|
||||||
|
"version": "3.30.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.30.5.tgz",
|
||||||
|
"integrity": "sha512-YRHacQy09nFcokAScICxJ6pWr5daWbsLJuBiMo+DJQlr1XLi6o/zFhh3eR9+DKluL4OY3IZDYBYZfDs7YZtJNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.30.5",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.30.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "3.30.5",
|
||||||
|
"@tiptap/pm": "3.30.5",
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/better-sqlite3": {
|
"node_modules/@types/better-sqlite3": {
|
||||||
"version": "7.6.13",
|
"version": "7.6.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||||
@@ -3302,6 +3796,36 @@
|
|||||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sanitize-html": {
|
||||||
|
"version": "2.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.1.tgz",
|
||||||
|
"integrity": "sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"htmlparser2": "^10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/htmlparser2": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"entities": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@unhead/bundler": {
|
"node_modules/@unhead/bundler": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@unhead/bundler/-/bundler-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@unhead/bundler/-/bundler-3.3.1.tgz",
|
||||||
@@ -5039,6 +5563,12 @@
|
|||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
|
||||||
|
"integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/db0": {
|
"node_modules/db0": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/db0/-/db0-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/db0/-/db0-0.3.4.tgz",
|
||||||
@@ -5919,6 +6449,107 @@
|
|||||||
"integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==",
|
"integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "12.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^3.0.0",
|
||||||
|
"domhandler": "^6.0.0",
|
||||||
|
"domutils": "^4.0.2",
|
||||||
|
"entities": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/dom-serializer": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^3.0.0",
|
||||||
|
"domhandler": "^6.0.0",
|
||||||
|
"entities": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/domelementtype": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/domhandler": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/domutils": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^3.0.0",
|
||||||
|
"domelementtype": "^3.0.0",
|
||||||
|
"domhandler": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/entities": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
@@ -6211,6 +6842,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-plain-object": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-reference": {
|
"node_modules/is-reference": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
||||||
@@ -6350,6 +6990,15 @@
|
|||||||
"shell-quote": "^1.8.4"
|
"shell-quote": "^1.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/launder": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/launder/-/launder-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dayjs": "^1.11.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lazystream": {
|
"node_modules/lazystream": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||||
@@ -6653,6 +7302,12 @@
|
|||||||
"url": "https://github.com/sponsors/antonk52"
|
"url": "https://github.com/sponsors/antonk52"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
|
||||||
|
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/listhen": {
|
"node_modules/listhen": {
|
||||||
"version": "1.10.1",
|
"version": "1.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/listhen/-/listhen-1.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/listhen/-/listhen-1.10.1.tgz",
|
||||||
@@ -7497,6 +8152,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/orderedmap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/oxc-walker": {
|
"node_modules/oxc-walker": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-1.1.1.tgz",
|
||||||
@@ -7525,6 +8186,12 @@
|
|||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
"license": "BlueOak-1.0.0"
|
"license": "BlueOak-1.0.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-srcset": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -8148,6 +8815,145 @@
|
|||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-changeset": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-ViYrjMSg3YFiXwIhKaluu+/mi3Yrxt6AR8ri14ulTaGcZtXO1CThl7A2gv79qx5fQnOw8woKwyBU2u+9PVCm3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-commands": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-dropcursor": {
|
||||||
|
"version": "1.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz",
|
||||||
|
"integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0",
|
||||||
|
"prosemirror-view": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-gapcursor": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.0.0",
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-history": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.2.2",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.31.0",
|
||||||
|
"rope-sequence": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-inputrules": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-keymap": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"w3c-keyname": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-model": {
|
||||||
|
"version": "1.25.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz",
|
||||||
|
"integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"orderedmap": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-list": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-state": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-tables": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.4",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-transform": "^1.10.5",
|
||||||
|
"prosemirror-view": "^1.41.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-transform": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-view": {
|
||||||
|
"version": "1.42.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.3.tgz",
|
||||||
|
"integrity": "sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.25.8",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/quansync": {
|
"node_modules/quansync": {
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||||
@@ -8478,6 +9284,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rope-sequence": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rou3": {
|
"node_modules/rou3": {
|
||||||
"version": "0.9.1",
|
"version": "0.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.1.tgz",
|
||||||
@@ -8539,6 +9351,36 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sanitize-html": {
|
||||||
|
"version": "2.17.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.7.tgz",
|
||||||
|
"integrity": "sha512-PGtEkc9cbnedU3s9TmzDbpsZ8w086g/0Q8k8/oIO1NLNU3i5k9yn835CrjJSajp1KMmkisbO1qPXxNKO3welAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"htmlparser2": "^12.0.0",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"launder": "^1.7.1",
|
||||||
|
"parse-srcset": "^1.0.2",
|
||||||
|
"postcss": "^8.3.11"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.102.0",
|
"version": "1.102.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.102.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.102.0.tgz",
|
||||||
@@ -10487,6 +11329,12 @@
|
|||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
@@ -12,14 +12,21 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phosphor-icons/vue": "^2.2.1",
|
"@phosphor-icons/vue": "^2.2.1",
|
||||||
|
"@tiptap/extension-image": "^3.30.5",
|
||||||
|
"@tiptap/extension-text-align": "^3.30.5",
|
||||||
|
"@tiptap/extension-text-style": "^3.30.5",
|
||||||
|
"@tiptap/starter-kit": "^3.30.5",
|
||||||
|
"@tiptap/vue-3": "^3.30.5",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"better-sqlite3": "^13.0.3",
|
"better-sqlite3": "^13.0.3",
|
||||||
"nuxt": "^4.5.2",
|
"nuxt": "^4.5.2",
|
||||||
|
"sanitize-html": "^2.17.7",
|
||||||
"sharp": "^0.35.3",
|
"sharp": "^0.35.3",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"@types/sanitize-html": "^2.16.1",
|
||||||
"sass": "^1.93.3",
|
"sass": "^1.93.3",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { requireAdmin } from '../../../utils/auth'
|
import { requireAdmin } from '../../../utils/auth'
|
||||||
import { writeAuditLog } from '../../../utils/audit'
|
import { writeAuditLog } from '../../../utils/audit'
|
||||||
import { useDatabase } from '../../../utils/db'
|
import { useDatabase } from '../../../utils/db'
|
||||||
|
import { sanitizeRichText } from '../../../utils/rich-text'
|
||||||
import { siteDocumentSchema } from '../../../utils/site-schema'
|
import { siteDocumentSchema } from '../../../utils/site-schema'
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
@@ -10,6 +11,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
throw createError({ statusCode: 422, statusMessage: '内容校验失败', data: { issues: parsed.error.issues } })
|
throw createError({ statusCode: 422, statusMessage: '内容校验失败', data: { issues: parsed.error.issues } })
|
||||||
}
|
}
|
||||||
const document = parsed.data
|
const document = parsed.data
|
||||||
|
document.products.forEach(product => { product.content = sanitizeRichText(product.content) })
|
||||||
|
document.articles.forEach(article => { article.content = sanitizeRichText(article.content) })
|
||||||
|
document.team.forEach(member => { member.summary = sanitizeRichText(member.summary) })
|
||||||
const timestamp = new Date().toISOString()
|
const timestamp = new Date().toISOString()
|
||||||
const db = useDatabase()
|
const db = useDatabase()
|
||||||
db.prepare('UPDATE site_documents SET draft_json = ?, updated_by = ?, updated_at = ? WHERE document_key = ?')
|
db.prepare('UPDATE site_documents SET draft_json = ?, updated_by = ?, updated_at = ? WHERE document_key = ?')
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
UPDATE site_documents
|
||||||
|
SET draft_json = replace(draft_json, '"/images/hero-bg-v1.png"', '"/images/hero-bg-v3.webp"'),
|
||||||
|
published_json = replace(published_json, '"/images/hero-bg-v1.png"', '"/images/hero-bg-v3.webp"')
|
||||||
|
WHERE draft_json LIKE '%"/images/hero-bg-v1.png"%'
|
||||||
|
OR published_json LIKE '%"/images/hero-bg-v1.png"%';
|
||||||
@@ -3,6 +3,7 @@ import { hashSync } from 'bcryptjs'
|
|||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
||||||
import { dirname, resolve } from 'node:path'
|
import { dirname, resolve } from 'node:path'
|
||||||
|
import { sanitizeRichText } from './rich-text'
|
||||||
import { defaultSiteDocument, siteDocumentSchema } from './site-schema'
|
import { defaultSiteDocument, siteDocumentSchema } from './site-schema'
|
||||||
|
|
||||||
let database: Database.Database | undefined
|
let database: Database.Database | undefined
|
||||||
@@ -92,6 +93,9 @@ export function readSiteDocument(mode: 'draft' | 'published' = 'published') {
|
|||||||
function normalizeLegacyDocument(input: unknown) {
|
function normalizeLegacyDocument(input: unknown) {
|
||||||
if (!input || typeof input !== 'object') return input
|
if (!input || typeof input !== 'object') return input
|
||||||
const document = structuredClone(input) as Record<string, any>
|
const document = structuredClone(input) as Record<string, any>
|
||||||
|
if (document.hero?.backgroundImage === '/images/hero-bg-v1.png') {
|
||||||
|
document.hero.backgroundImage = '/images/hero-bg-v3.webp'
|
||||||
|
}
|
||||||
if (Array.isArray(document.products)) {
|
if (Array.isArray(document.products)) {
|
||||||
document.products = document.products.map((product: Record<string, any>, index: number) => {
|
document.products = document.products.map((product: Record<string, any>, index: number) => {
|
||||||
const fallback = defaultSiteDocument.products[index]
|
const fallback = defaultSiteDocument.products[index]
|
||||||
@@ -102,7 +106,7 @@ function normalizeLegacyDocument(input: unknown) {
|
|||||||
subtitle: product.subtitle || '',
|
subtitle: product.subtitle || '',
|
||||||
type: product.type || fallback?.type || 'course',
|
type: product.type || fallback?.type || 'course',
|
||||||
summary: product.summary || fallback?.summary || '',
|
summary: product.summary || fallback?.summary || '',
|
||||||
content: product.content || fallback?.content || '',
|
content: sanitizeRichText(product.content || fallback?.content || ''),
|
||||||
image: product.image || fallback?.image || '/images/hero-bg-v3.webp',
|
image: product.image || fallback?.image || '/images/hero-bg-v3.webp',
|
||||||
href: product.href || `/products/${slug}`,
|
href: product.href || `/products/${slug}`,
|
||||||
status: product.status || 'published',
|
status: product.status || 'published',
|
||||||
@@ -110,7 +114,15 @@ function normalizeLegacyDocument(input: unknown) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
document.articles ??= structuredClone(defaultSiteDocument.articles)
|
document.articles ??= structuredClone(defaultSiteDocument.articles)
|
||||||
|
document.articles = document.articles.map((article: Record<string, any>) => ({
|
||||||
|
...article,
|
||||||
|
content: sanitizeRichText(article.content || ''),
|
||||||
|
}))
|
||||||
document.team ??= structuredClone(defaultSiteDocument.team)
|
document.team ??= structuredClone(defaultSiteDocument.team)
|
||||||
|
document.team = document.team.map((member: Record<string, any>) => ({
|
||||||
|
...member,
|
||||||
|
summary: sanitizeRichText(member.summary || ''),
|
||||||
|
}))
|
||||||
document.channels ??= structuredClone(defaultSiteDocument.channels)
|
document.channels ??= structuredClone(defaultSiteDocument.channels)
|
||||||
document.legal ??= structuredClone(defaultSiteDocument.legal)
|
document.legal ??= structuredClone(defaultSiteDocument.legal)
|
||||||
document.brand = { ...defaultSiteDocument.brand, ...(document.brand || {}) }
|
document.brand = { ...defaultSiteDocument.brand, ...(document.brand || {}) }
|
||||||
|
|||||||
36
server/utils/rich-text.ts
Normal file
36
server/utils/rich-text.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
|
const allowedFontFamilies = /^(?:Noto Serif SC|Source Han Serif SC|Songti SC|SimSun|Noto Sans SC|Source Han Sans SC|PingFang SC|Microsoft YaHei|Arial|sans-serif|serif)(?:,\s*(?:serif|sans-serif))?$/
|
||||||
|
|
||||||
|
export function sanitizeRichText(value: string) {
|
||||||
|
if (!value) return ''
|
||||||
|
|
||||||
|
return sanitizeHtml(value, {
|
||||||
|
allowedTags: ['p', 'h2', 'h3', 'ul', 'ol', 'li', 'blockquote', 'strong', 'em', 'u', 's', 'a', 'br', 'hr', 'span'],
|
||||||
|
allowedAttributes: {
|
||||||
|
a: ['href', 'target', 'rel'],
|
||||||
|
'*': ['style'],
|
||||||
|
},
|
||||||
|
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
|
||||||
|
allowProtocolRelative: false,
|
||||||
|
allowedStyles: {
|
||||||
|
'*': {
|
||||||
|
color: [/^#[0-9a-f]{6}$/i],
|
||||||
|
'background-color': [/^#[0-9a-f]{6}$/i],
|
||||||
|
'font-family': [allowedFontFamilies],
|
||||||
|
'font-size': [/^(?:14|16|18|20|24|28|32)px$/],
|
||||||
|
'line-height': [/^(?:1\.5|1\.75|2|2\.25)$/],
|
||||||
|
'text-align': [/^(?:left|center|right|justify)$/],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transformTags: {
|
||||||
|
a: (tagName, attributes) => ({
|
||||||
|
tagName,
|
||||||
|
attribs: {
|
||||||
|
...attributes,
|
||||||
|
...(attributes.target === '_blank' ? { rel: 'noopener noreferrer' } : {}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ const productSchema = z.object({
|
|||||||
subtitle: z.string().max(80),
|
subtitle: z.string().max(80),
|
||||||
type: z.enum(['course', 'service', 'tool']).default('course'),
|
type: z.enum(['course', 'service', 'tool']).default('course'),
|
||||||
summary: z.string().max(300).default(''),
|
summary: z.string().max(300).default(''),
|
||||||
content: z.string().max(6000).default(''),
|
content: z.string().max(30000).default(''),
|
||||||
image: localOrHttpUrl,
|
image: localOrHttpUrl,
|
||||||
href: localOrHttpUrl,
|
href: localOrHttpUrl,
|
||||||
status: z.enum(['draft', 'published', 'offline']).default('draft'),
|
status: z.enum(['draft', 'published', 'offline']).default('draft'),
|
||||||
@@ -26,7 +26,7 @@ const articleSchema = z.object({
|
|||||||
type: z.enum(['news', 'statement', 'media']),
|
type: z.enum(['news', 'statement', 'media']),
|
||||||
title: z.string().min(1).max(120),
|
title: z.string().min(1).max(120),
|
||||||
summary: z.string().max(300),
|
summary: z.string().max(300),
|
||||||
content: z.string().max(12000),
|
content: z.string().max(50000),
|
||||||
cover: localOrHttpUrl,
|
cover: localOrHttpUrl,
|
||||||
publishedAt: z.string().max(40),
|
publishedAt: z.string().max(40),
|
||||||
status: z.enum(['draft', 'published', 'offline']),
|
status: z.enum(['draft', 'published', 'offline']),
|
||||||
|
|||||||
22
tests/rich-text.test.ts
Normal file
22
tests/rich-text.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { sanitizeRichText } from '../server/utils/rich-text'
|
||||||
|
|
||||||
|
describe('sanitizeRichText', () => {
|
||||||
|
it('keeps supported article formatting', () => {
|
||||||
|
const html = '<h2 style="text-align:center">标题</h2><p><strong>重点</strong><span style="font-size:20px;color:#17364e">正文</span></p>'
|
||||||
|
expect(sanitizeRichText(html)).toBe(html)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes scripts, event handlers and unsafe links', () => {
|
||||||
|
const html = '<p onclick="alert(1)">正文<script>alert(1)</script><a href="javascript:alert(1)">危险链接</a></p>'
|
||||||
|
const cleaned = sanitizeRichText(html)
|
||||||
|
expect(cleaned).not.toContain('script')
|
||||||
|
expect(cleaned).not.toContain('onclick')
|
||||||
|
expect(cleaned).not.toContain('javascript:')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds isolation attributes to new-window links', () => {
|
||||||
|
expect(sanitizeRichText('<a href="https://example.com" target="_blank">示例</a>'))
|
||||||
|
.toContain('rel="noopener noreferrer"')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user