Files
mce/templates/MetaLab-2026/static/css/posts.css
Victor_Jay 174d715e50 fix: 修复 CSRF 缺失 + 重写编辑器为三模式(所见即所得/源码/分屏)
CSRF 修复:
- /posts/new 和 /posts/:id/edit 原在独立 postPages group 中,仅含 Required 中间件,缺少 CSRF 下发
- 移动到 pages group(含 CSRF 中间件),用 per-route d.authMdw.Required() 弥补登录校验
- 同时消除 /posts/:id 与 /posts/new 潜在的路由冲突

编辑器重写:
- 添加三种编辑模式切换标签:所见即所得 / 源码 / 分屏预览
- 所见即所得(默认):contenteditable 编辑区 + 工具条(execCommand)
  支持加粗、斜体、H2/H3、链接、图片、引用、代码、列表、分割线
- 源码:纯 textarea,textarea 作为数据源始终保存 markdown 原文
- 分屏:左 textarea 编辑 + 右实时渲染预览(API 渲染)
- 模式切换时自动同步:WYSIWYG → HTML → Markdown → textarea
- 工具条在 WYSIWYG 模式用 execCommand,源码模式用文本包裹语法

实现细节:
- contenteditable ← textarea 双向同步(WYSIWYG 模式)
- 基本 HTML→Markdown 转换器(strong/em/h1-h6/a/pre/code/li/blockquote/img)
- 分屏布局用 JS toggled .split-mode class(非 :has(),兼容 Firefox)
- 编辑器初始时自动进 WYSIWYG 模式渲染已有内容
2026-05-27 18:35:13 +08:00

519 lines
9.4 KiB
CSS

