feat: 完善内容管理通用富文本编辑器
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { requireAdmin } from '../../../utils/auth'
|
||||
import { writeAuditLog } from '../../../utils/audit'
|
||||
import { useDatabase } from '../../../utils/db'
|
||||
import { sanitizeRichText } from '../../../utils/rich-text'
|
||||
import { siteDocumentSchema } from '../../../utils/site-schema'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -10,6 +11,9 @@ export default defineEventHandler(async (event) => {
|
||||
throw createError({ statusCode: 422, statusMessage: '内容校验失败', data: { issues: parsed.error.issues } })
|
||||
}
|
||||
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 db = useDatabase()
|
||||
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 { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { sanitizeRichText } from './rich-text'
|
||||
import { defaultSiteDocument, siteDocumentSchema } from './site-schema'
|
||||
|
||||
let database: Database.Database | undefined
|
||||
@@ -92,6 +93,9 @@ export function readSiteDocument(mode: 'draft' | 'published' = 'published') {
|
||||
function normalizeLegacyDocument(input: unknown) {
|
||||
if (!input || typeof input !== 'object') return input
|
||||
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)) {
|
||||
document.products = document.products.map((product: Record<string, any>, index: number) => {
|
||||
const fallback = defaultSiteDocument.products[index]
|
||||
@@ -102,7 +106,7 @@ function normalizeLegacyDocument(input: unknown) {
|
||||
subtitle: product.subtitle || '',
|
||||
type: product.type || fallback?.type || 'course',
|
||||
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',
|
||||
href: product.href || `/products/${slug}`,
|
||||
status: product.status || 'published',
|
||||
@@ -110,7 +114,15 @@ function normalizeLegacyDocument(input: unknown) {
|
||||
})
|
||||
}
|
||||
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 = document.team.map((member: Record<string, any>) => ({
|
||||
...member,
|
||||
summary: sanitizeRichText(member.summary || ''),
|
||||
}))
|
||||
document.channels ??= structuredClone(defaultSiteDocument.channels)
|
||||
document.legal ??= structuredClone(defaultSiteDocument.legal)
|
||||
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),
|
||||
type: z.enum(['course', 'service', 'tool']).default('course'),
|
||||
summary: z.string().max(300).default(''),
|
||||
content: z.string().max(6000).default(''),
|
||||
content: z.string().max(30000).default(''),
|
||||
image: localOrHttpUrl,
|
||||
href: localOrHttpUrl,
|
||||
status: z.enum(['draft', 'published', 'offline']).default('draft'),
|
||||
@@ -26,7 +26,7 @@ const articleSchema = z.object({
|
||||
type: z.enum(['news', 'statement', 'media']),
|
||||
title: z.string().min(1).max(120),
|
||||
summary: z.string().max(300),
|
||||
content: z.string().max(12000),
|
||||
content: z.string().max(50000),
|
||||
cover: localOrHttpUrl,
|
||||
publishedAt: z.string().max(40),
|
||||
status: z.enum(['draft', 'published', 'offline']),
|
||||
|
||||
Reference in New Issue
Block a user