Files
nj/docs/opendesign/screens/editor.html
iven b320641d9c
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): 全链路验证修复 — 编译错误/CORS/迁移/启动脚本
前端修复:
- calendar_page: 移除不存在的 JournalEntry.content getter
- responsive_scaffold: 移除不存在的 notchThickness 参数
- splash_page: SingleTickerProvider → TickerProvider (多 AnimationController)
- profile_page: UserRoleType.name → .code (修复运行时崩溃)
- 导入缺失的 user.dart

后端修复:
- class_service: generate_class_code 取 UUID 后6位(随机部分)避免碰撞
- diary_role_seed: 移除不存在的 id 列,使用复合主键 ON CONFLICT

基础设施:
- config/default.toml: CORS 改为通配符(开发模式)
- scripts/dev.sh: 统一启动脚本(自动清理端口)
- docs/opendesign/: Open Design 设计规格 HTML 原型稿

验证结果: flutter analyze 0 error, cargo test 77/77 通过, 17个页面全部渲染正常
2026-06-02 01:03:58 +08:00

1136 lines
38 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=390, height=844, initial-scale=1, viewport-fit=cover">
<title>暖记 — 手账编辑器</title>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../css/tokens.css">
<link rel="stylesheet" href="../css/components.css">
<link rel="stylesheet" href="../css/editor-common.css">
<style>
body {
width: 390px;
height: 844px;
overflow: hidden;
background: var(--bg);
position: relative;
}
/* Global focus styles */
button:focus-visible, a:focus-visible, [role="tab"]:focus-visible {
box-shadow: 0 0 0 3px var(--focus-ring);
outline: none;
}
/* Top toolbar */
.editor-topbar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: calc(var(--safe-top) + 44px);
padding-top: var(--safe-top);
display: flex;
align-items: center;
justify-content: space-between;
padding-left: var(--space-3);
padding-right: var(--space-3);
background: var(--surface);
border-bottom: 1px solid var(--border-soft);
z-index: 100;
}
.topbar-left {
display: flex;
align-items: center;
gap: 2px;
}
.topbar-right {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.topbar-btn {
min-width: 36px;
min-height: 36px;
border-radius: var(--radius-pill);
border: none;
background: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--fg);
transition: background var(--motion-fast);
}
.topbar-btn:hover { background: var(--surface-warm); }
.topbar-btn svg { width: 22px; height: 22px; }
.topbar-center {
font-family: var(--font-display);
font-size: var(--text-base);
font-weight: 600;
color: var(--fg-2);
}
.save-btn {
padding: 6px 16px;
background: var(--accent);
color: var(--accent-on);
border: none;
border-radius: var(--radius-pill);
font-family: var(--font-display);
font-size: var(--text-sm);
font-weight: 600;
cursor: pointer;
min-height: 36px;
transition: all var(--motion-fast);
}
.save-btn:hover { background: var(--accent-hover); }
/* Date & mood strip */
.date-strip {
position: absolute;
top: calc(var(--safe-top) + 44px);
left: 0;
right: 0;
padding: var(--space-2) var(--space-4);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--surface);
z-index: 90;
}
.date-info {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--muted);
}
.date-info strong {
color: var(--fg);
font-weight: 600;
}
.mood-quick {
display: flex;
gap: 6px;
align-items: center;
}
.mood-mini {
width: 24px;
height: 24px;
border-radius: 50%;
border: 1.5px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
background: var(--surface);
cursor: pointer;
transition: all var(--motion-fast);
padding: 6px;
min-width: 36px;
min-height: 36px;
box-sizing: content-box;
background-clip: content-box;
}
.mood-mini.selected {
border-color: var(--accent);
background: var(--surface-warm);
}
/* Canvas area */
.canvas-area {
position: absolute;
top: calc(var(--safe-top) + 44px + 40px);
left: var(--space-3);
right: var(--space-3);
bottom: 72px;
background: var(--surface);
border-radius: var(--radius-md);
box-shadow: var(--elev-soft);
border: 1px solid var(--border-soft);
overflow: hidden;
padding: var(--space-5);
}
/* Dotted grid background */
.canvas-grid {
position: absolute;
inset: 0;
background-image: radial-gradient(circle, var(--border) 1px, transparent 1px);
background-size: 24px 24px;
opacity: 0.5;
pointer-events: none;
}
.journal-content {
position: relative;
z-index: 1;
min-height: 100%;
}
.journal-title {
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 700;
color: var(--fg);
margin-bottom: var(--space-3);
border: none;
outline: none;
width: 100%;
background: transparent;
}
.journal-title::placeholder { color: var(--border); }
.journal-body {
font-size: var(--text-base);
line-height: 1.8;
color: var(--fg-2);
border: none;
outline: none;
width: 100%;
background: transparent;
resize: none;
min-height: 200px;
}
.journal-body::placeholder { color: var(--meta); font-family: var(--font-handwritten); }
/* Placed sticker example */
.placed-sticker {
position: absolute;
font-size: 32px;
cursor: move;
user-select: none;
transition: transform 0.1s;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.placed-sticker:hover { transform: scale(1.1); }
/* Washi tape decoration */
.washi-tape {
position: absolute;
top: 12px;
right: 30px;
width: 90px;
height: 20px;
background: repeating-linear-gradient(
45deg,
color-mix(in oklab, var(--tertiary), transparent 30%),
color-mix(in oklab, var(--tertiary), transparent 30%) 4px,
color-mix(in oklab, var(--surface-warm), transparent 30%) 4px,
color-mix(in oklab, var(--surface-warm), transparent 30%) 8px
);
transform: rotate(-8deg);
border-radius: 2px;
opacity: 0.8;
}
/* Photo placeholder in journal */
.photo-in-journal {
width: 100%;
height: 140px;
border-radius: var(--radius-sm);
background: linear-gradient(135deg, var(--surface-warm), var(--border-soft));
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 8px;
margin: var(--space-4) 0;
border: 2px dashed var(--border);
cursor: pointer;
transition: all var(--motion-fast);
}
.photo-in-journal:hover { border-color: var(--accent); background: var(--surface-warm); }
.photo-in-journal svg { width: 28px; height: 28px; color: var(--muted); }
.photo-in-journal span { font-size: var(--text-sm); color: var(--muted); }
/* Placed photo */
.placed-photo {
width: 100%;
height: 140px;
border-radius: var(--radius-sm);
background: linear-gradient(135deg, var(--surface-warm), var(--tertiary-soft));
margin: var(--space-4) 0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.placed-photo::after {
content: '\01F4F7 拍照';
font-size: var(--text-sm);
color: var(--muted);
}
/* Bottom toolbar */
.editor-toolbar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 72px;
background: var(--surface);
border-top: 1px solid var(--border-soft);
z-index: 100;
padding-bottom: var(--safe-bottom);
}
.toolbar-main {
display: flex;
justify-content: space-around;
align-items: center;
height: 100%;
padding: 0 var(--space-4);
}
.tool-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
background: none;
border: none;
cursor: pointer;
padding: 4px 6px;
min-width: 36px;
min-height: 36px;
border-radius: var(--radius-sm);
transition: all var(--motion-fast);
color: var(--muted);
}
.tool-btn:hover { color: var(--accent); background: var(--surface-warm); }
.tool-btn.active { color: var(--accent); }
.tool-btn svg { width: 20px; height: 20px; }
.tool-btn span {
font-size: 10px;
font-weight: 500;
}
/* Expanded tool panel (sticker example) */
.tool-panel {
position: absolute;
bottom: 72px;
left: 0;
right: 0;
max-height: 260px;
overflow-y: auto;
background: var(--surface);
border-top: 1px solid var(--border-soft);
padding: var(--space-3) var(--space-4);
transform: translateY(100%);
transition: transform var(--motion-base) var(--ease-bounce);
z-index: 95;
}
.tool-panel.open { transform: translateY(0); }
.panel-tabs {
display: flex;
gap: var(--space-3);
margin-bottom: var(--space-4);
overflow-x: auto;
scrollbar-width: none;
}
.panel-tabs::-webkit-scrollbar { display: none; }
.panel-tab {
padding: 8px 16px;
min-height: 44px;
border-radius: var(--radius-pill);
font-size: var(--text-sm);
font-weight: 500;
background: var(--surface-warm);
color: var(--fg-2);
border: none;
cursor: pointer;
white-space: nowrap;
transition: all var(--motion-fast);
}
.panel-tab.active { background: var(--accent); color: var(--accent-on); }
.sticker-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: var(--space-3);
}
.sticker-item {
aspect-ratio: 1;
border-radius: var(--radius-sm);
background: var(--surface-warm);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
cursor: pointer;
transition: all var(--motion-fast) var(--ease-bounce);
border: 1px solid transparent;
min-width: 36px;
min-height: 36px;
}
.sticker-item:hover { transform: scale(1.1); border-color: var(--accent); }
.sticker-item:active { transform: scale(0.95); }
/* Text formatting bar */
.format-bar {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) 0;
margin-bottom: var(--space-3);
border-bottom: 1px solid var(--border-soft);
}
.format-btn {
min-width: 36px;
min-height: 36px;
border-radius: 6px;
border: none;
background: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--muted);
font-size: var(--text-sm);
font-weight: 600;
transition: all var(--motion-fast);
}
.format-btn:hover { background: var(--surface-warm); color: var(--fg); }
.format-btn.active { background: var(--accent); color: var(--accent-on); }
.format-divider {
width: 1px;
height: 20px;
background: var(--border);
}
.color-dot {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: all var(--motion-fast);
padding: 8px;
min-width: 36px;
min-height: 36px;
box-sizing: content-box;
background-clip: content-box;
}
.color-dot:hover { transform: scale(1.15); }
.color-dot.active { border-color: var(--fg); }
/* Brush tool panel */
.brush-overlay {
position: absolute;
inset: 0;
background: rgba(45, 36, 32, 0.3);
z-index: 200;
opacity: 0;
pointer-events: none;
transition: opacity var(--motion-base);
}
.brush-overlay.open { opacity: 1; pointer-events: auto; }
.brush-panel {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 280px;
background: var(--surface);
border-top-left-radius: var(--radius-lg);
border-top-right-radius: var(--radius-lg);
box-shadow: var(--elev-float);
z-index: 210;
transform: translateY(100%);
transition: transform var(--motion-base) var(--ease-bounce);
padding: var(--space-4) var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.brush-panel.open { transform: translateY(0); }
.brush-panel-handle {
width: 36px;
height: 4px;
border-radius: 2px;
background: var(--border);
align-self: center;
margin-bottom: var(--space-1);
}
.brush-tools-row {
display: flex;
justify-content: space-around;
gap: var(--space-2);
}
.brush-tool-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
background: none;
border: 2px solid transparent;
border-radius: var(--radius-sm);
padding: var(--space-1) var(--space-2);
min-width: 36px;
min-height: 36px;
cursor: pointer;
color: var(--muted);
transition: all var(--motion-fast);
}
.brush-tool-btn:hover { background: var(--surface-warm); color: var(--fg); }
.brush-tool-btn.active { border-color: var(--accent); color: var(--accent); background: var(--surface-warm); }
.brush-tool-btn svg { width: 24px; height: 24px; }
.brush-tool-btn span { font-size: 11px; font-weight: 500; }
.brush-size-row {
display: flex;
align-items: center;
gap: var(--space-3);
}
.brush-size-label {
font-size: var(--text-xs);
color: var(--muted);
font-weight: 500;
min-width: 32px;
}
.brush-size-value {
font-size: var(--text-sm);
font-weight: 600;
color: var(--fg);
min-width: 36px;
text-align: right;
}
.brush-slider {
flex: 1;
-webkit-appearance: none;
appearance: none;
height: 4px;
border-radius: 2px;
background: var(--border);
outline: none;
}
.brush-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
box-shadow: 0 1px 4px rgba(0,0,0,0.15);
}
.brush-colors-row {
display: flex;
align-items: center;
gap: var(--space-3);
overflow-x: auto;
scrollbar-width: none;
padding: var(--space-1) 0;
}
.brush-colors-row::-webkit-scrollbar { display: none; }
.brush-color-dot {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2.5px solid transparent;
cursor: pointer;
transition: all var(--motion-fast);
flex-shrink: 0;
padding: 6px;
min-width: 36px;
min-height: 36px;
box-sizing: content-box;
background-clip: content-box;
}
.brush-color-dot:hover { transform: scale(1.15); }
.brush-color-dot.active { border-color: var(--fg); box-shadow: 0 0 0 2px var(--surface); }
.brush-color-more {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px dashed var(--border);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
min-width: 36px;
min-height: 36px;
padding: 6px;
box-sizing: content-box;
background: var(--surface-warm);
color: var(--muted);
font-size: 14px;
transition: all var(--motion-fast);
}
.brush-color-more:hover { border-color: var(--accent); color: var(--accent); }
.brush-opacity-row {
display: flex;
align-items: center;
gap: var(--space-3);
transition: opacity var(--motion-fast);
}
.brush-opacity-row.disabled { opacity: 0.35; pointer-events: none; }
.brush-opacity-label {
font-size: var(--text-xs);
color: var(--muted);
font-weight: 500;
min-width: 32px;
}
.brush-opacity-value {
font-size: var(--text-sm);
font-weight: 600;
color: var(--fg);
min-width: 36px;
text-align: right;
}
/* Undo/Redo buttons */
.topbar-undo-redo {
display: flex;
align-items: center;
gap: 2px;
}
.undo-btn, .redo-btn {
min-width: 32px;
min-height: 32px;
border-radius: var(--radius-pill);
border: none;
background: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--muted);
transition: all var(--motion-fast);
}
.undo-btn:hover:not(:disabled), .redo-btn:hover:not(:disabled) { background: var(--surface-warm); color: var(--fg); }
.undo-btn:disabled, .redo-btn:disabled { opacity: 0.35; cursor: default; }
.undo-btn svg, .redo-btn svg { width: 18px; height: 18px; }
/* Auto-save indicator */
.autosave-status {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--success);
font-weight: 500;
}
.autosave-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--success);
}
/* Tag panel */
.tag-panel {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 200px;
background: var(--surface);
border-top-left-radius: var(--radius-lg);
border-top-right-radius: var(--radius-lg);
box-shadow: var(--elev-float);
z-index: 210;
transform: translateY(100%);
transition: transform var(--motion-base) var(--ease-bounce);
padding: var(--space-3) var(--space-5) var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.tag-panel.open { transform: translateY(0); }
.tag-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.tag-panel-title {
font-family: var(--font-display);
font-size: var(--text-md);
font-weight: 700;
color: var(--fg);
}
.tag-panel-close {
width: 28px;
height: 28px;
min-width: 44px;
min-height: 44px;
border: none;
background: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--muted);
border-radius: var(--radius-pill);
transition: all var(--motion-fast);
box-sizing: content-box;
background-clip: content-box;
}
.tag-panel-close:hover { color: var(--fg); background: var(--surface-warm); }
.tag-panel-close svg { width: 18px; height: 18px; }
.tag-selected-area {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
min-height: 32px;
}
.tag-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 9999px;
background: var(--surface-warm);
color: var(--accent);
font-size: 13px;
font-weight: 500;
transition: all var(--motion-fast);
}
.tag-pill .remove {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent);
color: var(--surface);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
cursor: pointer;
border: none;
line-height: 1;
transition: background var(--motion-fast);
}
.tag-pill .remove:hover { background: var(--accent-hover); }
.tag-input-row {
display: flex;
}
.tag-input {
flex: 1;
height: 44px;
border: 1.5px solid var(--border);
border-radius: var(--radius-pill);
padding: 0 var(--space-4);
font-size: var(--text-sm);
color: var(--fg);
background: var(--bg);
outline: none;
transition: border-color var(--motion-fast);
font-family: var(--font-body);
}
.tag-input::placeholder { color: var(--meta); }
.tag-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--shadow-input-focus); }
.tag-suggest-label {
font-size: var(--text-xs);
color: var(--muted);
font-weight: 600;
}
.tag-suggest-row {
display: flex;
gap: var(--space-2);
overflow-x: auto;
scrollbar-width: none;
padding: 2px 0;
}
.tag-suggest-row::-webkit-scrollbar { display: none; }
.tag-suggest-item {
display: inline-flex;
align-items: center;
padding: 6px 14px;
border-radius: 9999px;
background: var(--surface-warm);
color: var(--fg-2);
font-size: var(--text-sm);
font-weight: 500;
border: 1px solid transparent;
cursor: pointer;
white-space: nowrap;
min-height: 44px;
transition: all var(--motion-fast);
}
.tag-suggest-item:hover { border-color: var(--accent); color: var(--accent); }
</style>
</head>
<body>
<!-- Top toolbar -->
<div class="editor-topbar">
<div class="topbar-left">
<button class="topbar-btn" aria-label="返回">
<svg viewBox="0 0 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>
</button>
</div>
<div class="topbar-center">5月31日 · 周日</div>
<div class="topbar-right">
<div class="topbar-undo-redo">
<button class="undo-btn" disabled aria-label="撤销">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 10h10a5 5 0 015 5v2"/><polyline points="3 10 7 6"/><polyline points="3 10 7 14"/></svg>
</button>
<button class="redo-btn" disabled aria-label="重做">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10H11a5 5 0 00-5 5v2"/><polyline points="21 10 17 6"/><polyline points="21 10 17 14"/></svg>
</button>
</div>
<div class="autosave-status" aria-live="polite">
<span class="autosave-dot" aria-hidden="true"></span>
<span>已保存</span>
</div>
<button class="topbar-btn" aria-label="标签" onclick="toggleTagPanel()">
<svg viewBox="0 0 22 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
</button>
<button class="save-btn" aria-label="保存日记">保存</button>
</div>
</div>
<!-- Date & mood strip - standardized 5 moods -->
<div class="date-strip">
<div class="date-info">
<strong>14:32</strong>
<span>· 晴 26°</span>
</div>
<div class="mood-quick">
<button class="mood-mini" aria-label="开心">😊</button>
<button class="mood-mini selected" aria-label="平静">😐</button>
<button class="mood-mini" aria-label="难过">😢</button>
<button class="mood-mini" aria-label="生气">😡</button>
<button class="mood-mini" aria-label="思考">🤔</button>
</div>
</div>
<!-- Canvas -->
<div class="canvas-area">
<div class="canvas-grid" aria-hidden="true"></div>
<div class="washi-tape" aria-hidden="true"></div>
<!-- Placed sticker decorations -->
<div class="placed-sticker" style="top: 8px; left: 8px;" aria-hidden="true">🌸</div>
<div class="placed-sticker" style="top: 140px; right: 12px;" aria-hidden="true"></div>
<div class="journal-content">
<input class="journal-title" type="text" value="图书馆的午后时光" placeholder="给日记起个标题..." aria-label="日记标题">
<!-- Format bar -->
<div class="format-bar">
<button class="format-btn active" aria-label="粗体" style="font-weight:800">B</button>
<button class="format-btn" aria-label="斜体" style="font-style:italic">I</button>
<button class="format-btn" aria-label="下划线" style="text-decoration:underline">U</button>
<div class="format-divider" aria-hidden="true"></div>
<button class="color-dot active" aria-label="黑色" style="background: #2D2420"></button>
<button class="color-dot" aria-label="珊瑚色" style="background: var(--accent)"></button>
<button class="color-dot" aria-label="绿色" style="background: var(--secondary)"></button>
<button class="color-dot" aria-label="蓝色" style="background: #5B7DB1"></button>
<div class="format-divider" aria-hidden="true"></div>
<button class="format-btn" aria-label="对齐方式">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><rect x="1" y="3" width="14" height="2" rx="1"/><rect x="1" y="7" width="10" height="2" rx="1"/><rect x="1" y="11" width="6" height="2" rx="1"/></svg>
</button>
</div>
<textarea class="journal-body" placeholder="开始写今天的日记..." rows="8" aria-label="日记内容">今天下午去图书馆自习,找了一个靠窗的位置。阳光从窗外洒进来,落在摊开的高数课本上,暖暖的。
不知不觉就学了三个小时,中间喝了一杯抹茶拿铁☕。虽然期末压力大,但看到窗外的樱花还在开,觉得一切都会好的。
晚上回宿舍路上,买了喜欢的草莓蛋糕犒劳自己 🍰</textarea>
<div class="placed-photo" style="background: linear-gradient(135deg, var(--secondary-soft), color-mix(in oklab, var(--secondary), transparent 10%))">
<div style="text-align:center;color:var(--secondary)">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="1.5" style="margin-bottom:4px" aria-hidden="true"><rect x="4" y="6" width="24" height="20" rx="3"/><circle cx="12" cy="14" r="3"/><path d="M4 22l6-6 4 4 6-8 8 10"/></svg>
<div style="font-size:12px">图书馆窗边的阳光 ☀</div>
</div>
</div>
</div>
</div>
<!-- Bottom toolbar -->
<div class="editor-toolbar">
<div class="toolbar-main">
<button class="tool-btn" aria-label="贴纸" onclick="togglePanel(this, 'sticker')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="9" cy="10" r="1.5" fill="currentColor"/><circle cx="15" cy="10" r="1.5" fill="currentColor"/><path d="M9 15c.8.8 2.2 1.2 3 1.2s2.2-.4 3-1.2"/></svg>
<span>贴纸</span>
</button>
<button class="tool-btn" aria-label="模板" onclick="togglePanel(this, 'template')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
<span>模板</span>
</button>
<button class="tool-btn" aria-label="画笔工具" onclick="toggleBrushPanel(this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/></svg>
<span>画笔</span>
</button>
<button class="tool-btn" aria-label="照片">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<span>照片</span>
</button>
<button class="tool-btn" aria-label="文字">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg>
<span>文字</span>
</button>
<button class="tool-btn" aria-label="更多选项">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
<span>更多</span>
</button>
</div>
</div>
<!-- Sticker panel (expandable) -->
<div class="tool-panel" id="stickerPanel">
<div class="panel-tabs" role="tablist">
<button class="panel-tab active" role="tab" aria-label="热门贴纸" aria-selected="true">热门</button>
<button class="panel-tab" role="tab" aria-label="可爱贴纸" aria-selected="false">可爱</button>
<button class="panel-tab" role="tab" aria-label="植物贴纸" aria-selected="false">植物</button>
<button class="panel-tab" role="tab" aria-label="天气贴纸" aria-selected="false">天气</button>
<button class="panel-tab" role="tab" aria-label="节日贴纸" aria-selected="false">节日</button>
<button class="panel-tab" role="tab" aria-label="手绘贴纸" aria-selected="false">手绘</button>
<button class="panel-tab" role="tab" aria-label="校园贴纸" aria-selected="false">校园</button>
</div>
<div class="sticker-grid">
<div class="sticker-item" aria-label="樱花贴纸">🌸</div>
<div class="sticker-item" aria-label="星星贴纸"></div>
<div class="sticker-item" aria-label="树叶贴纸">🌿</div>
<div class="sticker-item" aria-label="太阳贴纸">☀️</div>
<div class="sticker-item" aria-label="月亮贴纸">🌙</div>
<div class="sticker-item" aria-label="蛋糕贴纸">🍰</div>
<div class="sticker-item" aria-label="书本贴纸">📚</div>
<div class="sticker-item" aria-label="音乐贴纸">🎵</div>
<div class="sticker-item" aria-label="灯泡贴纸">💡</div>
<div class="sticker-item" aria-label="蝴蝶结贴纸">🎀</div>
<div class="sticker-item" aria-label="枫叶贴纸">🍂</div>
<div class="sticker-item" aria-label="咖啡贴纸"></div>
<div class="sticker-item" aria-label="彩虹贴纸">🌈</div>
<div class="sticker-item" aria-label="铅笔贴纸">✏️</div>
<div class="sticker-item" aria-label="蝴蝶贴纸">🦋</div>
</div>
</div>
<!-- Brush overlay & panel -->
<div class="brush-overlay" id="brushOverlay" onclick="closeBrushPanel()" aria-hidden="true"></div>
<div class="brush-panel" id="brushPanel" role="dialog" aria-label="画笔工具">
<div class="brush-panel-handle" aria-hidden="true"></div>
<!-- Tool selection -->
<div class="brush-tools-row">
<button class="brush-tool-btn active" aria-label="钢笔工具" data-tool="pen" onclick="selectBrushTool(this, 'pen')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3l4 4L7 21H3v-4L17 3z"/></svg>
<span>钢笔</span>
</button>
<button class="brush-tool-btn" aria-label="铅笔工具" data-tool="pencil" onclick="selectBrushTool(this, 'pencil')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20l1.5-5.5L18 2l4 4L9.5 18.5 4 20z"/><path d="M15 5l4 4"/></svg>
<span>铅笔</span>
</button>
<button class="brush-tool-btn" aria-label="马克笔工具" data-tool="marker" onclick="selectBrushTool(this, 'marker')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="3" width="14" height="8" rx="2" transform="rotate(45 13 7)"/><path d="M4 20l3-7"/></svg>
<span>马克笔</span>
</button>
<button class="brush-tool-btn" aria-label="橡皮工具" data-tool="eraser" onclick="selectBrushTool(this, 'eraser')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20H9L3.5 14.5a2 2 0 010-2.83l9.17-9.17a2 2 0 012.83 0L20 7"/><path d="M6 12l6 6"/></svg>
<span>橡皮</span>
</button>
</div>
<!-- Size slider -->
<div class="brush-size-row">
<span class="brush-size-label">粗细</span>
<input type="range" class="brush-slider" id="brushSizeSlider" min="1" max="20" value="2" aria-label="画笔粗细">
<span class="brush-size-value" id="brushSizeValue">2px</span>
</div>
<!-- Color selection -->
<div class="brush-colors-row">
<button class="brush-color-dot active" style="background:#2D2420" aria-label="黑色" data-color="#2D2420" onclick="selectBrushColor(this)"></button>
<button class="brush-color-dot" style="background:var(--accent)" aria-label="珊瑚色" data-color="#E07A5F" onclick="selectBrushColor(this)"></button>
<button class="brush-color-dot" style="background:var(--secondary)" aria-label="绿色" data-color="#81B29A" onclick="selectBrushColor(this)"></button>
<button class="brush-color-dot" style="background:var(--tertiary)" aria-label="金色" data-color="#F2CC8F" onclick="selectBrushColor(this)"></button>
<button class="brush-color-dot" style="background:var(--rose)" aria-label="玫瑰色" data-color="#D4A5A5" onclick="selectBrushColor(this)"></button>
<button class="brush-color-dot" style="background:#5B8FB9" aria-label="蓝色" data-color="#5B8FB9" onclick="selectBrushColor(this)"></button>
<button class="brush-color-dot" style="background:#9B72AA" aria-label="紫色" data-color="#9B72AA" onclick="selectBrushColor(this)"></button>
<button class="brush-color-dot" style="background:#C93D3D" aria-label="红色" data-color="#C93D3D" onclick="selectBrushColor(this)"></button>
<button class="brush-color-more" aria-label="更多颜色" onclick="openColorPicker()">+</button>
</div>
<!-- Opacity slider (marker only) -->
<div class="brush-opacity-row disabled" id="brushOpacityRow">
<span class="brush-opacity-label">透明度</span>
<input type="range" class="brush-slider" id="brushOpacitySlider" min="0" max="100" value="50" aria-label="画笔透明度">
<span class="brush-opacity-value" id="brushOpacityValue">50%</span>
</div>
</div>
<!-- Tag panel (bottom sheet) -->
<div class="brush-overlay" id="tagOverlay" onclick="closeTagPanel()" aria-hidden="true"></div>
<div class="tag-panel" id="tagPanel" role="dialog" aria-label="标签管理">
<div class="brush-panel-handle" aria-hidden="true"></div>
<!-- Header row -->
<div class="tag-panel-header">
<span class="tag-panel-title">标签</span>
<button class="tag-panel-close" aria-label="关闭标签面板" onclick="closeTagPanel()">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><line x1="5" y1="5" x2="15" y2="15"/><line x1="15" y1="5" x2="5" y2="15"/></svg>
</button>
</div>
<!-- Selected tags -->
<div class="tag-selected-area" id="tagSelectedArea">
<span class="tag-pill">#图书馆<span class="remove" aria-label="删除标签 图书馆" role="button">&times;</span></span>
<span class="tag-pill">#学习<span class="remove" aria-label="删除标签 学习" role="button">&times;</span></span>
</div>
<!-- Input -->
<div class="tag-input-row">
<input class="tag-input" type="text" id="tagInput" placeholder="添加标签..." autocomplete="off" aria-label="输入新标签" onkeydown="handleTagInput(event)">
</div>
<!-- Suggested tags -->
<div class="tag-suggest-label">推荐标签</div>
<div class="tag-suggest-row">
<button class="tag-suggest-item" aria-label="添加标签 日常" onclick="addSuggestedTag(this)">#日常</button>
<button class="tag-suggest-item" aria-label="添加标签 学习" onclick="addSuggestedTag(this)">#学习</button>
<button class="tag-suggest-item" aria-label="添加标签 读书" onclick="addSuggestedTag(this)">#读书</button>
<button class="tag-suggest-item" aria-label="添加标签 运动" onclick="addSuggestedTag(this)">#运动</button>
<button class="tag-suggest-item" aria-label="添加标签 旅行" onclick="addSuggestedTag(this)">#旅行</button>
<button class="tag-suggest-item" aria-label="添加标签 美食" onclick="addSuggestedTag(this)">#美食</button>
<button class="tag-suggest-item" aria-label="添加标签 心情" onclick="addSuggestedTag(this)">#心情</button>
<button class="tag-suggest-item" aria-label="添加标签 灵感" onclick="addSuggestedTag(this)">#灵感</button>
</div>
</div>
<div class="dynamic-island" aria-hidden="true"></div>
<script>
function togglePanel(btn, name) {
closeBrushPanel();
closeTagPanel();
var panels = document.querySelectorAll('.tool-panel');
panels.forEach(function(p) { p.classList.remove('open'); });
var target = document.getElementById(name + 'Panel');
if (target) target.classList.add('open');
document.querySelectorAll('.tool-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
}
function toggleBrushPanel(btn) {
var panel = document.getElementById('brushPanel');
var overlay = document.getElementById('brushOverlay');
var isOpen = panel.classList.contains('open');
document.querySelectorAll('.tool-panel').forEach(function(p) { p.classList.remove('open'); });
if (isOpen) {
panel.classList.remove('open');
overlay.classList.remove('open');
} else {
panel.classList.add('open');
overlay.classList.add('open');
}
document.querySelectorAll('.tool-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
}
function closeBrushPanel() {
document.getElementById('brushPanel').classList.remove('open');
document.getElementById('brushOverlay').classList.remove('open');
}
function selectBrushTool(btn, tool) {
document.querySelectorAll('.brush-tool-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
var defaults = { pen: 2, pencil: 3, marker: 8, eraser: 10 };
var slider = document.getElementById('brushSizeSlider');
slider.value = defaults[tool] || 2;
document.getElementById('brushSizeValue').textContent = slider.value + 'px';
var opacityRow = document.getElementById('brushOpacityRow');
if (tool === 'marker') {
opacityRow.classList.remove('disabled');
} else {
opacityRow.classList.add('disabled');
}
}
function selectBrushColor(dot) {
document.querySelectorAll('.brush-color-dot').forEach(function(d) { d.classList.remove('active'); });
dot.classList.add('active');
}
function openColorPicker() {
// Placeholder for color picker dialog
}
/* Tag panel */
function toggleTagPanel() {
var panel = document.getElementById('tagPanel');
var overlay = document.getElementById('tagOverlay');
var isOpen = panel.classList.contains('open');
if (isOpen) {
closeTagPanel();
} else {
panel.classList.add('open');
overlay.classList.add('open');
document.getElementById('tagInput').focus();
}
}
function closeTagPanel() {
document.getElementById('tagPanel').classList.remove('open');
document.getElementById('tagOverlay').classList.remove('open');
}
function handleTagInput(e) {
if (e.key === 'Enter') {
var input = e.target;
var text = input.value.trim();
if (text) {
if (!text.startsWith('#')) text = '#' + text;
var area = document.getElementById('tagSelectedArea');
var pill = document.createElement('span');
pill.className = 'tag-pill';
pill.innerHTML = text + '<span class="remove" aria-label="删除标签 ' + text.slice(1) + '" role="button">&times;</span>';
area.appendChild(pill);
input.value = '';
}
}
}
function addSuggestedTag(btn) {
var text = btn.textContent.trim();
var area = document.getElementById('tagSelectedArea');
var pill = document.createElement('span');
pill.className = 'tag-pill';
pill.innerHTML = text + '<span class="remove" aria-label="删除标签 ' + text.slice(1) + '" role="button">&times;</span>';
area.appendChild(pill);
btn.style.display = 'none';
}
document.getElementById('tagSelectedArea').addEventListener('click', function(e) {
if (e.target.classList.contains('remove')) {
e.target.parentElement.remove();
}
});
document.getElementById('brushSizeSlider').addEventListener('input', function() {
document.getElementById('brushSizeValue').textContent = this.value + 'px';
});
document.getElementById('brushOpacitySlider').addEventListener('input', function() {
document.getElementById('brushOpacityValue').textContent = this.value + '%';
});
</script>
<script src="../js/theme-switcher.js"></script>
</body>
</html>