/* ========== 帖子列表 ========== */
.posts-container {
max-width: 960px;
margin: 0 auto;
padding: 24px 16px;
}
.posts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.posts-header h1 {
font-size: 28px;
font-weight: 700;
color: #111;
margin: 0;
}
.post-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 20px 24px;
margin-bottom: 16px;
transition: box-shadow 0.2s;
}
.post-card:hover {
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.post-card-title a {
font-size: 20px;
font-weight: 600;
color: #111;
text-decoration: none;
}
.post-card-title a:hover {
color: #6366f1;
}
.post-card-meta {
display: flex;
gap: 16px;
margin-top: 8px;
font-size: 13px;
color: #6b7280;
}
.post-card-summary {
margin-top: 12px;
color: #4b5563;
font-size: 14px;
line-height: 1.6;
}
/* ========== 帖子详情 ========== */
.post-detail {
max-width: 800px;
margin: 0 auto;
padding: 32px 16px;
}
.post-detail-header {
margin-bottom: 32px;
padding-bottom: 20px;
border-bottom: 1px solid #e5e7eb;
}
.post-detail-title {
font-size: 32px;
font-weight: 700;
color: #111;
margin: 0 0 12px 0;
line-height: 1.3;
}
.post-detail-meta {
display: flex;
gap: 16px;
align-items: center;
font-size: 14px;
color: #6b7280;
}
.post-detail-body {
font-size: 16px;
line-height: 1.8;
color: #1f2937;
}
.post-detail-body h1,
.post-detail-body h2,
.post-detail-body h3 {
margin-top: 24px;
margin-bottom: 12px;
}
.post-detail-body p {
margin-bottom: 12px;
}
.post-detail-body pre {
background: #f3f4f6;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
font-size: 14px;
line-height: 1.5;
}
.post-detail-body code {
background: #f3f4f6;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
}
.post-detail-body img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
.post-detail-body blockquote {
border-left: 4px solid #6366f1;
padding-left: 16px;
color: #6b7280;
margin: 16px 0;
}
.post-status {
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.post-status-draft { background: #f3f4f6; color: #6b7280; }
.post-status-pending { background: #fef3c7; color: #d97706; }
.post-status-approved { background: #d1fae5; color: #059669; }
.post-status-rejected { background: #fef2f2; color: #dc2626; }
.post-status-locked { background: #fee2e2; color: #b91c1c; }
.post-reject-reason {
margin-top: 20px;
padding: 12px 16px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
color: #b91c1c;
font-size: 14px;
}
.post-actions {
margin-top: 24px;
display: flex;
gap: 12px;
}
/* ========== 编辑器 ========== */
.editor-container {
max-width: 1024px;
margin: 0 auto;
padding: 32px 16px;
}
.editor-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 24px;
}
.post-form .form-group {
margin-bottom: 16px;
}
.post-form label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 6px;
}
.form-input {
width: 100%;
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
box-sizing: border-box;
}
/* ========== 模式标签 ========== */
.editor-modes {
display: flex;
gap: 4px;
margin-bottom: 0;
}
.mode-tab {
padding: 8px 18px;
border: 1px solid #d1d5db;
border-bottom: none;
border-radius: 8px 8px 0 0;
background: #f9fafb;
color: #6b7280;
font-size: 13px;
font-weight: 500;
cursor: pointer;
position: relative;
z-index: 1;
}
.mode-tab.active {
background: #fff;
color: #6366f1;
border-color: #d1d5db;
z-index: 2;
margin-bottom: -1px;
padding-bottom: 9px;
}
/* ========== 工具条 ========== */
.editor-toolbar {
display: flex;
align-items: center;
gap: 2px;
padding: 6px 8px;
background: #fff;
border: 1px solid #d1d5db;
border-top: none;
border-radius: 0 0 8px 8px;
min-height: 40px;
flex-wrap: wrap;
}
/* 编辑区上边距 */
.editor-toolbar + .editor-body,
.editor-modes + .editor-body {
margin-top: 0;
}
.tb-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 30px;
border: none;
border-radius: 4px;
background: transparent;
color: #4b5563;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: background 0.15s;
}
.tb-btn:hover {
background: #f3f4f6;
color: #111;
}
.tb-btn svg {
width: 18px;
height: 18px;
}
.tb-sep {
width: 1px;
height: 20px;
background: #e5e7eb;
margin: 0 4px;
}
/* ========== 编辑区 ========== */
.editor-body {
position: relative;
clear: both;
overflow: hidden;
}
.form-textarea {
width: 100%;
padding: 12px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 15px;
font-family: 'Menlo', 'Consolas', 'Monaco', monospace;
line-height: 1.7;
resize: vertical;
box-sizing: border-box;
min-height: 400px;
}
/* WYSIWYG 编辑器 */
.wysiwyg-editor {
min-height: 400px;
padding: 14px 18px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
line-height: 1.8;
color: #1f2937;
outline: none;
word-wrap: break-word;
overflow-wrap: break-word;
}
.wysiwyg-editor:focus {
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99,102,241,0.15);
}
.wysiwyg-editor h1 { font-size: 2em; margin: 0.5em 0 0.3em; }
.wysiwyg-editor h2 { font-size: 1.6em; margin: 0.5em 0 0.3em; }
.wysiwyg-editor h3 { font-size: 1.3em; margin: 0.4em 0 0.25em; }
.wysiwyg-editor p { margin: 0 0 0.5em; }
.wysiwyg-editor pre {
background: #f3f4f6;
padding: 12px 16px;
border-radius: 6px;
font-family: 'Menlo','Consolas',monospace;
font-size: 14px;
overflow-x: auto;
margin: 0.5em 0;
}
.wysiwyg-editor code {
background: #f3f4f6;
padding: 1px 5px;
border-radius: 3px;
font-family: 'Menlo','Consolas',monospace;
font-size: 0.9em;
}
.wysiwyg-editor blockquote {
border-left: 4px solid #6366f1;
padding: 2px 14px;
color: #6b7280;
margin: 0.5em 0;
}
.wysiwyg-editor ul, .wysiwyg-editor ol {
padding-left: 24px;
margin: 0.4em 0;
}
.wysiwyg-editor li {
margin-bottom: 2px;
}
.wysiwyg-editor img {
max-width: 100%;
border-radius: 6px;
}
.wysiwyg-editor a {
color: #6366f1;
}
.wysiwyg-editor hr {
border: none;
border-top: 2px solid #e5e7eb;
margin: 1em 0;
}
.wysiwyg-editor table {
border-collapse: collapse;
width: 100%;
margin: 0.5em 0;
}
.wysiwyg-editor th, .wysiwyg-editor td {
border: 1px solid #d1d5db;
padding: 6px 12px;
text-align: left;
}
/* ========== 分屏预览 ========== */
.editor-body.split-mode .form-textarea {
width: 50%;
float: left;
border-radius: 8px 0 0 8px;
border-right: none;
}
.split-preview {
width: 50%;
float: right;
padding: 0;
box-sizing: border-box;
overflow-y: auto;
}
.split-preview .split-preview-content {
padding: 14px 18px;
background: #f9fafb;
border: 1px solid #d1d5db;
border-radius: 0 8px 8px 0;
min-height: 400px;
font-size: 15px;
line-height: 1.8;
}
.split-preview .split-preview-content h1,
.split-preview .split-preview-content h2,
.split-preview .split-preview-content h3 {
margin-top: 18px;
margin-bottom: 8px;
}
.split-preview .split-preview-content pre {
background: #e5e7eb;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
font-size: 13px;
}
.split-preview .split-preview-content blockquote {
border-left: 4px solid #6366f1;
padding-left: 14px;
color: #6b7280;
}
/* ========== 404 ========== */
.error-404 {
text-align: center;
padding: 80px 0;
}
.error-404 h1 {
font-size: 72px;
color: #d1d5db;
margin: 0;
}
.error-404 p {
font-size: 18px;
color: #6b7280;
margin: 12px 0 24px;
}
/* ========== 空状态 ========== */
.empty-state {
text-align: center;
padding: 60px 0;
color: #9ca3af;
font-size: 16px;
}
.search-hint {
margin-bottom: 16px;
color: #6b7280;
font-size: 14px;
}
/* ========== 按钮 ========== */
.btn {
display: inline-block;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: background 0.2s;
}
.btn-primary {
background: #6366f1;
color: #fff;
}
.btn-primary:hover { background: #4f46e5; }
.btn-secondary {
background: #f3f4f6;
color: #374151;
}
.btn-secondary:hover { background: #e5e7eb; }
/* ========== 分页 ========== */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin-top: 32px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
}
.page-link {
padding: 8px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
color: #374151;
text-decoration: none;
font-size: 14px;
}
.page-link:hover { background: #f3f4f6; }
.page-info {
font-size: 14px;
color: #6b7280;
}
/* ========== 错误消息 ========== */
.error-message {
padding: 12px 16px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
color: #b91c1c;
margin-bottom: 16px;
}