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 模式渲染已有内容
519 lines
9.4 KiB
CSS
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;
|
|
}
|