feat(web): 文章编辑器重设计 — 公众号风格三栏布局 + styled-block 自定义模块

- 左栏样式组件库(标题/内容/区块 14 种模板,5 种配色主题)
- 中间 Notion 风格编辑区(标题置顶 + wangEditor + 自定义 styled-block)
- 右栏 iPhone 仿真预览(匹配小程序暖奶油配色)
- 设置面板移至 Drawer 抽屉按需打开
- 注册 wangEditor 自定义模块保留模板内联样式
- 使用 snabbdom VNode + insertNode API 解决样式被剥离问题
This commit is contained in:
iven
2026-05-11 02:18:24 +08:00
parent 4788e19a1d
commit f4b09858c4
11 changed files with 1362 additions and 558 deletions

View File

@@ -0,0 +1,156 @@
/**
* 文章编辑器样式模板数据
* 所有 HTML 片段使用内联样式,通过 wangEditor 自定义 styled-block 模块保留样式。
* 模板中使用 {{primary}} / {{primaryLight}} 占位符,由 applyTheme() 替换为实际颜色值。
*/
export interface StyleTemplate {
id: string;
name: string;
category: 'heading' | 'content' | 'block';
preview: string;
html: string;
}
export interface ColorTheme {
id: string;
name: string;
primary: string;
primaryLight: string;
}
export const COLOR_THEMES: ColorTheme[] = [
{ id: 'green', name: '清新绿', primary: '#16a34a', primaryLight: '#dcfce7' },
{ id: 'blue', name: '专业蓝', primary: '#2563eb', primaryLight: '#dbeafe' },
{ id: 'red', name: '暖橘红', primary: '#C4623A', primaryLight: '#F0DDD4' },
{ id: 'purple', name: '雅致紫', primary: '#7c3aed', primaryLight: '#ede9fe' },
{ id: 'amber', name: '琥珀金', primary: '#d97706', primaryLight: '#fef3c7' },
];
const W = 'data-w-e-type="styled-block"';
export const HEADING_TEMPLATES: StyleTemplate[] = [
{
id: 'heading-classic',
name: '经典',
category: 'heading',
preview: '▎左边框标题',
html: `<div ${W} style="border-left: 4px solid {{primary}}; padding-left: 12px; font-size: 20px; font-weight: 700; color: #1a1a1a; margin: 24px 0 12px;">标题文本</div>`,
},
{
id: 'heading-simple',
name: '简约',
category: 'heading',
preview: '下划线标题 ──',
html: `<div ${W} style="border-bottom: 2px solid {{primary}}; padding-bottom: 8px; font-size: 20px; font-weight: 700; color: #1a1a1a; margin: 24px 0 12px; display: inline-block;">标题文本</div>`,
},
{
id: 'heading-rounded',
name: '圆标',
category: 'heading',
preview: '■ 标签式标题',
html: `<div ${W} style="display: inline-block; background: {{primaryLight}}; color: {{primary}}; padding: 4px 14px; border-radius: 4px; font-size: 18px; font-weight: 700; margin: 24px 0 12px;">标题文本</div>`,
},
{
id: 'heading-centered',
name: '居中',
category: 'heading',
preview: '居中标题',
html: `<div ${W} style="text-align: center; font-size: 20px; font-weight: 700; color: #1a1a1a; margin: 28px 0 12px;">标题文本</div>`,
},
];
export const CONTENT_TEMPLATES: StyleTemplate[] = [
{
id: 'blockquote',
name: '引用框',
category: 'content',
preview: '▎引用文字...',
html: `<div ${W} style="border-left: 3px solid {{primary}}; padding: 12px 16px; margin: 16px 0; background: #f9fafb; border-radius: 0 8px 8px 0; font-size: 15px; line-height: 1.8; color: #5a554f; font-style: italic;">引用内容请在此处编辑</div>`,
},
{
id: 'tip-warning',
name: '提示框 · 警告',
category: 'content',
preview: '⚠ 温馨提示',
html: `<div ${W} style="background: #fffbeb; border: 1px solid #fde68a; border-radius: 8px; padding: 14px 16px; margin: 16px 0; font-size: 15px; line-height: 1.8; color: #92400e;"><strong>⚠ 温馨提示:</strong>请在此处编辑警告内容。</div>`,
},
{
id: 'tip-info',
name: '提示框 · 信息',
category: 'content',
preview: ' 补充说明',
html: `<div ${W} style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 14px 16px; margin: 16px 0; font-size: 15px; line-height: 1.8; color: #1e40af;"><strong> 补充说明:</strong>请在此处编辑信息内容。</div>`,
},
{
id: 'list-ordered',
name: '有序列表',
category: 'content',
preview: '① ② ③',
html: `<div ${W} style="padding-left: 20px; margin: 16px 0; font-size: 16px; line-height: 2; color: #3a3a3c;"><div style="position: relative; padding-left: 8px; margin-bottom: 4px;"><span style="position: absolute; left: -20px; color: {{primary}}; font-weight: 600;">1.</span>第一项内容</div><div style="position: relative; padding-left: 8px; margin-bottom: 4px;"><span style="position: absolute; left: -20px; color: {{primary}}; font-weight: 600;">2.</span>第二项内容</div><div style="position: relative; padding-left: 8px; margin-bottom: 4px;"><span style="position: absolute; left: -20px; color: {{primary}}; font-weight: 600;">3.</span>第三项内容</div></div>`,
},
{
id: 'list-unordered',
name: '无序列表',
category: 'content',
preview: '• • •',
html: `<div ${W} style="padding-left: 20px; margin: 16px 0; font-size: 16px; line-height: 2; color: #3a3a3c;"><div style="position: relative; padding-left: 8px; margin-bottom: 4px;"><span style="position: absolute; left: -14px; color: {{primary}};">●</span>第一项内容</div><div style="position: relative; padding-left: 8px; margin-bottom: 4px;"><span style="position: absolute; left: -14px; color: {{primary}};">●</span>第二项内容</div><div style="position: relative; padding-left: 8px; margin-bottom: 4px;"><span style="position: absolute; left: -14px; color: {{primary}};">●</span>第三项内容</div></div>`,
},
{
id: 'card-image-text',
name: '图文卡片',
category: 'content',
preview: '[图] 文字说明',
html: `<div ${W} style="display: flex; gap: 12px; margin: 16px 0; padding: 12px; background: #f9fafb; border-radius: 8px; align-items: center;"><div style="width: 100px; height: 80px; background: #e5e7eb; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #9ca3af; font-size: 12px; flex-shrink: 0;">图片</div><div style="flex: 1; font-size: 14px; line-height: 1.8; color: #3a3a3c;">图文卡片的文字描述内容请在此处编辑。</div></div>`,
},
{
id: 'card-data',
name: '数据卡片',
category: 'content',
preview: '血压 120/80',
html: `<div ${W} style="display: flex; gap: 12px; margin: 16px 0; flex-wrap: wrap;"><div style="flex: 1; min-width: 120px; background: {{primaryLight}}; border-radius: 8px; padding: 14px; text-align: center;"><div style="font-size: 24px; font-weight: 700; color: {{primary}};">120/80</div><div style="font-size: 13px; color: #5a554f; margin-top: 4px;">血压 (mmHg)</div></div><div style="flex: 1; min-width: 120px; background: #f3f4f6; border-radius: 8px; padding: 14px; text-align: center;"><div style="font-size: 24px; font-weight: 700; color: #1a1a1a;">72</div><div style="font-size: 13px; color: #5a554f; margin-top: 4px;">心率 (bpm)</div></div></div>`,
},
];
export const BLOCK_TEMPLATES: StyleTemplate[] = [
{
id: 'divider',
name: '分割线',
category: 'block',
preview: '─ ─ ─ ─',
html: `<div ${W} style="border: none; border-top: 1px dashed #d1d5db; margin: 24px 0; height: 0;"></div>`,
},
{
id: 'section-header',
name: '章节标题',
category: 'block',
preview: '§ 带编号章节',
html: `<div ${W} style="margin: 24px 0 12px; display: flex; align-items: center; gap: 10px;"><span style="display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; background: {{primary}}; color: #fff; border-radius: 50%; font-size: 14px; font-weight: 700; flex-shrink: 0;">1</span><span style="font-size: 18px; font-weight: 700; color: #1a1a1a;">章节标题</span></div>`,
},
{
id: 'table',
name: '数据表格',
category: 'block',
preview: '⊞ 3×2 表格',
html: `<div ${W} style="display: grid; grid-template-columns: 1fr 1fr 1fr; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; margin: 16px 0; font-size: 14px;"><div style="background: {{primaryLight}}; padding: 10px 12px; font-weight: 600; color: {{primary}}; border-bottom: 1px solid #e5e7eb; border-right: 1px solid #e5e7eb;">项目</div><div style="background: {{primaryLight}}; padding: 10px 12px; font-weight: 600; color: {{primary}}; border-bottom: 1px solid #e5e7eb; border-right: 1px solid #e5e7eb;">数值</div><div style="background: {{primaryLight}}; padding: 10px 12px; font-weight: 600; color: {{primary}}; border-bottom: 1px solid #e5e7eb;">备注</div><div style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; border-right: 1px solid #e5e7eb;">收缩压</div><div style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; border-right: 1px solid #e5e7eb;">120 mmHg</div><div style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb;">正常</div><div style="background: #f9fafb; padding: 10px 12px; border-right: 1px solid #e5e7eb;">舒张压</div><div style="background: #f9fafb; padding: 10px 12px; border-right: 1px solid #e5e7eb;">80 mmHg</div><div style="background: #f9fafb; padding: 10px 12px;">正常</div></div>`,
},
];
/** 所有模板合并列表 */
export const ALL_TEMPLATES: StyleTemplate[] = [
...HEADING_TEMPLATES,
...CONTENT_TEMPLATES,
...BLOCK_TEMPLATES,
];
/** 将模板 HTML 中的颜色占位符替换为主题实际颜色值 */
export function applyTheme(html: string, theme: ColorTheme): string {
return html
.replaceAll('{{primary}}', theme.primary)
.replaceAll('{{primaryLight}}', theme.primaryLight);
}
/** 根据 ID 查找颜色主题 */
export function getColorTheme(themeId: string): ColorTheme {
return COLOR_THEMES.find((t) => t.id === themeId) ?? COLOR_THEMES[0];
}