Files
bilin/design/ui_pc.html
T
bing 011bd6d4ca feat: 实现笔临 P0/P1 Electron 桌面应用 v0.1.0
交付写作核心(TipTap、自动保存、分卷分章)、Onboarding、7 主题与双语 i18n、快捷键设置;主进程使用 node:sqlite;补全 Vitest 与 Playwright E2E;统一 design/ 目录并移除 desigin 拼写错误路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 10:39:20 +08:00

3578 lines
207 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>笔临 - 长篇创作智能协作平台</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--bg-primary: #1c1816; --bg-secondary: #201c1a; --bg-tertiary: #262220;
--bg-surface: #2a2624; --bg-hover: #302c28; --bg-active: #363230; --bg-card: #221e1c;
--text-primary: #e8e0d8; --text-secondary: #b8aca0; --text-muted: #786858; --text-dim: #584838;
--accent: #c0504a; --accent-glow: rgba(192,80,74,0.25); --accent-light: #d4706a; --accent-dark: #a0403a;
--gold: #c8a050; --gold-dim: #786030; --red: #c0504a; --red-dim: #703030;
--green: #609060; --green-dim: #305030; --orange: #c08050; --blue: #6088b0;
--border: #383028; --border-light: #484030;
--shadow: 0 4px 24px rgba(0,0,0,0.6); --shadow-lg: 0 12px 48px rgba(0,0,0,0.7); --shadow-card: 0 2px 12px rgba(0,0,0,0.5);
--editor-bg: #1c1816; --editor-text: #e8e0d8;
--radius-sm: 6px; --radius: 10px; --radius-lg: 14px; --radius-xl: 18px;
--font-mono: 'SF Mono','Cascadia Code','JetBrains Mono','Fira Code',monospace;
--font-sans: -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
--transition: 0.2s cubic-bezier(0.4,0,0.2,1);
--transition-slow: 0.35s cubic-bezier(0.4,0,0.2,1);
}
/* 竹语 — 竹林雨后 · 深绿调 */
[data-theme="bamboo"] { --bg-primary:#1a1e18;--bg-secondary:#1e221c;--bg-tertiary:#242820;--bg-surface:#282c24;--bg-hover:#2e322a;--bg-active:#343830;--bg-card:#20241c;--text-primary:#e0e4d8;--text-secondary:#b0b8a8;--text-muted:#687858;--text-dim:#485838;--accent:#7a9a60;--accent-glow:rgba(122,154,96,0.25);--accent-light:#98b878;--accent-dark:#5a7a40;--gold:#b0a048;--gold-dim:#706028;--red:#b05850;--red-dim:#703030;--green:#7a9a60;--green-dim:#3a5030;--orange:#b88050;--blue:#6088a8;--border:#303828;--border-light:#404838;--shadow:0 4px 24px rgba(0,0,0,0.6);--shadow-lg:0 12px 48px rgba(0,0,0,0.7);--shadow-card:0 2px 12px rgba(0,0,0,0.5);--editor-bg:#1a1e18;--editor-text:#e0e4d8; }
/* 月夜 — 月光静谧 · 深冷调 */
[data-theme="moonlit"] { --bg-primary:#1a1e24;--bg-secondary:#1e2228;--bg-tertiary:#242830;--bg-surface:#282c34;--bg-hover:#2e3238;--bg-active:#343840;--bg-card:#202428;--text-primary:#d8dce4;--text-secondary:#a8b0b8;--text-muted:#586878;--text-dim:#384858;--accent:#8098b8;--accent-glow:rgba(128,152,184,0.25);--accent-light:#a0b4d0;--accent-dark:#607898;--gold:#b0a860;--gold-dim:#706838;--red:#b06058;--red-dim:#703838;--green:#68a070;--green-dim:#385838;--orange:#b88858;--blue:#8098b8;--border:#2a3038;--border-light:#3a4048;--shadow:0 4px 24px rgba(0,0,0,0.6);--shadow-lg:0 12px 48px rgba(0,0,0,0.7);--shadow-card:0 2px 12px rgba(0,0,0,0.5);--editor-bg:#1a1e24;--editor-text:#d8dce4; }
/* 宣纸 — 古籍书卷 · 浅暖调 */
[data-theme="ricepaper"] { --bg-primary:#f4efe4;--bg-secondary:#faf6ee;--bg-tertiary:#efe8d8;--bg-surface:#fefaf2;--bg-hover:#e8e0d0;--bg-active:#ded6c4;--bg-card:#fcf6ec;--text-primary:#3a3028;--text-secondary:#6a5a48;--text-muted:#988878;--text-dim:#b8a898;--accent:#8b4513;--accent-glow:rgba(139,69,19,0.15);--accent-light:#a86030;--accent-dark:#6b3010;--gold:#b89840;--gold-dim:#786028;--red:#b04840;--red-dim:#783028;--green:#508850;--green-dim:#285828;--orange:#b87040;--blue:#4878a8;--border:#d8d0c0;--border-light:#e0d8c8;--shadow:0 4px 24px rgba(0,0,0,0.06);--shadow-lg:0 12px 48px rgba(0,0,0,0.1);--shadow-card:0 2px 12px rgba(0,0,0,0.04);--editor-bg:#fdf9f2;--editor-text:#3a3028; }
/* 晨雾 — 清晨薄雾 · 浅灰调 */
[data-theme="mist"] { --bg-primary:#eff0f2;--bg-secondary:#f5f6f8;--bg-tertiary:#eaeaee;--bg-surface:#fafafc;--bg-hover:#e2e3e8;--bg-active:#d8dae0;--bg-card:#f8f8fa;--text-primary:#2c3038;--text-secondary:#585c68;--text-muted:#888c98;--text-dim:#b0b4b8;--accent:#6878a0;--accent-glow:rgba(104,120,160,0.15);--accent-light:#8898b8;--accent-dark:#485880;--gold:#b0a058;--gold-dim:#706838;--red:#b05850;--red-dim:#783838;--green:#589068;--green-dim:#305838;--orange:#b87850;--blue:#6878a0;--border:#d8dae0;--border-light:#e2e4e8;--shadow:0 4px 24px rgba(0,0,0,0.05);--shadow-lg:0 12px 48px rgba(0,0,0,0.08);--shadow-card:0 2px 12px rgba(0,0,0,0.03);--editor-bg:#f8f9fc;--editor-text:#2c3038; }
/* 茶园 — 茶园新叶 · 浅绿调 */
[data-theme="teagarden"] { --bg-primary:#eef0e4;--bg-secondary:#f4f6ec;--bg-tertiary:#e6e8d8;--bg-surface:#fafcf2;--bg-hover:#dee2d0;--bg-active:#d4d8c4;--bg-card:#f2f4e8;--text-primary:#2d3a28;--text-secondary:#4a5840;--text-muted:#7a8870;--text-dim:#a0b098;--accent:#688848;--accent-glow:rgba(104,136,72,0.15);--accent-light:#80a060;--accent-dark:#486830;--gold:#a89840;--gold-dim:#686028;--red:#a85048;--red-dim:#683028;--green:#688848;--green-dim:#304828;--orange:#b07848;--blue:#4878a0;--border:#d0d8c0;--border-light:#dce4cc;--shadow:0 4px 24px rgba(0,0,0,0.05);--shadow-lg:0 12px 48px rgba(0,0,0,0.08);--shadow-card:0 2px 12px rgba(0,0,0,0.03);--editor-bg:#f6faf0;--editor-text:#2d3a28; }
/* 暮光 — 落日余晖 · 深暖调 */
[data-theme="twilight"] { --bg-primary:#1f1a16;--bg-secondary:#241e1a;--bg-tertiary:#2a2420;--bg-surface:#2e2824;--bg-hover:#342e28;--bg-active:#3a342e;--bg-card:#26201c;--text-primary:#e8dcc8;--text-secondary:#b8a890;--text-muted:#786850;--text-dim:#584838;--accent:#c88850;--accent-glow:rgba(200,136,80,0.25);--accent-light:#d8a870;--accent-dark:#a06838;--gold:#c89848;--gold-dim:#786028;--red:#c05848;--red-dim:#783030;--green:#609058;--green-dim:#305028;--orange:#c88850;--blue:#5880a8;--border:#383028;--border-light:#484038;--shadow:0 4px 24px rgba(0,0,0,0.6);--shadow-lg:0 12px 48px rgba(0,0,0,0.7);--shadow-card:0 2px 12px rgba(0,0,0,0.5);--editor-bg:#1f1a16;--editor-text:#e8dcc8; }
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
overflow: hidden;
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
user-select: none;
-webkit-font-smoothing: antialiased;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
}
/* ============ TOPBAR ============ */
#topbar {
display: flex;
align-items: center;
height: 44px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
padding: 0 10px;
gap: 2px;
-webkit-app-region: drag;
z-index: 100;
}
#topbar .logo-area {
-webkit-app-region: no-drag;
display: flex;
align-items: center;
gap: 6px;
font-weight: 700;
color: #fff;
font-size: 14px;
letter-spacing: 0.5px;
cursor: pointer;
padding: 6px 10px;
border-radius: var(--radius-sm);
transition: var(--transition);
margin-right: 6px;
white-space: nowrap;
}
#topbar .logo-area:hover {
background: var(--bg-hover);
}
#topbar .tab {
-webkit-app-region: no-drag;
display: flex;
align-items: center;
gap: 7px;
padding: 7px 14px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 13px;
color: var(--text-secondary);
background: transparent;
border: 1px solid transparent;
transition: var(--transition);
white-space: nowrap;
font-weight: 500;
position: relative;
max-width: 180px;
}
#topbar .tab .tab-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#topbar .tab.active {
background: var(--bg-tertiary);
color: #fff;
border-color: var(--border-light);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
#topbar .tab:hover:not(.active) {
background: var(--bg-hover);
color: var(--text-primary);
}
#topbar .tab .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
}
#topbar .tab .close-tab {
opacity: 0;
margin-left: 2px;
font-size: 10px;
transition: var(--transition);
color: var(--text-muted);
flex-shrink: 0;
}
#topbar .tab:hover .close-tab {
opacity: 1;
}
#topbar .tab .close-tab:hover {
color: var(--red);
}
#topbar .spacer {
flex: 1;
-webkit-app-region: drag;
}
#topbar .top-btn {
-webkit-app-region: no-drag;
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
font-size: 15px;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
}
#topbar .top-btn:hover {
background: var(--bg-hover);
color: #fff;
}
#topbar .top-btn.accent {
color: var(--accent-light);
}
/* 窗口控制按钮 */
.window-btn {
width: 28px;
height: 28px;
border-radius: 4px;
font-size: 12px;
}
.window-close-btn:hover {
background: var(--red) !important;
color: #fff !important;
}
#btn-maximize .fa-window-maximize {
font-size: 11px;
}
/* ============ MAIN AREA ============ */
#main-area {
display: flex;
flex: 1;
overflow: hidden;
}
/* ============ HOME PAGE ============ */
#home-page {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
background: var(--bg-primary);
}
#home-page.hidden {
display: none;
}
#home-page::-webkit-scrollbar {
width: 5px;
}
#home-page::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
.home-hero {
padding: 40px 48px 28px;
text-align: center;
}
.home-hero h1 {
font-size: 28px;
font-weight: 700;
color: #fff;
letter-spacing: 1px;
}
.home-hero .subtitle {
font-size: 13px;
color: var(--text-muted);
margin-top: 6px;
letter-spacing: 0.5px;
}
.home-actions {
display: flex;
justify-content: center;
gap: 12px;
padding: 0 48px 20px;
flex-wrap: wrap;
}
.home-actions .btn-large {
padding: 12px 28px;
border-radius: var(--radius);
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: 1px solid var(--border);
background: var(--bg-tertiary);
color: #fff;
transition: var(--transition);
display: flex;
align-items: center;
gap: 8px;
}
.home-actions .btn-large.primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.home-actions .btn-large.primary:hover {
background: var(--accent-light);
box-shadow: 0 0 20px var(--accent-glow);
}
.home-actions .btn-large:hover {
background: var(--bg-hover);
border-color: var(--border-light);
}
.home-section {
padding: 16px 48px 24px;
}
.home-section .section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.home-section .section-title {
font-size: 14px;
font-weight: 700;
color: var(--text-secondary);
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.home-section .section-count {
font-size: 11px;
color: var(--text-muted);
font-weight: 400;
}
.book-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 14px;
}
.book-card {
background: var(--bg-card);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
padding: 20px;
cursor: pointer;
transition: var(--transition);
position: relative;
overflow: hidden;
}
.book-card:hover {
border-color: var(--accent);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5), 0 0 0 1px var(--accent-glow);
transform: translateY(-2px);
}
.book-card .book-cover-icon {
width: 44px;
height: 44px;
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
margin-bottom: 12px;
}
.book-card .book-name {
font-weight: 700;
font-size: 15px;
color: #fff;
margin-bottom: 4px;
}
.book-card .book-meta {
font-size: 11px;
color: var(--text-muted);
line-height: 1.6;
}
.book-card .book-meta span {
margin-right: 10px;
}
.book-card .book-progress {
margin-top: 10px;
height: 3px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.book-card .book-progress-fill {
height: 100%;
border-radius: 2px;
background: var(--accent);
}
.book-card .book-badge {
position: absolute;
top: 14px;
right: 14px;
font-size: 9px;
padding: 3px 8px;
border-radius: 10px;
font-weight: 600;
letter-spacing: 0.5px;
}
.badge-draft {
background: var(--orange);
color: #000;
}
.badge-ongoing {
background: var(--accent);
color: #fff;
}
.badge-done {
background: var(--green);
color: #000;
}
.book-card-placeholder {
background: transparent;
border: 2px dashed var(--border);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 140px;
color: var(--text-muted);
font-size: 13px;
gap: 8px;
cursor: pointer;
transition: var(--transition);
}
.book-card-placeholder:hover {
border-color: var(--accent);
color: var(--accent-light);
background: rgba(124, 140, 248, 0.03);
}
.book-card-placeholder .plus-icon {
font-size: 32px;
color: var(--text-dim);
transition: var(--transition);
}
.book-card-placeholder:hover .plus-icon {
color: var(--accent-light);
}
/* ============ EDITOR LAYOUT ============ */
#editor-layout {
display: flex;
flex: 1;
overflow: hidden;
}
#editor-layout.hidden {
display: none;
}
/* ============ LEFT SIDEBAR ============ */
#left-sidebar {
width: 280px;
min-width: 220px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
resize: horizontal;
}
#left-sidebar .sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
gap: 8px;
}
#left-sidebar .sidebar-header .book-title {
font-weight: 700;
font-size: 14px;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
#left-sidebar .sidebar-header .badge {
font-size: 10px;
background: var(--accent);
color: #fff;
padding: 2px 8px;
border-radius: 10px;
font-weight: 600;
letter-spacing: 0.5px;
flex-shrink: 0;
}
.sidebar-nav {
display: flex;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.sidebar-nav .nav-btn {
flex: 1;
padding: 9px 4px;
text-align: center;
font-size: 10.5px;
cursor: pointer;
color: var(--text-muted);
border: none;
background: transparent;
border-bottom: 2px solid transparent;
transition: var(--transition);
font-weight: 500;
letter-spacing: 0.3px;
white-space: nowrap;
}
.sidebar-nav .nav-btn.active {
color: var(--accent-light);
border-bottom-color: var(--accent);
background: rgba(124, 140, 248, 0.05);
}
.sidebar-nav .nav-btn:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.sidebar-panel {
flex: 1;
overflow-y: auto;
display: none;
flex-direction: column;
}
.sidebar-panel.active {
display: flex;
}
.sidebar-panel::-webkit-scrollbar {
width: 4px;
}
.sidebar-panel::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
/* Chapter tree */
#chapter-tree {
padding: 6px 4px;
scroll-behavior: smooth;
}
.vol-group {
margin-bottom: 4px;
}
.vol-header {
display: flex;
align-items: center;
padding: 7px 10px;
cursor: pointer;
border-radius: var(--radius-sm);
transition: var(--transition);
gap: 6px;
font-weight: 600;
font-size: 12px;
color: var(--text-secondary);
letter-spacing: 0.3px;
}
.vol-header:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.vol-header.active {
background: var(--bg-active);
color: var(--text-primary);
border-left: 3px solid var(--accent);
}
.vol-header .vol-arrow {
font-size: 9px;
transition: var(--transition);
color: var(--text-muted);
}
.vol-header.open .vol-arrow {
transform: rotate(90deg);
}
.vol-header .vol-count {
font-size: 10px;
color: var(--text-muted);
margin-left: auto;
}
.chapter-item {
display: flex;
align-items: center;
padding: 6px 10px 6px 28px;
cursor: pointer;
border-radius: var(--radius-sm);
transition: var(--transition);
gap: 8px;
font-size: 12.5px;
color: var(--text-secondary);
position: relative;
margin: 1px 0;
}
.chapter-item:hover {
background: var(--bg-hover);
color: #fff;
}
.chapter-item.active {
background: var(--bg-active);
color: #fff;
font-weight: 600;
box-shadow: inset 3px 0 0 var(--accent);
}
.chapter-item .ch-num {
font-size: 10px;
color: var(--text-muted);
font-family: var(--font-mono);
min-width: 28px;
}
.chapter-item .ch-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.chapter-item .ch-title:hover,
.setting-entry .entry-name:hover,
.outline-item .item-title:hover,
.inspiration-item .item-title:hover {
color: var(--accent-light);
}
.inline-title-input {
width: 100%;
min-width: 0;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid var(--accent);
background: var(--bg-surface);
color: var(--text-primary);
font-size: inherit;
font-family: inherit;
font-weight: inherit;
outline: none;
box-shadow: 0 0 0 2px var(--accent-glow);
}
#editor-toolbar .ch-label {
font-size: 11px;
color: var(--text-muted);
margin-left: 8px;
font-weight: 500;
cursor: text;
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#editor-toolbar .ch-label:hover { color: var(--text-primary); }
#editor-toolbar .inline-title-input.editor-label-input {
max-width: 240px;
font-size: 11px;
font-weight: 500;
}
.outline-item .item-prefix {
flex-shrink: 0;
margin-right: 4px;
}
.outline-item .item-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.outline-item .item-meta {
flex-shrink: 0;
font-size: 10px;
color: var(--text-muted);
margin-left: 4px;
}
.outline-item[data-status="writing"] .item-title { color: var(--accent-light); }
.outline-item[data-status="done"] .item-title { color: var(--green); }
.inspiration-item .item-title {
display: block;
cursor: text;
}
.setting-entry .entry-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.chapter-item .ch-badge {
font-size: 9px;
padding: 2px 6px;
border-radius: 8px;
font-weight: 600;
letter-spacing: 0.5px;
flex-shrink: 0;
}
.ch-badge.draft {
background: var(--orange);
color: #000;
}
.ch-badge.ai {
background: var(--accent);
color: #fff;
}
.ch-badge.review {
background: var(--gold);
color: #000;
}
.ch-badge.done {
background: var(--green);
color: #000;
}
#left-sidebar .sidebar-footer {
padding: 8px 12px;
border-top: 1px solid var(--border);
flex-shrink: 0;
display: flex;
gap: 6px;
}
#left-sidebar .sidebar-footer button {
flex: 1;
padding: 7px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: transparent;
color: var(--text-secondary);
cursor: pointer;
font-size: 11px;
transition: var(--transition);
font-weight: 500;
}
#left-sidebar .sidebar-footer button:hover {
background: var(--bg-hover);
color: #fff;
border-color: var(--border-light);
}
/* Settings panel in sidebar - 书籍设定 */
.setting-cat {
padding: 8px 12px;
font-size: 10px;
color: var(--text-muted);
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.setting-entry {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
border-radius: var(--radius-sm);
transition: var(--transition);
gap: 8px;
font-size: 12px;
color: var(--text-secondary);
margin: 1px 4px;
}
.setting-entry:hover {
background: var(--bg-hover);
color: #fff;
}
.setting-entry .entry-icon {
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
flex-shrink: 0;
}
.entry-icon.char {
background: rgba(124, 140, 248, 0.2);
color: var(--accent-light);
}
.entry-icon.skill {
background: rgba(240, 192, 96, 0.2);
color: var(--gold);
}
.entry-icon.world {
background: rgba(92, 184, 120, 0.2);
color: var(--green);
}
.entry-icon.scene {
background: rgba(91, 156, 245, 0.2);
color: var(--blue);
}
.setting-entry .entry-count {
font-size: 10px;
color: var(--text-muted);
}
/* ============ CENTER EDITOR ============ */
#editor-area {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg-primary);
overflow: hidden;
min-width: 300px;
}
#editor-toolbar {
display: flex;
align-items: center;
gap: 3px;
padding: 7px 14px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
flex-wrap: wrap;
background: var(--bg-secondary);
}
#editor-toolbar .tool-btn {
width: 30px;
height: 28px;
border-radius: 4px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
font-size: 13px;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
}
#editor-toolbar .tool-btn:hover {
background: var(--bg-hover);
color: #fff;
}
#editor-toolbar .sep {
width: 1px;
height: 20px;
background: var(--border);
margin: 0 6px;
}
#editor-toolbar .word-count-display {
font-size: 11px;
color: var(--accent-light);
margin-left: auto;
font-family: var(--font-mono);
font-weight: 600;
cursor: pointer;
}
#editor-toolbar .word-count-display.warn {
color: var(--orange);
}
#editor-content {
flex: 1;
padding: 32px 48px;
overflow-y: auto;
line-height: 2;
font-size: 16px;
color: #d8d8e0;
outline: none;
font-family: 'Georgia', 'Noto Serif SC', 'Source Han Serif SC', 'STSong', serif;
letter-spacing: 0.4px;
white-space: pre-wrap;
word-break: break-word;
scroll-behavior: smooth;
}
#editor-content:focus {
color: #eaeaef;
}
#editor-content::-webkit-scrollbar {
width: 6px;
}
#editor-content::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
#editor-content .placeholder {
color: var(--text-dim);
pointer-events: none;
}
#editor-statusbar {
display: flex;
align-items: center;
padding: 5px 16px;
border-top: 1px solid var(--border);
flex-shrink: 0;
font-size: 11px;
color: var(--text-muted);
gap: 16px;
background: var(--bg-secondary);
}
#editor-statusbar span {
display: flex;
align-items: center;
gap: 4px;
}
#editor-statusbar .speed-indicator {
color: var(--accent-light);
font-weight: 600;
}
/* ============ RIGHT PANEL ============ */
#right-panel {
width: 340px;
min-width: 280px;
background: var(--bg-secondary);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
resize: horizontal;
}
#right-panel .panel-tabs {
display: flex;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#right-panel .panel-tab {
flex: 1;
padding: 10px 6px;
text-align: center;
font-size: 10.5px;
cursor: pointer;
color: var(--text-muted);
border: none;
background: transparent;
border-bottom: 2px solid transparent;
transition: var(--transition);
font-weight: 500;
letter-spacing: 0.3px;
white-space: nowrap;
}
#right-panel .panel-tab.active {
color: var(--accent-light);
border-bottom-color: var(--accent);
}
#right-panel .panel-tab:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
#right-panel .panel-content {
flex: 1;
overflow-y: auto;
display: none;
flex-direction: column;
}
#right-panel .panel-content.active {
display: flex;
}
#right-panel .panel-content::-webkit-scrollbar {
width: 4px;
}
#right-panel .panel-content::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
/* AI Chat */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
}
.chat-msg {
padding: 10px 12px;
border-radius: var(--radius);
font-size: 12.5px;
line-height: 1.6;
max-width: 92%;
animation: fadeInUp 0.25s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.chat-msg.user {
align-self: flex-end;
background: var(--accent-dark);
color: #fff;
border-bottom-right-radius: 4px;
}
.chat-msg.ai {
align-self: flex-start;
background: var(--bg-tertiary);
color: var(--text-primary);
border-bottom-left-radius: 4px;
border: 1px solid var(--border-light);
}
.chat-msg.ai .plot-options {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.chat-msg.ai .plot-opt {
padding: 8px 10px;
border-radius: var(--radius-sm);
background: var(--bg-surface);
cursor: pointer;
border: 1px solid var(--border);
transition: var(--transition);
font-size: 11px;
color: var(--text-secondary);
}
.chat-msg.ai .plot-opt:hover {
border-color: var(--accent);
color: #fff;
background: var(--bg-hover);
}
.chat-msg.ai .plot-opt .opt-label {
font-weight: 700;
color: var(--accent-light);
font-size: 10px;
}
.chat-input-area {
padding: 10px;
border-top: 1px solid var(--border);
display: flex;
gap: 6px;
flex-shrink: 0;
}
.chat-input-area input {
flex: 1;
padding: 9px 12px;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--bg-tertiary);
color: #fff;
font-size: 12px;
outline: none;
font-family: var(--font-sans);
}
.chat-input-area input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
.chat-input-area button {
padding: 9px 14px;
border-radius: var(--radius);
border: none;
background: var(--accent);
color: #fff;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: var(--transition);
}
.chat-input-area button:hover {
background: var(--accent-light);
}
.quick-cmds {
display: flex;
gap: 4px;
padding: 6px 10px;
flex-wrap: wrap;
flex-shrink: 0;
border-top: 1px solid var(--border);
background: var(--bg-tertiary);
}
.quick-cmd {
font-size: 10px;
padding: 4px 10px;
border-radius: 12px;
background: var(--bg-surface);
color: var(--text-muted);
cursor: pointer;
border: 1px solid transparent;
transition: var(--transition);
white-space: nowrap;
}
.quick-cmd:hover {
border-color: var(--accent);
color: var(--accent-light);
}
/* Settings panel (右侧书籍设置) */
.settings-group {
padding: 12px;
border-bottom: 1px solid var(--border);
}
.settings-group h4 {
font-size: 11px;
color: var(--text-muted);
margin-bottom: 8px;
letter-spacing: 1px;
text-transform: uppercase;
}
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
font-size: 12px;
gap: 8px;
}
.settings-row input[type="range"] {
flex: 1;
accent-color: var(--accent);
}
.settings-row input[type="number"] {
width: 50px;
padding: 4px 6px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--bg-tertiary);
color: #fff;
font-size: 11px;
text-align: center;
}
.settings-row select {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--bg-tertiary);
color: #fff;
font-size: 11px;
}
/* Knowledge panel */
.kb-item {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: var(--transition);
font-size: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.kb-item:hover {
background: var(--bg-hover);
}
.kb-item .kb-stars {
color: var(--gold);
font-size: 10px;
}
.kb-item .kb-tag {
font-size: 9px;
padding: 2px 6px;
border-radius: 8px;
background: var(--bg-active);
color: var(--text-muted);
}
/* ============ SETTINGS PAGE (as tab) ============ */
#settings-page {
flex: 1;
display: flex;
overflow-y: auto;
background: var(--bg-primary);
}
#settings-page.hidden {
display: none;
}
#settings-page::-webkit-scrollbar {
width: 5px;
}
#settings-page::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
.settings-layout {
display: flex;
flex: 1;
}
.settings-sidebar {
width: 200px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
padding: 8px 0;
flex-shrink: 0;
}
.settings-sidebar .settings-nav-item {
padding: 10px 16px;
cursor: pointer;
font-size: 12.5px;
color: var(--text-secondary);
transition: var(--transition);
border-left: 3px solid transparent;
font-weight: 500;
}
.settings-sidebar .settings-nav-item:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.settings-sidebar .settings-nav-item.active {
color: var(--text-primary);
background: var(--bg-tertiary);
border-left-color: var(--accent);
font-weight: 600;
}
.settings-content {
flex: 1;
padding: 24px 32px;
overflow-y: auto;
}
.settings-content::-webkit-scrollbar {
width: 5px;
}
.settings-content::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
.settings-content h2 {
font-size: 20px;
color: var(--text-primary);
margin-bottom: 20px;
font-weight: 700;
}
.settings-content .setting-block {
margin-bottom: 24px;
}
.settings-content .setting-block h4 {
font-size: 12px;
color: var(--text-muted);
letter-spacing: 1px;
margin-bottom: 10px;
text-transform: uppercase;
}
.settings-content .setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
margin-bottom: 6px;
font-size: 13px;
gap: 12px;
color: var(--text-secondary);
}
.settings-content .setting-item .setting-label {
flex: 1;
color: var(--text-primary);
}
.settings-content .setting-item .setting-desc {
font-size: 10px;
color: var(--text-muted);
}
.settings-content input[type="text"],
.settings-content input[type="number"],
.settings-content input[type="password"],
.settings-content select {
padding: 6px 10px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--bg-surface);
color: var(--text-primary);
font-size: 12px;
font-family: var(--font-sans);
}
/* ============ V1.1 NEW UI ============ */
.ai-session-bar { display:flex;align-items:center;gap:6px;padding:8px 10px;border-bottom:1px solid var(--border);flex-shrink:0; }
.ai-session-bar select { flex:1;font-size:11px;padding:4px 8px;background:var(--bg-tertiary);color:#fff;border:1px solid var(--border);border-radius:4px; }
.ai-mode-tabs { display:flex;gap:2px;padding:6px 8px;border-bottom:1px solid var(--border);flex-shrink:0; }
.ai-mode-tab { flex:1;padding:5px 4px;font-size:10px;text-align:center;border-radius:4px;cursor:pointer;color:var(--text-muted);background:transparent;border:none;transition:var(--transition); }
.ai-mode-tab.active { background:var(--accent);color:#fff;font-weight:600; }
.context-preview { padding:6px 10px;font-size:10px;color:var(--text-muted);border-bottom:1px solid var(--border);cursor:pointer;flex-shrink:0; }
.context-preview:hover { color:var(--accent-light); }
#reference-panel { width:0;overflow:hidden;transition:width var(--transition-slow);border-left:1px solid var(--border);background:var(--bg-secondary);display:flex;flex-direction:column;flex-shrink:0; }
#reference-panel.open { width:240px; }
.ref-tabs { display:flex;border-bottom:1px solid var(--border); }
.ref-tab { flex:1;padding:8px 4px;font-size:10px;text-align:center;cursor:pointer;color:var(--text-muted);border:none;background:transparent; }
.ref-tab.active { color:var(--accent-light);border-bottom:2px solid var(--accent); }
.ref-list { flex:1;overflow-y:auto;padding:6px; }
.ref-item { padding:8px;margin-bottom:4px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:11px;cursor:grab;color:var(--text-secondary); }
.ref-item:hover { background:var(--bg-hover);color:#fff; }
.search-input-row { display:flex;gap:8px;margin-bottom:12px; }
.search-input-row input { flex:1;padding:10px 14px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg-tertiary);color:#fff;font-size:13px; }
.search-result { padding:10px 12px;border-radius:var(--radius-sm);background:var(--bg-tertiary);margin-bottom:6px;cursor:pointer;font-size:12px; }
.search-result:hover { background:var(--bg-hover); }
.search-result .sr-type { font-size:10px;color:var(--accent-light);margin-bottom:4px; }
.search-result .sr-context { color:var(--text-muted);font-size:11px; }
.inspiration-capture textarea { width:100%;min-height:100px;padding:12px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg-tertiary);color:#fff;font-size:13px;resize:vertical;font-family:var(--font-sans); }
/* ============ MODALS ============ */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
box-shadow: var(--shadow-lg);
width: 90%;
max-width: 800px;
max-height: 85vh;
display: flex;
flex-direction: column;
animation: scaleIn 0.25s ease;
}
@keyframes scaleIn {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.modal-header h3 {
font-size: 15px;
color: #fff;
}
.modal-close {
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
font-size: 18px;
transition: var(--transition);
}
.modal-close:hover {
background: var(--bg-hover);
color: #fff;
}
.modal-body {
flex: 1;
overflow-y: auto;
padding: 18px;
font-size: 13px;
line-height: 1.7;
}
.modal-body::-webkit-scrollbar {
width: 5px;
}
.modal-body::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
.modal-footer {
padding: 12px 18px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 8px;
flex-shrink: 0;
}
.btn {
padding: 8px 16px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: transparent;
color: var(--text-primary);
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: var(--transition);
}
.btn:hover {
background: var(--bg-hover);
}
.btn.primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
font-weight: 600;
}
.btn.primary:hover {
background: var(--accent-light);
}
.btn.danger {
border-color: var(--red-dim);
color: var(--red);
}
.btn.danger:hover {
background: var(--red);
color: #fff;
border-color: var(--red);
}
.hidden {
display: none !important;
}
.diff-view {
display: flex;
gap: 2px;
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.6;
}
.diff-pane {
flex: 1;
padding: 10px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
overflow-y: auto;
max-height: 400px;
white-space: pre-wrap;
}
.diff-pane .diff-add {
background: rgba(92, 184, 120, 0.25);
color: var(--green);
}
.diff-pane .diff-del {
background: rgba(224, 85, 106, 0.25);
color: var(--red);
text-decoration: line-through;
}
.naming-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.naming-opt {
padding: 10px 14px;
border-radius: var(--radius);
background: var(--bg-tertiary);
cursor: pointer;
border: 1px solid var(--border);
transition: var(--transition);
display: flex;
justify-content: space-between;
align-items: center;
}
.naming-opt:hover {
border-color: var(--accent);
background: var(--bg-hover);
}
.naming-opt .name-cn {
font-weight: 700;
font-size: 15px;
color: #fff;
}
.naming-opt .name-desc {
font-size: 11px;
color: var(--text-muted);
}
#focus-indicator {
display: none;
position: fixed;
top: 12px;
right: 20px;
font-size: 11px;
color: var(--text-dim);
z-index: 999;
letter-spacing: 1px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}
#app.focus-mode #topbar,
#app.focus-mode #left-sidebar,
#app.focus-mode #right-panel,
#app.focus-mode #editor-toolbar,
#app.focus-mode #editor-statusbar {
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease;
}
#app.focus-mode #editor-area {
flex: 1;
}
#app.focus-mode #editor-content {
padding: 60px 120px;
font-size: 18px;
line-height: 2.2;
max-width: 800px;
margin: 0 auto;
}
#app.focus-mode #editor-content::before {
content: '';
display: block;
height: 30vh;
}
#app.focus-mode #focus-indicator {
display: block;
}
.toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: var(--bg-surface);
color: #fff;
padding: 10px 20px;
border-radius: 20px;
font-size: 12px;
z-index: 2000;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-light);
animation: toastIn 0.3s ease, toastOut 0.3s ease 2s forwards;
pointer-events: none;
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@keyframes toastOut {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translateX(-50%) translateY(-10px);
}
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
}
.dash-card {
background: var(--bg-tertiary);
border-radius: var(--radius);
padding: 16px;
border: 1px solid var(--border);
cursor: pointer;
transition: var(--transition);
}
.dash-card:hover {
border-color: var(--accent);
box-shadow: 0 0 16px var(--accent-glow);
}
.dash-card .dash-num {
font-size: 28px;
font-weight: 700;
color: #fff;
}
.dash-card .dash-label {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
}
.heatmap-cell {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 3px;
margin: 1px;
background: var(--bg-active);
}
.heatmap-cell.filled {
background: var(--accent);
}
.heatmap-cell.high {
background: var(--accent-light);
}
@media (max-width: 1100px) {
#right-panel {
width: 260px;
min-width: 200px;
}
#left-sidebar {
width: 220px;
min-width: 180px;
}
#editor-content {
padding: 20px 24px;
}
.book-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
/* 新建书籍弹窗 */
.form-group {
margin-bottom: 12px;
}
.form-group label {
display: block;
font-size: 11px;
color: var(--text-muted);
margin-bottom: 4px;
font-weight: 600;
letter-spacing: 0.5px;
}
.form-group input,
.form-group select {
width: 100%;
padding: 9px 12px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-tertiary);
color: var(--text-primary);
font-size: 13px;
font-family: var(--font-sans);
outline: none;
}
.form-group input:focus,
.form-group select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
/* ============ NEW COMPONENT STYLES (V2.0) ============ */
#dailyGoalBar { -webkit-app-region:no-drag;display:flex;align-items:center;gap:6px;padding:0 8px;cursor:pointer; }
#dailyGoalBar .goal-track { width:80px;height:5px;background:var(--bg-active);border-radius:3px;overflow:hidden; }
#dailyGoalBar .goal-fill { height:100%;border-radius:3px;background:var(--accent);transition:width 0.5s ease; }
#dailyGoalBar .goal-fill.warn { background:var(--orange); } #dailyGoalBar .goal-fill.done { background:var(--green); }
#dailyGoalBar .goal-text { font-size:10px;color:var(--text-muted);white-space:nowrap; }
#dailyGoalBar:hover .goal-text { color:var(--text-primary); }
.sound-popover { display:none;position:absolute;top:38px;right:0;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-lg);z-index:200;min-width:180px;padding:6px 0; }
.sound-popover.show { display:block; }
.sound-popover .sound-item { padding:8px 14px;cursor:pointer;font-size:12px;color:var(--text-secondary);transition:var(--transition);display:flex;align-items:center;gap:8px;white-space:nowrap; }
.sound-popover .sound-item:hover { background:var(--bg-hover);color:#fff; }
.sound-popover .sound-item.active { color:var(--accent-light);font-weight:600; }
.sound-popover .sound-item .vol-slider { width:60px;margin-left:auto;accent-color:var(--accent); }
.sound-popover .sound-divider { height:1px;background:var(--border);margin:4px 0; }
.chapter-item .ch-select { display:none;width:16px;height:16px;border-radius:3px;border:2px solid var(--text-muted);flex-shrink:0;cursor:pointer;align-items:center;justify-content:center;font-size:9px;color:transparent;position:absolute;left:6px; }
.chapter-item:hover .ch-select { display:flex; }
.chapter-item .ch-select.checked, .chapter-item.selected .ch-select { display:flex;background:var(--accent);border-color:var(--accent);color:#fff; }
.chapter-item.selected { background:var(--bg-hover); }
#batchToolbar { display:none;align-items:center;gap:6px;padding:6px 10px;background:var(--accent-dark);border-radius:var(--radius-sm);margin:4px 8px;font-size:11px;color:#fff;flex-shrink:0; }
#batchToolbar.show { display:flex; }
#batchToolbar button { padding:3px 8px;border-radius:3px;border:1px solid rgba(255,255,255,0.2);background:transparent;color:#fff;cursor:pointer;font-size:10px;transition:var(--transition); }
#batchToolbar button:hover { background:rgba(255,255,255,0.15); }
.outline-item { display:flex;align-items:center;padding:6px 8px 6px 18px;cursor:pointer;border-radius:4px;font-size:12px;color:var(--text-secondary);border-left:3px solid transparent;margin:1px 4px;gap:6px;transition:var(--transition); }
.outline-item:hover { background:var(--bg-hover);color:var(--text-primary); }
.outline-item.active { background:var(--bg-active);color:var(--text-primary);font-weight:600; }
.outline-item.uncovered { border-left-color:var(--orange); }
.outline-item.uncovered::after { content:'\26A0';font-size:10px;color:var(--orange);margin-left:auto; }
#editor-toolbar .tool-btn.active { color:var(--accent-light);background:var(--bg-active); }
#editor-toolbar .tool-btn.tts-playing { color:var(--green);animation:pulse-tts 1.5s infinite; }
@keyframes pulse-tts { 0%,100%{opacity:1;} 50%{opacity:0.5;} }
#editor-content { background:var(--editor-bg);color:var(--editor-text); }
.pomodoro { display:flex;align-items:center;gap:6px;cursor:pointer;padding:2px 8px;border-radius:10px;border:1px solid var(--border);font-family:var(--font-mono);font-size:11px;transition:var(--transition); }
.pomodoro:hover { border-color:var(--accent); } .pomodoro.running { border-color:var(--red);color:var(--red);animation:pulse-tts 2s infinite; }
.wf-header { padding:10px 12px;border-bottom:1px solid var(--border);display:flex;gap:6px;flex-shrink:0; }
.wf-header button { padding:4px 10px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text-muted);cursor:pointer;font-size:10px;transition:var(--transition); }
.wf-header button.active { background:var(--accent);color:#fff;border-color:var(--accent); }
.wf-row { display:flex;align-items:center;padding:6px 10px;cursor:pointer;border-radius:4px;font-size:12px;color:var(--text-secondary);gap:8px;transition:var(--transition); }
.wf-row:hover { background:var(--bg-hover); }
.wf-row .wf-rank { font-size:10px;color:var(--text-muted);width:20px; }
.wf-row .wf-word { flex:1;font-weight:500; } .wf-row .wf-count { font-family:var(--font-mono);font-size:11px;color:var(--accent-light); }
.wf-row .wf-bar { width:60px;height:4px;background:var(--bg-active);border-radius:2px;overflow:hidden; }
.wf-row .wf-bar-fill { height:100%;border-radius:2px;background:var(--accent); }
.wf-row.overused .wf-word { color:var(--orange); }
.offline-banner { display:none;padding:6px 12px;background:var(--orange);color:#000;font-size:11px;text-align:center;font-weight:600; }
.offline-banner.show { display:block; }
#reading-mode { display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:var(--editor-bg);z-index:500;flex-direction:column; }
#reading-mode.active { display:flex; }
#reading-mode .reading-topbar { display:flex;align-items:center;padding:8px 16px;background:var(--bg-secondary);border-bottom:1px solid var(--border);gap:8px;flex-shrink:0; }
#reading-mode .reading-topbar button { padding:6px 12px;border-radius:var(--radius-sm);border:1px solid var(--border);background:transparent;color:var(--text-secondary);cursor:pointer;font-size:12px;transition:var(--transition); }
#reading-mode .reading-topbar button:hover { background:var(--bg-hover);color:#fff; }
#reading-mode .reading-topbar button.active { background:var(--accent);color:#fff;border-color:var(--accent); }
#reading-mode .reading-topbar .sep { width:1px;height:20px;background:var(--border); }
#reading-mode .reading-content { flex:1;overflow-y:scroll;scroll-snap-type:y mandatory;scroll-behavior:smooth;padding:40px 0; }
#reading-mode .reading-page { scroll-snap-align:start;min-height:100%;display:flex;align-items:center;justify-content:center;padding:40px; }
#reading-mode .reading-page .page-inner { max-width:680px;width:100%;font-family:'Georgia','Noto Serif SC',serif;font-size:17px;line-height:2.2;color:var(--editor-text); }
#reading-mode.device-phone .page-inner { max-width:360px;font-size:15px; }
#reading-mode.device-tablet .page-inner { max-width:540px; }
#reading-mode.font-sans .page-inner { font-family:var(--font-sans); }
#reading-mode.font-eyecare .page-inner { background:#f5f0e0;color:#3a3028;padding:24px;border-radius:8px; }
#graphContainer { width:100%;height:60vh;background:var(--bg-primary);border-radius:var(--radius);border:1px solid var(--border); }
.graph-controls { display:flex;gap:6px;padding:8px 0;flex-wrap:wrap; }
.graph-controls button { padding:5px 12px;border-radius:var(--radius-sm);border:1px solid var(--border);background:transparent;color:var(--text-secondary);cursor:pointer;font-size:11px;transition:var(--transition); }
.graph-controls button:hover,.graph-controls button.active { background:var(--accent);color:#fff;border-color:var(--accent); }
.graph-sidebar { display:none;position:absolute;right:20px;top:60px;width:240px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px;z-index:10;font-size:12px;box-shadow:var(--shadow-lg); }
.graph-sidebar.show { display:block; }
.timeline-container { position:relative;padding:20px 20px 20px 50px; }
.timeline-container::before { content:'';position:absolute;left:30px;top:0;bottom:0;width:3px;background:var(--accent);border-radius:2px; }
.timeline-event { position:relative;margin-bottom:20px;padding:12px 16px;background:var(--bg-tertiary);border-radius:var(--radius);border:1px solid var(--border);cursor:pointer;transition:var(--transition);margin-left:20px; }
.timeline-event:hover { border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow); }
.timeline-event::before { content:'';position:absolute;left:-28px;top:18px;width:12px;height:12px;border-radius:50%;background:var(--accent);border:3px solid var(--bg-primary); }
.timeline-event .tl-time { font-size:10px;color:var(--accent-light);font-weight:600;margin-bottom:4px; }
.timeline-event .tl-title { font-weight:700;font-size:13px;color:#fff; }
.timeline-event .tl-desc { font-size:11px;color:var(--text-muted);margin-top:4px; }
.timeline-event .tl-type { font-size:9px;padding:2px 6px;border-radius:8px;background:var(--bg-active);color:var(--text-muted);display:inline-block;margin-top:4px; }
.timeline-event.conflict { border-color:var(--red); }
.timeline-event.conflict::before { background:var(--red); }
.timeline-event.conflict .tl-time { color:var(--red); }
.ctx-menu { display:none;position:fixed;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-lg);z-index:1500;min-width:160px;padding:4px 0; }
.ctx-menu.show { display:block; }
.ctx-menu .ctx-item { padding:8px 16px;cursor:pointer;font-size:12px;color:var(--text-secondary);transition:var(--transition);display:flex;align-items:center;gap:8px; }
.ctx-menu .ctx-item:hover { background:var(--bg-hover);color:#fff; }
.ctx-divider { height:1px;background:var(--border);margin:4px 0; }
.export-options { display:flex;flex-direction:column;gap:8px; }
.export-opt { display:flex;align-items:center;padding:10px 14px;border-radius:var(--radius);background:var(--bg-tertiary);cursor:pointer;border:1px solid var(--border);transition:var(--transition);gap:10px; }
.export-opt:hover { border-color:var(--accent);background:var(--bg-hover); }
.export-opt .export-icon { font-size:20px;width:36px;text-align:center; }
.export-opt .export-info { flex:1; }
.export-opt .export-name { font-weight:600;font-size:13px;color:#fff; }
.export-opt .export-desc { font-size:10px;color:var(--text-muted); }
.license-notice { padding:8px 12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:10px;color:var(--text-muted);line-height:1.6;margin-top:8px; }
.license-notice a { color:var(--accent-light); }
.template-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px; }
.template-card { padding:14px;border-radius:var(--radius);background:var(--bg-tertiary);border:1px solid var(--border);cursor:pointer;transition:var(--transition); }
.template-card:hover { border-color:var(--accent);background:var(--bg-hover); }
.template-card .tpl-name { font-weight:700;font-size:13px;color:#fff;margin-bottom:6px; }
.template-card .tpl-preview { font-size:10px;color:var(--text-muted);line-height:1.5;font-family:var(--font-mono); }
.template-card .tpl-badge { font-size:9px;padding:2px 6px;border-radius:8px;display:inline-block;margin-top:6px; }
.tpl-badge.builtin { background:var(--accent);color:#fff; } .tpl-badge.custom { background:var(--gold);color:#000; }
.onboarding-slide { text-align:center;padding:20px 0; }
.onboarding-slide .ob-icon { font-size:56px;margin-bottom:16px; }
.onboarding-slide h2 { font-size:20px;color:#fff;margin-bottom:8px; }
.onboarding-slide p { font-size:13px;color:var(--text-muted);line-height:1.8; }
.onboarding-dots { display:flex;gap:6px;justify-content:center;padding:10px; }
.onboarding-dots .dot { width:8px;height:8px;border-radius:50%;background:var(--border);transition:var(--transition); }
.onboarding-dots .dot.active { background:var(--accent);width:24px;border-radius:4px; }
#focusSoundIndicator { display:none;position:fixed;bottom:20px;left:20px;font-size:11px;color:var(--text-dim);z-index:999;align-items:center;gap:6px; }
#focusSoundIndicator .sound-icon { animation:pulse 2s infinite; }
.streak-badges { display:flex;gap:8px;padding:8px 48px 16px;flex-wrap:wrap; }
.streak-badge { padding:6px 14px;border-radius:20px;font-size:11px;font-weight:600;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-secondary);display:flex;align-items:center;gap:6px; }
.streak-badge.earned { border-color:var(--gold);color:var(--gold);background:rgba(240,192,96,0.1); }
.vol-header .vol-menu-btn { opacity:0;font-size:10px;color:var(--text-muted);cursor:pointer;padding:2px 6px;border-radius:3px; }
.vol-header:hover .vol-menu-btn { opacity:1; }
.vol-header .vol-menu-btn:hover { background:var(--bg-active);color:#fff; }
.setting-cat .add-btn { color:var(--accent-light);cursor:pointer;font-weight:400; }
.setting-entry.active {
background: var(--bg-active);
border-left: 3px solid var(--accent);
}
.setting-entry.active .entry-name {
color: var(--text-primary);
}
.inspiration-item {
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
font-size: 12px;
color: var(--text-secondary);
background: var(--bg-tertiary);
margin: 2px 0;
transition: var(--transition);
border-left: 3px solid transparent;
}
.inspiration-item:hover { background: var(--bg-hover); color: var(--text-primary); }
.inspiration-item.active { background: var(--bg-active); color: var(--text-primary); border-left-color: var(--accent); font-weight: 600; }
.panel-list-body { flex: 1; overflow-y: auto; min-height: 0; }
#left-sidebar .sidebar-footer button.primary-new {
flex: 1;
color: var(--accent-light);
border-color: var(--accent-dark);
}
#left-sidebar .sidebar-footer button.primary-new:hover {
background: var(--accent-glow);
color: var(--accent-light);
}
.retry-btn { display:none;padding:4px 10px;border-radius:10px;border:1px solid var(--orange);background:rgba(232,144,80,0.15);color:var(--orange);cursor:pointer;font-size:10px; }
.retry-btn.show { display:inline-block; }
.settings-content .shortcut-key { display:inline-block;padding:2px 8px;border-radius:3px;background:var(--bg-active);font-family:var(--font-mono);font-size:11px;border:1px solid var(--border);cursor:pointer;min-width:80px;text-align:center; }
.settings-content .shortcut-key.recording { border-color:var(--red);color:var(--red);animation:pulse-tts 1s infinite; }
#topbar .top-btn .badge-dot { position:absolute;top:4px;right:4px;width:6px;height:6px;border-radius:50%;background:var(--red); }
#editor-toolbar .word-count-display { cursor:pointer; }
#left-sidebar .sidebar-footer button.accent { color:var(--accent-light);border-color:var(--accent-dark); }
</style>
</head>
<body>
<div id="app">
<!-- ==================== TOP BAR ==================== -->
<div id="topbar">
<span class="logo-area" onclick="goHome()" title="返回主页">
✒ 笔临
</span>
<div id="tabContainer" style="display:flex;align-items:center;gap:2px;flex:1;overflow-x:auto;">
<!-- 动态生成tabs -->
</div>
<div id="dailyGoalBar" title="今日写作目标" aria-label="日更进度">
<span class="goal-text">今日</span>
<span class="goal-track"><span class="goal-fill" id="goalFill" style="width:42%"></span></span>
<span class="goal-text" id="goalLabel">1,280/3,000</span>
</div>
<div class="spacer"></div>
<div style="position:relative;-webkit-app-region:no-drag;">
<button class="top-btn" id="btnSound" title="专注音效" aria-label="专注音效" onclick="toggleSoundPopover()"><i class="fa-solid fa-music"></i></button>
<div class="sound-popover" id="soundPopover">
<div class="sound-item active" onclick="selectSound('none',this)"><i class="fa-solid fa-volume-xmark"></i> 无音效</div>
<div class="sound-divider"></div>
<div class="sound-item" onclick="selectSound('rain',this)"><i class="fa-solid fa-cloud-rain"></i> 雨声 <input type="range" class="vol-slider" min="0" max="100" value="60" onclick="event.stopPropagation()"></div>
<div class="sound-item" onclick="selectSound('keyboard',this)"><i class="fa-solid fa-keyboard"></i> 键盘声 <input type="range" class="vol-slider" min="0" max="100" value="40" onclick="event.stopPropagation()"></div>
<div class="sound-item" onclick="selectSound('cafe',this)"><i class="fa-solid fa-mug-hot"></i> 咖啡馆 <input type="range" class="vol-slider" min="0" max="100" value="50" onclick="event.stopPropagation()"></div>
<div class="sound-item" onclick="selectSound('fire',this)"><i class="fa-solid fa-fire"></i> 篝火 <input type="range" class="vol-slider" min="0" max="100" value="45" onclick="event.stopPropagation()"></div>
<div class="sound-item" onclick="selectSound('forest',this)"><i class="fa-solid fa-tree"></i> 森林 <input type="range" class="vol-slider" min="0" max="100" value="55" onclick="event.stopPropagation()"></div>
<div class="sound-item" onclick="selectSound('ocean',this)"><i class="fa-solid fa-water"></i> 海浪 <input type="range" class="vol-slider" min="0" max="100" value="50" onclick="event.stopPropagation()"></div>
<div class="sound-item" onclick="selectSound('white',this)"><i class="fa-solid fa-wave-square"></i> 白噪音 <input type="range" class="vol-slider" min="0" max="100" value="40" onclick="event.stopPropagation()"></div>
<div class="sound-divider"></div>
<div class="sound-item" onclick="showToast('选择自定义音频文件...')"><i class="fa-solid fa-plus"></i> 自定义音频…</div>
</div>
</div>
<button class="top-btn" title="系统设置" onclick="openSettingsTab()" aria-label="系统设置"><i class="fa-solid fa-gear"></i></button>
<button class="top-btn" title="专注模式 Ctrl+Alt+F" onclick="toggleFocusMode()" aria-label="专注模式"><i class="fa-solid fa-eye"></i></button>
<button class="top-btn" title="驾驶舱" onclick="showDashboardModal()" aria-label="驾驶舱"><i class="fa-solid fa-gauge-high"></i></button>
<button class="top-btn" title="关系图谱" onclick="showGraphModal()" aria-label="角色关系图谱"><i class="fa-solid fa-diagram-project"></i></button>
<button class="top-btn" title="故事时间线" onclick="showTimelineModal()" aria-label="故事时间线"><i class="fa-solid fa-timeline"></i></button>
<button class="top-btn window-btn" id="btn-minimize" title="最小化" aria-label="最小化窗口"><i class="fa-solid fa-window-minimize"></i></button>
<button class="top-btn window-btn" id="btn-maximize" title="最大化"><i class="fa-solid fa-window-maximize"></i></button>
<button class="top-btn window-btn window-close-btn" id="btn-close" title="关闭"><i class="fa-solid fa-xmark"></i></button>
</div>
<!-- ==================== MAIN AREA ==================== -->
<div id="main-area">
<!-- HOME PAGE -->
<div id="home-page">
<div class="home-hero">
<h1>✒ 笔临</h1>
<p class="subtitle">长篇创作智能协作平台 · 让每一个故事都被认真对待</p>
</div>
<div class="home-actions">
<button class="btn-large primary" onclick="showNewBookModal()"><i class="fa-solid fa-plus"></i> 新建书籍</button>
<button class="btn-large" onclick="showImportModal()"><i class="fa-solid fa-file-import"></i> 导入书籍</button>
<button class="btn-large" onclick="showExportAllModal()"><i class="fa-solid fa-box-archive"></i> 导出所有书籍</button>
<button class="btn-large" onclick="openSettingsTab()"><i class="fa-solid fa-gear"></i> 系统设置</button>
</div>
<div class="streak-badges" id="streakBadges">
<span class="streak-badge earned">🔥 连续7天</span>
<span class="streak-badge earned">🏆 30天里程碑</span>
<span class="streak-badge">💯 100天挑战 (32/100)</span>
</div>
<div class="home-section">
<div class="section-header">
<span class="section-title">📚 我的书籍 <span class="section-count" id="bookCount">2本</span></span>
<span style="font-size:11px;color:var(--text-muted);cursor:pointer;">排序 ▾</span>
</div>
<div class="book-grid" id="bookGrid">
<!-- 书籍卡片动态生成 -->
</div>
</div>
<div class="home-section">
<div class="section-header">
<span class="section-title">🕐 最近编辑</span>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;" id="recentList">
<!-- 最近编辑动态生成 -->
</div>
</div>
</div>
<!-- EDITOR LAYOUT -->
<div id="editor-layout" class="hidden">
<!-- LEFT SIDEBAR -->
<div id="left-sidebar">
<div class="sidebar-header">
<span class="book-title" id="sidebarBookTitle">📖 仙道长青</span>
<span class="badge" id="sidebarBookBadge">连载中</span>
</div>
<div class="sidebar-nav">
<button class="nav-btn active" data-sidebar-panel="chapters-panel" onclick="switchSidebarPanel('chapters-panel',this)">📑 章节</button>
<button class="nav-btn" data-sidebar-panel="outline-panel" onclick="switchSidebarPanel('outline-panel',this)">🗺 大纲</button>
<button class="nav-btn" data-sidebar-panel="setting-panel" onclick="switchSidebarPanel('setting-panel',this)">📋 设定</button>
<button class="nav-btn" data-sidebar-panel="inspiration-panel" onclick="switchSidebarPanel('inspiration-panel',this)">💡 灵感</button>
</div>
<!-- 章节面板 -->
<div class="sidebar-panel active" id="chapters-panel">
<div id="batchToolbar" role="toolbar" aria-label="批量操作">
<span id="batchCount">已选 0 章</span>
<button onclick="batchSetStatus('draft')" aria-label="批量标为草稿">草稿</button>
<button onclick="batchSetStatus('review')" aria-label="批量标为需修">需修</button>
<button onclick="batchSetStatus('done')" aria-label="批量标为定稿">定稿</button>
<button onclick="batchExport()" aria-label="批量导出">导出</button>
<button onclick="clearBatchSelection()" style="margin-left:auto;" aria-label="取消选择"></button>
</div>
<div id="chapter-tree">
<div class="vol-group" data-vol-id="vol-1">
<div class="vol-header open active" onclick="selectVolume(this)" oncontextmenu="showVolContextMenu(event,this);return false;">
<span class="vol-arrow" onclick="event.stopPropagation();toggleVolCollapse(this.parentElement)"></span> 第一卷·青云之始
<span class="vol-count">12章 · 3.8万字</span>
<span class="vol-menu-btn" onclick="event.stopPropagation();showVolContextMenu(event,this.parentElement);" aria-label="分卷菜单"></span>
</div>
<div class="vol-chapters">
<div class="chapter-item" data-ch="1" onclick="selectChapter(this,1)" tabindex="0" role="treeitem" aria-label="第1章 山村少年 定稿">
<span class="ch-select" onclick="event.stopPropagation();toggleChapterSelect(this,1)" aria-label="选择章节"></span>
<span class="ch-num">第1章</span><span class="ch-title" ondblclick="startSidebarTitleEdit('chapter',1,this)">山村少年</span><span class="ch-badge done">定稿</span>
</div>
<div class="chapter-item" data-ch="2" onclick="selectChapter(this,2)" tabindex="0" role="treeitem" aria-label="第2章 意外觉醒 定稿">
<span class="ch-select" onclick="event.stopPropagation();toggleChapterSelect(this,2)" aria-label="选择章节"></span>
<span class="ch-num">第2章</span><span class="ch-title" ondblclick="startSidebarTitleEdit('chapter',2,this)">意外觉醒</span><span class="ch-badge done">定稿</span>
</div>
<div class="chapter-item active" data-ch="3" onclick="selectChapter(this,3)" tabindex="0" role="treeitem" aria-label="第3章 入门考验 AI初稿">
<span class="ch-select" onclick="event.stopPropagation();toggleChapterSelect(this,3)" aria-label="选择章节"></span>
<span class="ch-num">第3章</span><span class="ch-title" ondblclick="startSidebarTitleEdit('chapter',3,this)">入门考验</span><span class="ch-badge ai">AI初稿</span>
</div>
<div class="chapter-item" data-ch="4" onclick="selectChapter(this,4)" tabindex="0" role="treeitem" aria-label="第4章 灵根测试 草稿">
<span class="ch-select" onclick="event.stopPropagation();toggleChapterSelect(this,4)" aria-label="选择章节"></span>
<span class="ch-num">第4章</span><span class="ch-title" ondblclick="startSidebarTitleEdit('chapter',4,this)">灵根测试</span><span class="ch-badge draft">草稿</span>
</div>
<div class="chapter-item" data-ch="5" onclick="selectChapter(this,5)" tabindex="0" role="treeitem" aria-label="第5章 初次修炼 需修">
<span class="ch-select" onclick="event.stopPropagation();toggleChapterSelect(this,5)" aria-label="选择章节"></span>
<span class="ch-num">第5章</span><span class="ch-title" ondblclick="startSidebarTitleEdit('chapter',5,this)">初次修炼</span><span class="ch-badge review">需修</span>
</div>
</div>
</div>
<div class="vol-group" data-vol-id="vol-2">
<div class="vol-header" onclick="selectVolume(this)" oncontextmenu="showVolContextMenu(event,this);return false;">
<span class="vol-arrow" onclick="event.stopPropagation();toggleVolCollapse(this.parentElement)"></span> 第二卷·宗门风云
<span class="vol-count">8章 · 2.4万字</span>
<span class="vol-menu-btn" onclick="event.stopPropagation();showVolContextMenu(event,this.parentElement);" aria-label="分卷菜单"></span>
</div>
<div class="vol-chapters hidden">
<div class="chapter-item" data-ch="13" onclick="selectChapter(this,13)" tabindex="0" role="treeitem" aria-label="第13章 初入宗门 定稿">
<span class="ch-select" onclick="event.stopPropagation();toggleChapterSelect(this,13)" aria-label="选择章节"></span>
<span class="ch-num">第13章</span><span class="ch-title" ondblclick="startSidebarTitleEdit('chapter',13,this)">初入宗门</span><span class="ch-badge done">定稿</span>
</div>
</div>
</div>
</div>
<div class="sidebar-footer">
<button onclick="addChapter()" aria-label="新建章节"><i class="fa-solid fa-plus"></i> 新章</button>
<button onclick="showTemplateModal()" aria-label="模板快速新建"><i class="fa-solid fa-file-lines"></i> 模板新建</button>
<button onclick="addVolume()" aria-label="新建分卷"><i class="fa-solid fa-folder-plus"></i> 新卷</button>
<button onclick="showVersionModal()" aria-label="版本历史"><i class="fa-solid fa-clock-rotate-left"></i> 版本</button>
<button onclick="showLandmarksModal()" aria-label="写作地标"><i class="fa-solid fa-map-pin"></i> 地标</button>
<button class="accent" onclick="enterReadingMode()" aria-label="阅读模式"><i class="fa-solid fa-book-open"></i> 阅读</button>
</div>
</div>
<!-- 大纲面板 -->
<div class="sidebar-panel" id="outline-panel" style="padding:4px;">
<div style="padding:4px 8px;display:flex;gap:4px;flex-shrink:0;align-items:center;">
<button class="btn" style="font-size:10px;padding:3px 8px;background:var(--accent);color:#fff;">全部</button>
<button class="btn" style="font-size:10px;padding:3px 8px;">未覆盖</button>
<button class="btn" style="font-size:10px;padding:3px 8px;">已覆盖</button>
<button class="btn" style="font-size:10px;padding:3px 8px;margin-left:auto;" onclick="showToast('进入大纲-正文对照模式')" title="大纲-正文对照">⇔ 对照</button>
</div>
<div class="panel-list-body" id="outline-list">
<div style="padding:4px 8px;font-size:10px;color:var(--text-muted);">大纲树(拖拽排序 · 点击编辑)</div>
<div class="outline-item uncovered" data-id="ol-1" data-status="uncovered" onclick="openOutlineItem(this,'ol-1')" title="预期 3000字 vs 实际 0字 — 偏差100%"><span class="item-prefix"></span><span class="item-title" ondblclick="startSidebarTitleEdit('outline','ol-1',this)">第一卷核心剧情线</span></div>
<div class="outline-item uncovered" data-id="ol-2" data-status="uncovered" style="padding-left:28px;" onclick="openOutlineItem(this,'ol-2')" title="预期 2000字 vs 实际 0字 — 偏差100%"><span class="item-prefix"></span><span class="item-title" ondblclick="startSidebarTitleEdit('outline','ol-2',this)">林远觉醒三纹灵根</span></div>
<div class="outline-item" data-id="ol-3" data-status="writing" style="padding-left:28px;" onclick="openOutlineItem(this,'ol-3')"><span class="item-prefix"></span><span class="item-title" ondblclick="startSidebarTitleEdit('outline','ol-3',this)">入门考验</span><span class="item-meta">(写作中 — 第3章)</span></div>
<div class="outline-item" data-id="ol-4" data-status="done" style="padding-left:28px;" onclick="openOutlineItem(this,'ol-4')"><span class="item-prefix"></span><span class="item-title" ondblclick="startSidebarTitleEdit('outline','ol-4',this)">拜入青云宗</span><span class="item-meta">(第13章 · 2,100字)</span></div>
</div>
<div class="sidebar-footer">
<button class="primary-new" onclick="addOutlineItem()" aria-label="新建大纲"><i class="fa-solid fa-plus"></i> 新建大纲</button>
</div>
</div>
<!-- 设定面板(书籍设定:人物、技能、世界观、场景) -->
<div class="sidebar-panel" id="setting-panel">
<div class="panel-list-body" id="setting-list">
<div class="setting-cat" data-category="char">👤 人物角色</div>
<div class="setting-entry" data-id="set-1" data-category="char" onclick="openSettingItem(this,'set-1')">
<span class="entry-icon char">👤</span><span class="entry-name" ondblclick="startSidebarTitleEdit('setting','set-1',this)">林远</span><span class="entry-count">主角</span>
</div>
<div class="setting-entry" data-id="set-2" data-category="char" onclick="openSettingItem(this,'set-2')">
<span class="entry-icon char">🧓</span><span class="entry-name" ondblclick="startSidebarTitleEdit('setting','set-2',this)">白发长老</span><span class="entry-count">配角</span>
</div>
<div class="setting-entry" data-id="set-3" data-category="char" onclick="openSettingItem(this,'set-3')">
<span class="entry-icon char">👩</span><span class="entry-name" ondblclick="startSidebarTitleEdit('setting','set-3',this)">柳青烟</span><span class="entry-count">女主</span>
</div>
<div class="setting-cat" data-category="skill">⚔ 技能功法</div>
<div class="setting-entry" data-id="set-4" data-category="skill" onclick="openSettingItem(this,'set-4')">
<span class="entry-icon skill">📜</span><span class="entry-name" ondblclick="startSidebarTitleEdit('setting','set-4',this)">青云诀</span><span class="entry-count">核心功法</span>
</div>
<div class="setting-entry" data-id="set-5" data-category="skill" onclick="openSettingItem(this,'set-5')">
<span class="entry-icon skill">🔥</span><span class="entry-name" ondblclick="startSidebarTitleEdit('setting','set-5',this)">玄火掌</span><span class="entry-count">三阶段</span>
</div>
<div class="setting-cat" data-category="world">🌍 世界观</div>
<div class="setting-entry" data-id="set-6" data-category="world" onclick="openSettingItem(this,'set-6')">
<span class="entry-icon world">🗺</span><span class="entry-name" ondblclick="startSidebarTitleEdit('setting','set-6',this)">九州大陆</span><span class="entry-count">地理</span>
</div>
<div class="setting-entry" data-id="set-7" data-category="world" onclick="openSettingItem(this,'set-7')">
<span class="entry-icon world"></span><span class="entry-name" ondblclick="startSidebarTitleEdit('setting','set-7',this)">灵根体系</span><span class="entry-count">力量体系</span>
</div>
<div class="setting-cat" data-category="scene">🏞 场景地点</div>
<div class="setting-entry" data-id="set-8" data-category="scene" onclick="openSettingItem(this,'set-8')">
<span class="entry-icon scene">🏟</span><span class="entry-name" ondblclick="startSidebarTitleEdit('setting','set-8',this)">青云宗演武场</span><span class="entry-count">第3章</span>
</div>
<div class="setting-entry" data-id="set-9" data-category="scene" onclick="openSettingItem(this,'set-9')">
<span class="entry-icon scene">🏡</span><span class="entry-name" ondblclick="startSidebarTitleEdit('setting','set-9',this)">青石村</span><span class="entry-count">第1章</span>
</div>
</div>
<div class="sidebar-footer">
<button class="primary-new" onclick="addSettingItem()" aria-label="新建设定"><i class="fa-solid fa-plus"></i> 新建设定</button>
</div>
</div>
<!-- 灵感面板 -->
<div class="sidebar-panel" id="inspiration-panel" style="padding:8px;">
<div class="panel-list-body" id="inspiration-list">
<div style="padding:8px;color:var(--text-muted);font-size:11px;text-align:center;">💡 灵感碎片(点击编辑 · 可转为大纲或设定)</div>
<div class="inspiration-item" data-id="ins-1" onclick="openInspirationItem(this,'ins-1')"><span class="item-title" ondblclick="startSidebarTitleEdit('inspiration','ins-1',this)">林远的身世可能与太上长老有关...</span></div>
<div class="inspiration-item" data-id="ins-2" onclick="openInspirationItem(this,'ins-2')"><span class="item-title" ondblclick="startSidebarTitleEdit('inspiration','ins-2',this)">考虑在第三卷加入妖兽潮事件</span></div>
</div>
<div class="sidebar-footer">
<button class="primary-new" onclick="addInspirationItem()" aria-label="新建灵感"><i class="fa-solid fa-plus"></i> 新建灵感</button>
</div>
</div>
</div>
<!-- EDITOR AREA -->
<div id="editor-area">
<div id="editor-toolbar">
<button class="tool-btn" title="加粗"><b>B</b></button>
<button class="tool-btn" title="斜体"><i>I</i></button>
<button class="tool-btn" title="下划线"><u>U</u></button>
<span class="sep"></span>
<button class="tool-btn" title="标题">H</button>
<button class="tool-btn" title="引用"></button>
<span class="sep"></span>
<button class="tool-btn" title="撤销 Ctrl+Z" aria-label="撤销"><i class="fa-solid fa-rotate-left"></i></button>
<button class="tool-btn" title="重做 Ctrl+Y" aria-label="重做"><i class="fa-solid fa-rotate-right"></i></button>
<span class="sep"></span>
<button class="tool-btn" id="btnSpellCheck" title="拼写检查" aria-label="拼写检查" onclick="toggleSpellCheck()"><i class="fa-solid fa-spell-check"></i></button>
<button class="tool-btn" id="btnTTS" title="朗读 Ctrl+Shift+R" aria-label="朗读" onclick="toggleTTS()"><i class="fa-solid fa-headphones"></i></button>
<button class="tool-btn" id="btnReference" title="引用面板 Ctrl+Shift+P" aria-label="引用面板" onclick="toggleReferencePanel()"><i class="fa-solid fa-book-bookmark"></i></button>
<span class="sep"></span>
<span class="ch-label" id="editorDocLabel" ondblclick="startEditorLabelEdit(event)" title="双击修改标题">当前:第3章 · 入门考验</span>
<span class="word-count-display" id="wordCountDisplay" title="点击查看用词分析" onclick="showWordFreqInPanel()">2,847 字</span>
<button class="tool-btn" title="保存快照 Ctrl+Alt+S" aria-label="保存快照" onclick="saveSnapshot()"><i class="fa-solid fa-camera"></i></button>
<button class="tool-btn" title="导出当前章节" aria-label="导出" onclick="showExportModal()"><i class="fa-solid fa-download"></i></button>
</div>
<div id="editor-content" contenteditable="true" oninput="updateWordCount()">
晨光透过稀疏的竹帘,在青石地面上投下细碎的光斑。
林远深吸一口气,推开木门走了出去。门外是一个宽阔的演武场,已经有数十名少年等在那里。他们的脸上有期待,有紧张,也有毫不掩饰的傲气。
"下一个,林远。"
听到自己的名字,他迈步走向那块通体漆黑的测灵石。周围的目光齐刷刷地落在他身上——这个来自偏远山村的少年,在入门考核中表现平平,谁也不曾把他放在眼里。
他将手掌贴上冰冷的石面。
刹那间,刺目的青光冲天而起,测灵石上浮现出三道清晰的光纹。全场鸦雀无声。
"三纹灵根……"负责测试的白发长老眯起眼睛,语气中带着一丝惊讶,"有意思。"
</div>
<div id="editor-statusbar">
<span>📝 总字数:<b>128,450</b></span>
<span>📖 本章:<b>2,847</b> / 提醒 3,000</span>
<span>📊 本卷:<b>38,200</b></span>
<span class="speed-indicator">⚡ 今日:1,280字 · 速度 42字/分</span>
<span class="pomodoro" id="pomodoroTimer" onclick="togglePomodoro()" title="番茄钟 25分钟" aria-label="番茄钟">🍅 25:00</span>
<span style="margin-left:auto;">💾 已自动保存</span>
</div>
</div>
<!-- REFERENCE PANEL -->
<div id="reference-panel" aria-label="引用面板">
<div class="ref-tabs">
<button class="ref-tab active" onclick="switchRefTab('setting',this)">设定</button>
<button class="ref-tab" onclick="switchRefTab('outline',this)">大纲</button>
<button class="ref-tab" onclick="switchRefTab('inspiration',this)">灵感</button>
</div>
<div style="padding:6px 8px;border-bottom:1px solid var(--border);"><input type="text" placeholder="搜索引用..." style="width:100%;padding:6px 8px;font-size:11px;border-radius:4px;border:1px solid var(--border);background:var(--bg-tertiary);color:#fff;"></div>
<div class="ref-list" id="refList">
<div class="ref-item" draggable="true" onclick="showToast('展开:林远')">👤 林远 — 三纹灵根,主角</div>
<div class="ref-item" draggable="true" onclick="showToast('展开:测灵石')">📍 测灵石 — 青云宗测试法器</div>
<div class="ref-item" draggable="true" onclick="showToast('展开:入门考验大纲')">🗺 入门考验 — 预期3000字</div>
</div>
<div style="padding:8px;border-top:1px solid var(--border);font-size:10px;color:var(--text-muted);"><label><input type="checkbox"> 钉住当前条目</label></div>
</div>
<!-- RIGHT PANEL -->
<div id="right-panel">
<div class="panel-tabs">
<button class="panel-tab active" onclick="switchRightPanel('ai-chat',this)">🤖 AI助手</button>
<button class="panel-tab" onclick="switchRightPanel('knowledge',this)">🧠 知识库</button>
<button class="panel-tab" onclick="switchRightPanel('wordfreq',this)">📊 用词</button>
<button class="panel-tab" onclick="switchRightPanel('book-settings-panel',this)">⚙ 设置</button>
</div>
<!-- AI Chat Panel -->
<div class="panel-content active" id="ai-chat">
<div class="ai-session-bar">
<select id="aiSessionSelect" aria-label="AI会话选择" onchange="showToast('切换会话:'+this.options[this.selectedIndex].text)">
<option>第3章续写讨论</option>
<option>角色命名 brainstorm</option>
<option>第二卷大纲规划</option>
</select>
<button class="btn" style="font-size:10px;padding:4px 8px;" onclick="showToast('新建AI会话')" aria-label="新建会话">+</button>
</div>
<div class="ai-mode-tabs" role="tablist">
<button class="ai-mode-tab active" onclick="setAiMode('chat',this)">对话</button>
<button class="ai-mode-tab" onclick="setAiMode('interactive',this)">交互写作</button>
<button class="ai-mode-tab" onclick="setAiMode('auto',this)">自动写作</button>
<button class="ai-mode-tab" onclick="setAiMode('wizard',this)">向导</button>
</div>
<div class="context-preview" onclick="showToast('展开上下文编辑器:第1-2章摘要、大纲3条、设定7个')" title="点击编辑上下文">📎 上下文:第1-2章摘要 · 大纲3条 · 设定7个 · <span style="color:var(--accent-light)">更新</span></div>
<div class="offline-banner" id="offlineBanner">⚠ 当前网络不可用,已切换为离线模式。请配置本地模型或连接网络。</div>
<div class="chat-messages" id="chatMessages">
<div class="chat-msg ai">
<b>笔临AI</b> 已就绪。<br>当前上下文:第1-2章剧情摘要、大纲条目3条、设定条目7个。
<br><br>检测到您正在编辑<em>第3章·入门考验</em>。需要我为您提供接下来的剧情走向建议吗?
<div class="plot-options">
<div class="plot-opt" onclick="selectPlot('A')">
<span class="opt-label">走向A · 锋芒毕露</span><br>林远的三纹灵根引起轰动,被直接提拔为内门弟子,但引来同门嫉妒。
</div>
<div class="plot-opt" onclick="selectPlot('B')">
<span class="opt-label">走向B · 藏锋守拙</span><br>长老暗示他隐藏真实天赋,林远被安排在外门低调修行。
</div>
<div class="plot-opt" onclick="selectPlot('C')">
<span class="opt-label">走向C · 意外转折</span><br>测灵石碎裂,林远灵力引发异象,惊动闭关中的太上长老。
</div>
</div>
</div>
</div>
<div class="quick-cmds">
<span class="quick-cmd" onclick="quickCmd('/续写')">/续写</span>
<span class="quick-cmd" onclick="quickCmd('/扩写')">/扩写</span>
<span class="quick-cmd" onclick="quickCmd('/润色')">/润色</span>
<span class="quick-cmd" onclick="quickCmd('/总结')">/总结</span>
<span class="quick-cmd" onclick="quickCmd('/纠错')">/纠错</span>
</div>
<div class="chat-input-area">
<input type="text" id="chatInput" placeholder="输入指令或选择上方快捷命令..." onkeydown="if(event.key==='Enter')sendMessage()">
<button onclick="sendMessage()">发送</button>
</div>
</div>
<!-- Knowledge Panel -->
<div class="panel-content" id="knowledge">
<div style="padding:10px;border-bottom:1px solid var(--border);display:flex;gap:6px;flex-shrink:0;">
<button class="btn" style="flex:1;font-size:10px;" onclick="showNamingModal()">🔍 命名一致性检查</button>
</div>
<div style="padding:8px 12px;font-size:10px;color:var(--text-muted);border-bottom:1px solid var(--border);">
伏笔:<b style="color:var(--gold);">已埋 8</b> · <b style="color:var(--green);">已解 3</b> · <b style="color:var(--red);">疑似遗忘 1</b>
</div>
<div class="kb-item"><span>🔗</span><span style="flex:1;">伏笔:测灵石异常发热</span><span class="kb-stars">★★★★</span><span class="kb-tag">第3章</span></div>
<div class="kb-item"><span>👤</span><span style="flex:1;">角色:林远 - 三纹灵根觉醒</span><span class="kb-stars">★★★★★</span><span class="kb-tag">状态变更</span></div>
<div class="kb-item"><span>📍</span><span style="flex:1;">地点:青云宗演武场</span><span class="kb-stars">★★★</span><span class="kb-tag">新地点</span></div>
<div class="kb-item"><span>🤝</span><span style="flex:1;">关系:林远↔白发长老(注意)</span><span class="kb-stars">★★★★</span><span class="kb-tag">新关系</span></div>
</div>
<!-- 用词分析面板 -->
<div class="panel-content" id="wordfreq">
<div class="wf-header">
<button class="active">高频词</button><button>高频短语</button><button>过度用词</button>
</div>
<div style="flex:1;overflow-y:auto;padding:6px;">
<div class="wf-row"><span class="wf-rank">1</span><span class="wf-word">林远</span><span class="wf-bar"><span class="wf-bar-fill" style="width:100%"></span></span><span class="wf-count">847</span></div>
<div class="wf-row"><span class="wf-rank">2</span><span class="wf-word">灵力</span><span class="wf-bar"><span class="wf-bar-fill" style="width:72%"></span></span><span class="wf-count">612</span></div>
<div class="wf-row overused"><span class="wf-rank">3</span><span class="wf-word">深吸一口气</span><span class="wf-bar"><span class="wf-bar-fill" style="width:58%"></span></span><span class="wf-count">48</span></div>
<div class="wf-row"><span class="wf-rank">4</span><span class="wf-word">长老</span><span class="wf-bar"><span class="wf-bar-fill" style="width:51%"></span></span><span class="wf-count">430</span></div>
<div class="wf-row"><span class="wf-rank">5</span><span class="wf-word">冷笑</span><span class="wf-bar"><span class="wf-bar-fill" style="width:44%"></span></span><span class="wf-count">37</span></div>
<div class="wf-row"><span class="wf-rank">6</span><span class="wf-word">测灵石</span><span class="wf-bar"><span class="wf-bar-fill" style="width:38%"></span></span><span class="wf-count">28</span></div>
<div class="wf-row"><span class="wf-rank">7</span><span class="wf-word">演武场</span><span class="wf-bar"><span class="wf-bar-fill" style="width:34%"></span></span><span class="wf-count">22</span></div>
<div class="wf-row"><span class="wf-rank">8</span><span class="wf-word">天赋</span><span class="wf-bar"><span class="wf-bar-fill" style="width:30%"></span></span><span class="wf-count">19</span></div>
</div>
</div>
<!-- 书籍设置面板 -->
<div class="panel-content" id="book-settings-panel">
<div class="settings-group">
<h4>🎨 文风控制面板</h4>
<div class="settings-row"><span>平均句长</span><input type="range" min="10" max="60" value="28"><span>28字</span></div>
<div class="settings-row"><span>对话占比</span><input type="range" min="10" max="80" value="37"><span>37%</span></div>
<div class="settings-row"><span>比喻密度</span><input type="range" min="0" max="100" value="65"><span></span></div>
<div class="settings-row"><span>禁用"冷笑"</span><input type="range" min="0" max="100" value="0"><span>关闭</span></div>
</div>
<div class="settings-group">
<h4>📝 版本设置</h4>
<div class="settings-row"><span>自动快照间隔</span><input type="number" value="5" min="1" max="30">分钟</div>
<div class="settings-row"><span>临时版本上限</span><input type="number" value="20" min="5" max="100"></div>
<div class="settings-row"><span>持久版本上限</span><input type="number" value="3" min="1" max="20"></div>
</div>
<div class="settings-group">
<h4>🎯 字数提醒</h4>
<div class="settings-row"><span>章节提醒阈值</span><input type="number" value="3000" min="500" max="20000"></span></div>
</div>
<div class="settings-group">
<h4>📖 书籍信息</h4>
<div class="settings-row"><span>分类标签</span><select><option>玄幻</option><option>仙侠</option><option>武侠</option></select></div>
<div class="settings-row"><span>目标总字数</span><input type="number" value="500000" min="10000"></span></div>
</div>
</div>
</div>
</div>
<!-- SETTINGS PAGE (as tab) -->
<div id="settings-page" class="hidden">
<div class="settings-layout">
<div class="settings-sidebar">
<div class="settings-nav-item active" onclick="switchSettingsNav('general',this)">⚙ 通用</div>
<div class="settings-nav-item" onclick="switchSettingsNav('editor',this)">📝 编辑器</div>
<div class="settings-nav-item" onclick="switchSettingsNav('ai',this)">🤖 AI配置</div>
<div class="settings-nav-item" onclick="switchSettingsNav('shortcuts',this)">⌨ 快捷键</div>
<div class="settings-nav-item" onclick="switchSettingsNav('backup',this)">💾 备份与同步</div>
<div class="settings-nav-item" onclick="switchSettingsNav('about',this)"> 关于</div>
</div>
<div class="settings-content" id="settingsContent">
<!-- 通用设置 -->
<div class="settings-section" id="settings-general">
<h2>⚙ 通用设置</h2>
<div class="setting-block">
<h4>语言与外观</h4>
<div class="setting-item"><span class="setting-label">界面语言</span><select aria-label="界面语言"><option>简体中文</option><option>English</option></select></div>
<div class="setting-item"><span class="setting-label">主题</span><select id="themeSelect" onchange="switchTheme(this.value)" aria-label="主题选择"><option value="default">墨砚(默认)</option><option value="bamboo">竹语</option><option value="moonlit">月夜</option><option value="ricepaper">宣纸</option><option value="mist">晨雾</option><option value="teagarden">茶园</option><option value="twilight">暮光</option></select></div>
<div class="setting-item"><span class="setting-label">字体大小</span><select><option></option><option></option><option></option></select></div>
</div>
<div class="setting-block">
<h4>作者信息</h4>
<div class="setting-item"><span class="setting-label">笔名</span><input type="text" value="山海" placeholder="至少2个字符" style="width:160px;" aria-label="笔名"></div>
</div>
<div class="setting-block">
<h4>写作目标</h4>
<div class="setting-item"><span class="setting-label">日更字数目标</span><input type="number" value="3000" min="0" step="500" aria-label="日更目标"></div>
<div class="setting-item"><span class="setting-label">更新频率</span><select aria-label="更新频率"><option>日更</option><option>隔日更</option><option>周更</option><option>不设定</option></select></div>
<div class="setting-item"><span class="setting-label">存稿预警阈值</span><input type="number" value="3" min="1" max="30" aria-label="存稿阈值"></div>
</div>
<div class="setting-block">
<h4>默认行为</h4>
<div class="setting-item"><span class="setting-label">启动时打开上次书籍</span><select><option></option><option>否(显示主页)</option></select></div>
<div class="setting-item"><span class="setting-label">自动保存间隔</span><input type="number" value="2" min="1" max="10"></div>
</div>
</div>
<!-- 编辑器设置 -->
<div class="settings-section hidden" id="settings-editor">
<h2>📝 编辑器设置</h2>
<div class="setting-block">
<h4>排版</h4>
<div class="setting-item"><span class="setting-label">默认字体</span><select><option>宋体</option><option>楷体</option><option>微软雅黑</option></select></div>
<div class="setting-item"><span class="setting-label">行距</span><select><option>1.8</option><option>2.0</option><option>2.2</option></select></div>
<div class="setting-item"><span class="setting-label">专注模式背景</span><select><option>纯黑</option><option>深灰</option><option>渐变</option></select></div>
<div class="setting-item"><span class="setting-label">合规检查(敏感词提示)</span><select><option>关闭</option><option>开启</option></select></div>
</div>
</div>
<!-- AI设置 -->
<div class="settings-section hidden" id="settings-ai">
<h2>🤖 AI配置</h2>
<div class="setting-block">
<h4>服务配置</h4>
<div class="setting-item"><span class="setting-label">AI引擎</span><select><option>本地模型</option><option>云端API</option></select></div>
<div class="setting-item"><span class="setting-label">API密钥</span><input type="password" value="••••••••" style="width:180px;"></div>
<div class="setting-item"><span class="setting-label">超时时间</span><input type="number" value="60" min="10" max="300"></div>
<div class="setting-item"><span class="setting-label">数据上传</span><select><option>禁止(纯本地)</option><option>允许(增强学习)</option></select></div>
</div>
<div class="setting-block">
<h4>默认Skill</h4>
<div class="setting-item"><span class="setting-label">当前默认Skill</span><select><option>玄幻热血</option><option>悬疑深沉</option><option>轻松日常</option></select></div>
<div class="setting-item"><span class="setting-label">导入自定义Skill</span><button class="btn" style="font-size:10px;">选择文件</button></div>
</div>
<div class="license-notice">⚠ 使用云端 AI 服务将受到相应模型提供商的许可协议约束。请在使用前阅读并同意 <a href="#" onclick="event.preventDefault();showToast('打开OpenAI服务条款')">OpenAI 服务条款</a> / <a href="#" onclick="event.preventDefault();showToast('打开Anthropic服务条款')">Anthropic 服务条款</a> 等。</div>
</div>
<!-- 快捷键设置 -->
<div class="settings-section hidden" id="settings-shortcuts">
<h2>⌨ 快捷键设置</h2>
<div class="setting-block"><h4>编辑器快捷键</h4>
<div class="setting-item"><span class="setting-label">保存当前章节</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+S</span></div>
<div class="setting-item"><span class="setting-label">聚焦AI输入框</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+K</span></div>
<div class="setting-item"><span class="setting-label">新建章节</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+N</span></div>
<div class="setting-item"><span class="setting-label">切换专注模式</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Alt+F</span></div>
<div class="setting-item"><span class="setting-label">全局搜索</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+F</span></div>
<div class="setting-item"><span class="setting-label">朗读 播放/暂停</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+R</span></div>
<div class="setting-item"><span class="setting-label">灵感快速捕获</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+I</span></div>
<div class="setting-item"><span class="setting-label">引用面板</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+P</span></div>
<div class="setting-item"><span class="setting-label">写作地标</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+M</span></div>
</div>
</div>
<!-- 备份与同步 -->
<div class="settings-section hidden" id="settings-backup">
<h2>💾 备份与同步</h2>
<div class="setting-block">
<h4>本地备份</h4>
<div class="setting-item"><span class="setting-label">自动备份间隔</span><select><option>每天</option><option>每周</option><option>手动</option></select></div>
<div class="setting-item"><span class="setting-label">备份保留数量</span><input type="number" value="10" min="1" max="100"></div>
<div class="setting-item"><span class="setting-label">立即备份</span><button class="btn primary" style="font-size:10px;">执行备份</button></div>
<div class="setting-block">
<h4>☁ 云同步</h4>
<div class="setting-item"><span class="setting-label">同步方式</span><select><option>关闭</option><option>WebDAV</option><option>本地目录</option></select></div>
<div class="setting-item"><span class="setting-label">服务器地址</span><input type="text" placeholder="https://dav.example.com/novels/" style="width:200px;"></div>
<div class="setting-item"><span class="setting-label">同步密码</span><input type="password" placeholder="至少8位" style="width:150px;"></div>
<div class="setting-item"><span class="setting-label">定时同步</span><select><option>手动</option><option>每小时</option><option>每天</option></select></div>
<div class="setting-item"><span class="setting-label">立即同步</span><button class="btn primary" style="font-size:10px;" onclick="showToast('🔒 加密同步中...')">立即同步</button></div>
<div class="setting-item"><span class="setting-desc">数据以 AES-256-GCM 加密,密钥由同步密码派生,不存储明文。</span></div>
</div>
</div>
<!-- 关于 -->
<div class="settings-section hidden" id="settings-about">
<h2> 关于笔临</h2>
<div class="setting-block">
<p style="color:var(--text-secondary);line-height:1.8;">笔临 v1.0.0<br>长篇创作智能协作平台<br><br>专为长篇小说作者打造的跨平台写作系统。<br>本地优先,隐私至上,让AI真正服务于创作。</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Focus mode indicator -->
<div id="focus-indicator">🔴 专注模式 · 按 Esc 退出</div>
<div id="focusSoundIndicator"><span class="sound-icon">🎵</span> <span id="focusSoundLabel">无音效</span></div>
<!-- Volume Context Menu -->
<div class="ctx-menu" id="volCtxMenu">
<div class="ctx-item" onclick="enterReadingMode()"><i class="fa-solid fa-book-open"></i> 阅读模式</div>
<div class="ctx-item" onclick="showToast('重命名分卷')"><i class="fa-solid fa-pen"></i> 重命名</div>
<div class="ctx-divider"></div>
<div class="ctx-item" onclick="addChapter()"><i class="fa-solid fa-plus"></i> 新建章节</div>
<div class="ctx-item" onclick="showToast('导出该卷')"><i class="fa-solid fa-download"></i> 导出该卷</div>
<div class="ctx-divider"></div>
<div class="ctx-item" style="color:var(--red);" onclick="showToast('确认删除该分卷?')"><i class="fa-solid fa-trash"></i> 删除分卷</div>
</div>
<!-- Reading Mode Overlay -->
<div id="reading-mode" role="region" aria-label="连续阅读模式">
<div class="reading-topbar">
<button onclick="exitReadingMode()" aria-label="退出">✕ 退出</button><span class="sep"></span>
<button onclick="setReadingDevice('desktop',this)" class="active">🖥 桌面</button>
<button onclick="setReadingDevice('tablet',this)">📱 平板</button>
<button onclick="setReadingDevice('phone',this)">📱 手机</button><span class="sep"></span>
<button onclick="setReadingFont('serif',this)" class="active">衬线</button>
<button onclick="setReadingFont('sans',this)">无衬线</button>
<button onclick="setReadingFont('eyecare',this)">护眼</button><span class="sep"></span>
<button onclick="toggleTTS()" id="readingTTSBtn">🎧 朗读</button>
<span style="margin-left:auto;font-size:11px;color:var(--text-muted);">第一卷 · 12章连续</span>
</div>
<div class="reading-content" id="readingContent">
<div class="reading-page"><div class="page-inner"><h2>第一章 · 山村少年</h2><p>青云山脉绵延三千里,在九州大陆的东南角画出一道天然屏障...</p></div></div>
<div class="reading-page"><div class="page-inner"><h2>第二章 · 意外觉醒</h2><p>林远怎么也没想到,一切会来得如此突然...</p></div></div>
<div class="reading-page"><div class="page-inner"><h2>第三章 · 入门考验</h2><p>晨光透过稀疏的竹帘,在青石地面上投下细碎的光斑。林远深吸一口气...</p></div></div>
</div>
</div>
<!-- ==================== MODALS ==================== -->
<!-- New Book Modal -->
<div class="modal-overlay hidden" id="newBookModal">
<div class="modal" style="max-width:500px;">
<div class="modal-header"><h3>📖 新建书籍</h3><button class="modal-close" onclick="closeModal('newBookModal')"></button></div>
<div class="modal-body">
<div class="form-group"><label>书名</label><input type="text" placeholder="输入书名..." id="newBookName"></div>
<div class="form-group"><label>分类</label><select id="newBookCategory"><option value="玄幻">玄幻</option><option value="仙侠">仙侠</option><option value="科幻">科幻</option><option value="言情">言情</option><option value="悬疑">悬疑</option><option value="历史">历史</option></select></div>
<div class="form-group"><label>目标总字数(可选)</label><input type="number" placeholder="如:500000" id="newBookTarget"></div>
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal('newBookModal')">取消</button>
<button class="btn primary" onclick="createNewBook()">创建书籍</button>
</div>
</div>
</div>
<!-- Version History Modal -->
<div class="modal-overlay hidden" id="versionModal">
<div class="modal" style="max-width:900px;">
<div class="modal-header"><h3>📜 版本历史 — 第3章·入门考验</h3><button class="modal-close" onclick="closeModal('versionModal')"></button></div>
<div class="modal-body">
<div style="display:flex;gap:12px;margin-bottom:12px;flex-wrap:wrap;">
<span class="btn" style="font-size:11px;background:var(--bg-active);">全部</span><span class="btn" style="font-size:11px;">自动快照</span><span class="btn" style="font-size:11px;">手动快照</span><span class="btn" style="font-size:11px;">AI生成</span>
</div>
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);cursor:pointer;border:1px solid var(--border);" onclick="showDiffView()">
<span style="color:var(--accent-light);">📸</span><span style="flex:1;font-weight:600;">手动快照:删男二戏份前</span><span style="font-size:10px;color:var(--text-muted);">2026-06-25 14:32</span><span class="btn" style="font-size:10px;padding:4px 8px;">恢复</span>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);cursor:pointer;border:1px solid var(--border);">
<span style="color:var(--text-muted);"></span><span style="flex:1;">自动快照</span><span style="font-size:10px;color:var(--text-muted);">2026-06-25 14:25</span><span class="btn" style="font-size:10px;padding:4px 8px;">恢复</span>
</div>
</div>
<div class="diff-view hidden" id="diffViewArea">
<div style="font-size:11px;color:var(--text-muted);margin-bottom:4px;">对比:当前版本 ↔ 手动快照</div>
<div class="diff-pane"><span>晨光透过稀疏的竹帘...</span><span class="diff-del">林远看到男二站在演武场角落。</span><span class="diff-add">林远深吸一口气,推开木门走了出去。</span></div>
<div class="diff-pane"><span>晨光透过稀疏的竹帘...</span><span>林远看到男二站在演武场角落。</span></div>
</div>
</div>
<div class="modal-footer"><button class="btn" onclick="closeModal('versionModal')">关闭</button><button class="btn primary" onclick="showToast('已创建新快照');closeModal('versionModal')">📸 创建新快照</button></div>
</div>
</div>
<!-- Naming Modal -->
<div class="modal-overlay hidden" id="namingModal">
<div class="modal" style="max-width:550px;">
<div class="modal-header"><h3>🏷 AI命名建议 — 新角色</h3><button class="modal-close" onclick="closeModal('namingModal')"></button></div>
<div class="modal-body">
<p style="color:var(--text-secondary);margin-bottom:12px;">为新出场的<b>白发长老</b>选择名字:</p>
<div class="naming-options">
<div class="naming-opt" onclick="selectName('云逸真人')"><span class="name-cn">云逸真人</span><span class="name-desc">飘逸出尘,暗示其超然地位</span></div>
<div class="naming-opt" onclick="selectName('玄镜长老')"><span class="name-cn">玄镜长老</span><span class="name-desc">睿智深沉,取"玄妙如镜"</span></div>
<div class="naming-opt" onclick="selectName('白衍')"><span class="name-cn">白衍</span><span class="name-desc">简洁有力,"衍"暗示传承</span></div>
<div class="naming-opt" onclick="selectName('苍梧道人')"><span class="name-cn">苍梧道人</span><span class="name-desc">古意盎然,与宗门氛围契合</span></div>
<div class="naming-opt" onclick="selectName('素问长老')"><span class="name-cn">素问长老</span><span class="name-desc">温和平实,反衬深藏不露</span></div>
</div>
<div style="margin-top:12px;display:flex;gap:8px;"><input type="text" placeholder="或自行输入..." style="flex:1;padding:8px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg-tertiary);color:#fff;font-size:12px;" id="customNameInput"><button class="btn primary" onclick="selectName(document.getElementById('customNameInput').value||'自定义名称')">确定</button></div>
</div>
</div>
</div>
<!-- Dashboard Modal -->
<div class="modal-overlay hidden" id="dashboardModal">
<div class="modal" style="max-width:750px;">
<div class="modal-header"><h3>📊 驾驶舱 — 仙道长青</h3><button class="modal-close" onclick="closeModal('dashboardModal')"></button></div>
<div class="modal-body">
<div class="dashboard-grid">
<div class="dash-card"><div class="dash-num">128,450</div><div class="dash-label">📖 全书总字数</div></div>
<div class="dash-card"><div class="dash-num">1,280</div><div class="dash-label">⚡ 今日字数</div></div>
<div class="dash-card"><div class="dash-num">42</div><div class="dash-label">字/分钟 · 当前速度</div></div>
<div class="dash-card"><div class="dash-num">68天</div><div class="dash-label">📅 预计完本</div></div>
</div>
<div style="margin-top:16px;"><h4 style="font-size:12px;color:var(--text-muted);margin-bottom:8px;">📅 写作热力图(近30天)</h4>
<div style="display:flex;flex-wrap:wrap;gap:2px;" id="heatmapContainer"></div>
</div>
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;">
<button class="btn primary" style="font-size:11px;" onclick="closeModal('dashboardModal');showToast('回到上次编辑位置')">↩ 回到上次编辑</button>
<button class="btn" style="font-size:11px;" onclick="closeModal('dashboardModal');showBridgeModal()">🔗 检查章间衔接</button>
<button class="btn" style="font-size:11px;" onclick="closeModal('dashboardModal');switchRightPanel('knowledge',document.querySelector('.panel-tab:nth-child(2)'))">⚠ 处理遗忘伏笔</button>
</div>
</div>
</div>
</div>
<div id="toastContainer"></div>
<!-- ===== NEW MODALS (V2.0) ===== -->
<!-- Character Graph -->
<div class="modal-overlay hidden" id="graphModal"><div class="modal" style="max-width:95vw;width:1100px;">
<div class="modal-header"><h3>🕸 角色关系图谱 — 仙道长青</h3><button class="modal-close" onclick="closeModal('graphModal')" aria-label="关闭"></button></div>
<div class="modal-body" style="position:relative;">
<div class="graph-controls">
<button class="active" onclick="setGraphLayout('cose')">力导向</button>
<button onclick="setGraphLayout('grid')">网格</button>
<button onclick="setGraphLayout('circle')">圆形</button>
<span style="width:1px;height:20px;background:var(--border);margin:0 6px;"></span>
<button onclick="showToast('添加新角色节点')">+ 添加角色</button>
<span style="font-size:10px;color:var(--text-muted);margin-left:auto;">节点上限100 · 当前 5</span>
</div>
<div id="graphContainer" aria-label="角色关系力导向图"></div>
<div class="graph-sidebar" id="graphSidebar"><h4 style="margin-bottom:8px;color:#fff;">节点详情</h4><p style="color:var(--text-muted);font-size:11px;">点击节点查看角色详情<br>点击边编辑关系<br>Ctrl+拖拽添加连线</p></div>
</div>
</div></div>
<!-- Story Timeline -->
<div class="modal-overlay hidden" id="timelineModal"><div class="modal" style="max-width:1000px;">
<div class="modal-header"><h3>⏳ 故事时间线 — 仙道长青</h3><button class="modal-close" onclick="closeModal('timelineModal')" aria-label="关闭"></button></div>
<div class="modal-body">
<div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap;">
<button class="btn" style="font-size:10px;background:var(--accent);color:#fff;">全部</button>
<button class="btn" style="font-size:10px;">按卷</button>
<button class="btn" style="font-size:10px;">按角色</button>
<span style="font-size:10px;color:var(--text-muted);margin-left:auto;">粒度:<select style="padding:2px 6px;font-size:10px;background:var(--bg-tertiary);color:#fff;border:1px solid var(--border);border-radius:3px;"><option>按章</option><option>按天</option><option>按月</option></select></span>
</div>
<div class="timeline-container">
<div class="timeline-event" onclick="showToast('跳转到第1章')"><div class="tl-time">大陆历 732年 春 · 第1章</div><div class="tl-title">山村少年 — 林远出场</div><div class="tl-desc">林远在青石村过着平静的猎户生活,偶然发现山崖上的奇异符文。</div><span class="tl-type">📖 章节</span></div>
<div class="timeline-event" onclick="showToast('跳转到第2章')"><div class="tl-time">大陆历 732年 春 · 第2章(+3天)</div><div class="tl-title">意外觉醒 — 三纹灵根显现</div><div class="tl-desc">林远在危机中觉醒三纹灵根,击败妖兽。</div><span class="tl-type">⚡ 关键事件</span></div>
<div class="timeline-event conflict" onclick="showToast('跳转到第3章')"><div class="tl-time">大陆历 732年 春 · 第3章(+7天)⚠ 时间冲突</div><div class="tl-title">入门考验 — 测灵石测试</div><div class="tl-desc">林远抵达青云宗。角色年龄计算异常:林远应为16岁,推算显示17岁。</div><span class="tl-type">⚠ 冲突</span></div>
<div class="timeline-event" onclick="showToast('跳转到第13章')"><div class="tl-time">大陆历 732年 夏 · 第13章(+30天)</div><div class="tl-title">初入宗门 — 拜入青云宗</div><div class="tl-desc">林远正式拜入青云宗外门,结识柳青烟。</div><span class="tl-type">🤝 关系发展</span></div>
</div>
</div>
</div></div>
<!-- Export -->
<div class="modal-overlay hidden" id="exportModal"><div class="modal" style="max-width:500px;">
<div class="modal-header"><h3>📦 导出 — 第3章·入门考验</h3><button class="modal-close" onclick="closeModal('exportModal')" aria-label="关闭"></button></div>
<div class="modal-body"><div class="export-options">
<div class="export-opt" onclick="showToast('导出为 .txt');closeModal('exportModal')"><span class="export-icon">📄</span><div class="export-info"><span class="export-name">纯文本 .txt</span><span class="export-desc">无格式纯文本,兼容性最佳</span></div></div>
<div class="export-opt" onclick="showToast('导出为 .docx');closeModal('exportModal')"><span class="export-icon">📝</span><div class="export-info"><span class="export-name">Word 文档 .docx</span><span class="export-desc">保留格式,自动生成目录</span></div></div>
<div class="export-opt" onclick="showToast('导出为 .md');closeModal('exportModal')"><span class="export-icon">📋</span><div class="export-info"><span class="export-name">Markdown .md</span><span class="export-desc">适合发布到网络平台</span></div></div>
<div class="export-opt" onclick="showToast('导出为 PDF (A4模板)');closeModal('exportModal')"><span class="export-icon">📕</span><div class="export-info"><span class="export-name">PDF 文档 .pdf</span><span class="export-desc">A4/信纸排版,页眉含书名+章节名+页码</span></div></div>
<div class="export-opt" onclick="showToast('已复制到剪贴板(网文投稿格式)');closeModal('exportModal')"><span class="export-icon">📋</span><div class="export-info"><span class="export-name">复制到剪贴板</span><span class="export-desc">网文投稿预设:段首空两格、含章节标题</span></div></div>
<div class="export-opt" onclick="showToast('打包为 .novel 项目包');closeModal('exportModal')"><span class="export-icon">📦</span><div class="export-info"><span class="export-name">笔临项目包 .novel</span><span class="export-desc">整书数据打包,含版本历史+设定+大纲</span></div></div>
</div></div>
</div></div>
<!-- Export All -->
<div class="modal-overlay hidden" id="exportAllModal"><div class="modal" style="max-width:500px;">
<div class="modal-header"><h3>📦 导出所有书籍</h3><button class="modal-close" onclick="closeModal('exportAllModal')" aria-label="关闭"></button></div>
<div class="modal-body"><p style="color:var(--text-secondary);margin-bottom:12px;"><b>全部书籍</b>打包为 .novel-all 归档:所有章节版本、大纲、设定、AI对话、知识库、全局设置。</p><p style="font-size:11px;color:var(--orange);">⚠ 预计大小:约 45MB</p><div class="form-group" style="margin-top:12px;"><label>保存路径</label><input type="text" placeholder="选择保存路径..." value="D:/备份/笔临_2026-06-30.novel-all"></div></div>
<div class="modal-footer"><button class="btn" onclick="closeModal('exportAllModal')">取消</button><button class="btn primary" onclick="showToast('📦 正在打包导出...');closeModal('exportAllModal')">开始导出</button></div>
</div></div>
<!-- Chapter Template -->
<div class="modal-overlay hidden" id="templateModal"><div class="modal" style="max-width:650px;">
<div class="modal-header"><h3>📋 模板快速新建章节</h3><button class="modal-close" onclick="closeModal('templateModal')" aria-label="关闭"></button></div>
<div class="modal-body"><div class="template-grid">
<div class="template-card" onclick="applyTemplate('generic')"><div class="tpl-name">通用模板</div><div class="tpl-preview">## 第{chapterNumber}章 {chapterTitle}<br><br>正文内容...</div><span class="tpl-badge builtin">内置</span></div>
<div class="template-card" onclick="applyTemplate('webnovel')"><div class="tpl-name">网文常用</div><div class="tpl-preview">## 第{chapterNumber}章 {chapterTitle}<br><br>正文...<br><br>——<br>作者的话:</div><span class="tpl-badge builtin">内置</span></div>
<div class="template-card" onclick="applyTemplate('custom')"><div class="tpl-name">我的模板</div><div class="tpl-preview">## {volumeName} · 第{chapterNumber}章<br>关联:{outlineItem}<br>出场:{characterList}<br>{penName} | {date}</div><span class="tpl-badge custom">自定义</span></div>
</div><div class="form-group" style="margin-top:14px;"><label>章节标题</label><input type="text" placeholder="输入章节标题..." id="templateChapterTitle"></div></div>
<div class="modal-footer"><button class="btn" onclick="closeModal('templateModal')">取消</button><button class="btn primary" onclick="applyTemplate('generic');closeModal('templateModal')">创建章节</button></div>
</div></div>
<!-- Global Search -->
<div class="modal-overlay hidden" id="searchModal" role="dialog" aria-modal="true" aria-label="全局搜索">
<div class="modal" style="max-width:680px;">
<div class="modal-header"><h3>🔍 全局搜索</h3><button class="modal-close" onclick="closeModal('searchModal')" aria-label="关闭"></button></div>
<div class="modal-body">
<div class="search-input-row">
<input type="text" id="searchInput" placeholder="搜索章节、大纲、设定、灵感、地标..." autofocus>
<select style="padding:8px;font-size:11px;background:var(--bg-tertiary);color:#fff;border:1px solid var(--border);border-radius:4px;" aria-label="搜索范围"><option>全书</option><option>当前卷</option><option>当前章</option></select>
</div>
<div style="display:flex;gap:8px;margin-bottom:12px;font-size:11px;">
<label><input type="checkbox"> 正则</label><label><input type="checkbox"> 全词匹配</label><label><input type="checkbox"> 区分大小写</label>
</div>
<div id="searchResults">
<div class="search-result" onclick="showToast('跳转到第3章');closeModal('searchModal')"><div class="sr-type">章节 · 第3章 入门考验</div><div>...林远深吸一口气,推开木门走了出去。门外是一个宽阔的<b>演武场</b>...</div></div>
<div class="search-result" onclick="showToast('跳转到设定:测灵石');closeModal('searchModal')"><div class="sr-type">设定 · 测灵石</div><div class="sr-context">青云宗测试弟子灵根的法器,可显现一至五纹...</div></div>
<div class="search-result" onclick="showToast('跳转到地标');closeModal('searchModal')"><div class="sr-type">地标 · 第5章 TODO</div><div class="sr-context">[TODO: 补写林远与长老的私下对话]</div></div>
</div>
</div>
<div class="modal-footer"><button class="btn" onclick="closeModal('searchModal')">关闭</button><button class="btn" onclick="showToast('展开替换预览')">替换...</button></div>
</div>
</div>
<!-- Import Book -->
<div class="modal-overlay hidden" id="importModal" role="dialog" aria-modal="true" aria-label="导入书籍">
<div class="modal" style="max-width:520px;">
<div class="modal-header"><h3>📥 导入书籍</h3><button class="modal-close" onclick="closeModal('importModal')" aria-label="关闭"></button></div>
<div class="modal-body">
<p style="color:var(--text-secondary);font-size:12px;margin-bottom:12px;">支持 .txt、.docx、.md 及笔临专用 .novel 项目包</p>
<div class="form-group"><label>选择文件</label><button class="btn primary" style="width:100%;padding:12px;" onclick="showToast('打开文件选择器...')"><i class="fa-solid fa-folder-open"></i> 浏览文件...</button></div>
<div class="form-group"><label>章节分隔规则</label><select style="width:100%;padding:8px;background:var(--bg-tertiary);color:#fff;border:1px solid var(--border);border-radius:4px;"><option>自动识别(# 第.*章)</option><option>双换行分段</option><option>自定义正则</option></select></div>
<p style="font-size:11px;color:var(--text-muted);margin-top:8px;">大于 20MB 的文件将提示确认,导入过程显示进度条。</p>
</div>
<div class="modal-footer"><button class="btn" onclick="closeModal('importModal')">取消</button><button class="btn primary" onclick="showToast('📥 开始导入...');closeModal('importModal')">开始导入</button></div>
</div>
</div>
<!-- Inspiration Capture -->
<div class="modal-overlay hidden" id="inspirationModal" role="dialog" aria-modal="true" aria-label="灵感快速捕获">
<div class="modal" style="max-width:480px;">
<div class="modal-header"><h3>💡 灵感快速捕获</h3><button class="modal-close" onclick="closeModal('inspirationModal')" aria-label="关闭"></button></div>
<div class="modal-body inspiration-capture">
<textarea id="inspirationInput" placeholder="记录一闪而过的想法..."></textarea>
<div class="form-group" style="margin-top:10px;"><label>标签(可选)</label><input type="text" placeholder="如:伏笔、第三卷、角色" style="width:100%;padding:8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-tertiary);color:#fff;"></div>
<div class="form-group"><label>关联书籍</label><select style="width:100%;padding:8px;background:var(--bg-tertiary);color:#fff;border:1px solid var(--border);border-radius:4px;"><option>仙道长青(当前)</option><option>不关联书籍</option></select></div>
</div>
<div class="modal-footer"><button class="btn" onclick="showToast('🎤 语音输入...')"><i class="fa-solid fa-microphone"></i> 语音</button><button class="btn" onclick="closeModal('inspirationModal')">取消</button><button class="btn primary" onclick="saveInspiration()">保存</button></div>
</div>
</div>
<!-- Landmarks List -->
<div class="modal-overlay hidden" id="landmarksModal" role="dialog" aria-modal="true" aria-label="写作地标清单">
<div class="modal" style="max-width:560px;">
<div class="modal-header"><h3>📌 写作地标与待办</h3><button class="modal-close" onclick="closeModal('landmarksModal')" aria-label="关闭"></button></div>
<div class="modal-body">
<div style="display:flex;gap:6px;margin-bottom:12px;"><button class="btn" style="font-size:10px;background:var(--accent);color:#fff;">全部</button><button class="btn" style="font-size:10px;">待办</button><button class="btn" style="font-size:10px;">伏笔</button><button class="btn" style="font-size:10px;">待查证</button></div>
<div class="search-result" onclick="showToast('跳转到第5章地标');closeModal('landmarksModal')"><div class="sr-type">TODO · 第5章</div><div>补写林远与长老的私下对话</div></div>
<div class="search-result" onclick="showToast('跳转到第3章');closeModal('landmarksModal')"><div class="sr-type">伏笔提醒 · 第3章</div><div>测灵石异常发热 — 需在后续章节回收</div></div>
</div>
<div class="modal-footer"><button class="btn" onclick="closeModal('landmarksModal')">关闭</button><button class="btn primary" onclick="showToast('插入新地标');closeModal('landmarksModal')">+ 插入地标</button></div>
</div>
</div>
<!-- Chapter Bridge (衔接检查) -->
<div class="modal-overlay hidden" id="bridgeModal" role="dialog" aria-modal="true" aria-label="章间衔接检查">
<div class="modal" style="max-width:600px;">
<div class="modal-header"><h3>🔗 章间衔接 — 第2章 → 第3章</h3><button class="modal-close" onclick="closeModal('bridgeModal')" aria-label="关闭"></button></div>
<div class="modal-body">
<h4 style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">上一章末尾(第2章)</h4>
<div style="padding:12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:12px;line-height:1.7;margin-bottom:12px;border-left:3px solid var(--accent);">...三纹灵根觉醒的异象尚未散去,林远站在原地,感受着体内翻涌的灵力。他知道,从这一刻起,一切都将不同。</div>
<h4 style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">本章已写开头(第3章)</h4>
<div style="padding:12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:12px;line-height:1.7;margin-bottom:12px;">晨光透过稀疏的竹帘,在青石地面上投下细碎的光斑。林远深吸一口气,推开木门走了出去...</div>
<div style="padding:10px;background:var(--accent-glow);border-radius:var(--radius-sm);font-size:11px;color:var(--text-secondary);">💡 AI 建议:上一章以「一切都将不同」收束,本章可从时间跳转(数日后抵达宗门)或接续同一日清晨出发,避免重复「深吸一口气」式开头。</div>
</div>
<div class="modal-footer"><label style="font-size:11px;color:var(--text-muted);margin-right:auto;"><input type="checkbox"> 本章不再提示</label><button class="btn primary" onclick="closeModal('bridgeModal')">开始写作</button></div>
</div>
</div>
<!-- Onboarding Wizard -->
<div class="modal-overlay hidden" id="onboardingModal"><div class="modal" style="max-width:560px;">
<div class="modal-header"><h3>✒ 欢迎使用笔临</h3></div>
<div class="modal-body" id="onboardingBody">
<div class="onboarding-slide" id="obSlide0"><div class="ob-icon"></div><h2>欢迎使用笔临</h2><p>你的智能长篇创作伙伴<br>让每一个故事都被认真对待</p></div>
<div class="onboarding-slide hidden" id="obSlide1"><h2>📑 章节管理与版本历史</h2><p>分卷管理、自动快照、版本对比<br>每一次修改都有迹可循</p></div>
<div class="onboarding-slide hidden" id="obSlide2"><h2>🤖 AI 辅助写作</h2><p>上下文感知、剧情走向建议、文风学习<br>AI 服务于你的创作</p></div>
<div class="onboarding-slide hidden" id="obSlide3"><h2>🧠 知识库与伏笔追踪</h2><p>自动抽取角色、地点、伏笔<br>永不遗忘任何细节</p></div>
<div class="onboarding-slide hidden" id="obSlide4"><h2>设置你的笔名</h2><div class="form-group" style="max-width:300px;margin:16px auto;"><input type="text" placeholder="你的笔名(至少2个字符)" id="obPenName" style="text-align:center;"></div><div style="display:flex;align-items:center;gap:8px;justify-content:center;font-size:12px;color:var(--text-secondary);"><input type="checkbox" id="obCreateSample" checked><label>创建示例书籍"我的第一本书"</label></div></div>
</div>
<div class="onboarding-dots" id="obDots"><span class="dot active"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>
<div class="modal-footer"><button class="btn" id="obSkipBtn" onclick="skipOnboarding()">跳过</button><button class="btn primary" id="obNextBtn" onclick="nextOnboardingStep()">下一步</button></div>
</div></div>
<script>
// ============ STATE ============
let openTabs = []; // {id, type:'book'|'settings', name, bookId?}
let activeTabId = null;
let isFocusMode = false;
let wordCount = 2847;
// V2.0 new state
let selectedChapters = new Set();
let currentSound = 'none';
let isTTSPlaying = false;
let isSpellCheckOn = false;
let pomodoroSeconds = 25*60, pomodoroInterval = null, pomodoroRunning = false;
let onboardingStep = 0, onboardingTotalSlides = 5;
// 编辑器与侧栏状态
let activeVolId = 'vol-1';
const volLastChapter = { 'vol-1': 3, 'vol-2': 13 };
let currentEdit = { type: 'chapter', id: '3' };
const editorStore = {};
let outlineSeq = 4, settingSeq = 9, inspirationSeq = 2;
const settingCategoryMeta = {
char: { label: '人物角色', icon: '👤', iconClass: 'char', tag: '未分类' },
skill: { label: '技能功法', icon: '📜', iconClass: 'skill', tag: '未分类' },
world: { label: '世界观', icon: '🗺', iconClass: 'world', tag: '未分类' },
scene: { label: '场景地点', icon: '🏟', iconClass: 'scene', tag: '未分类' }
};
let lastSettingCategory = 'char';
const outlineTitles = {
'ol-1': '第一卷核心剧情线', 'ol-2': '林远觉醒三纹灵根',
'ol-3': '入门考验', 'ol-4': '拜入青云宗'
};
const settingTitles = {
'set-1': '林远', 'set-2': '白发长老', 'set-3': '柳青烟',
'set-4': '青云诀', 'set-5': '玄火掌', 'set-6': '九州大陆',
'set-7': '灵根体系', 'set-8': '青云宗演武场', 'set-9': '青石村'
};
const inspirationTitles = {
'ins-1': '林远的身世可能与太上长老有关...',
'ins-2': '考虑在第三卷加入妖兽潮事件'
};
const chapterTitles = {
'1': '山村少年', '2': '意外觉醒', '3': '入门考验',
'4': '灵根测试', '5': '初次修炼', '13': '初入宗门'
};
let activeInlineEdit = null;
const books = [
{ id: 1, name: '仙道长青', category: '玄幻', words: 128450, target: 500000, status: 'ongoing',
icon: '📖', color: '#7c8cf8', lastEdit: '10分钟前', chapters: 20, vols: 2 },
{ id: 2, name: '星海迷途', category: '科幻', words: 56200, target: 300000, status: 'draft', icon: '🚀',
color: '#f0c060', lastEdit: '3小时前', chapters: 8, vols: 1 },
];
function getBook(id) { return books.find(b => b.id === id); }
// ============ TAB MANAGEMENT ============
function openBookTab(bookId) {
const book = getBook(bookId);
if (!book) return;
const existing = openTabs.find(t => t.type === 'book' && t.bookId === bookId);
if (existing) {
switchTab(existing.id);
return;
}
const tabId = 'tab_' + Date.now();
openTabs.push({ id: tabId, type: 'book', name: book.name, bookId: bookId });
activeTabId = tabId;
renderTabs();
showEditorLayout(book);
}
function openSettingsTab() {
const existing = openTabs.find(t => t.type === 'settings');
if (existing) { switchTab(existing.id); return; }
const tabId = 'tab_settings';
openTabs.push({ id: tabId, type: 'settings', name: '系统设置' });
activeTabId = tabId;
renderTabs();
showSettingsPage();
}
function switchTab(tabId) {
activeTabId = tabId;
renderTabs();
const tab = openTabs.find(t => t.id === tabId);
if (!tab) return;
if (tab.type === 'book') {
showEditorLayout(getBook(tab.bookId));
} else if (tab.type === 'settings') {
showSettingsPage();
}
}
function closeTab(tabId, e) {
e.stopPropagation();
const idx = openTabs.findIndex(t => t.id === tabId);
if (idx === -1) return;
const closing = openTabs[idx];
openTabs.splice(idx, 1);
if (activeTabId === tabId) {
if (openTabs.length > 0) {
activeTabId = openTabs[Math.max(0, idx - 1)].id;
switchTab(activeTabId);
} else {
activeTabId = null;
renderTabs();
goHome();
return;
}
}
renderTabs();
}
function renderTabs() {
const container = document.getElementById('tabContainer');
container.innerHTML = openTabs.map(t => {
const isActive = t.id === activeTabId;
const dotColor = t.type === 'settings' ? '#aaa' : (getBook(t.bookId)?.color || '#7c8cf8');
return `<div class="tab ${isActive?'active':''}" onclick="switchTab('${t.id}')">
<span class="dot" style="background:${dotColor};"></span>
<span class="tab-label">${t.name}</span>
<span class="close-tab" onclick="closeTab('${t.id}',event)"><i class="fa-solid fa-xmark"></i></span>
</div>`;
}).join('');
}
// ============ VIEW SWITCHING ============
function goHome() {
document.getElementById('home-page').classList.remove('hidden');
document.getElementById('editor-layout').classList.add('hidden');
document.getElementById('settings-page').classList.add('hidden');
openTabs = [];
activeTabId = null;
renderTabs();
renderBookGrid();
}
function showEditorLayout(book) {
document.getElementById('home-page').classList.add('hidden');
document.getElementById('editor-layout').classList.remove('hidden');
document.getElementById('settings-page').classList.add('hidden');
document.getElementById('sidebarBookTitle').textContent = book.icon + ' ' + book.name;
const badge = document.getElementById('sidebarBookBadge');
if (book.status === 'ongoing') { badge.textContent = '连载中';
badge.style.background = 'var(--accent)'; } else if (book.status === 'draft') { badge.textContent =
'草稿';
badge.style.background = 'var(--orange)'; } else { badge.textContent = '已完成';
badge.style.background = 'var(--green)'; }
updateWordCount();
}
function showSettingsPage() {
document.getElementById('home-page').classList.add('hidden');
document.getElementById('editor-layout').classList.add('hidden');
document.getElementById('settings-page').classList.remove('hidden');
}
// ============ BOOK GRID ============
function renderBookGrid() {
const grid = document.getElementById('bookGrid');
grid.innerHTML = books.map(b => {
const progress = b.target ? Math.round((b.words / b.target) * 100) : 0;
const badgeClass = b.status === 'ongoing' ? 'badge-ongoing' : b.status === 'draft' ?
'badge-draft' : 'badge-done';
const badgeText = b.status === 'ongoing' ? '连载中' : b.status === 'draft' ? '草稿' : '已完成';
return `
<div class="book-card" onclick="openBookTab(${b.id})">
<div class="book-cover-icon" style="background:${b.color}22;color:${b.color};">${b.icon}</div>
<div class="book-name">${b.name}</div>
<div class="book-meta">
<span>📂 ${b.category}</span><span>📝 ${b.words.toLocaleString()}字</span><br>
<span>📑 ${b.chapters}章</span><span>🕐 ${b.lastEdit}</span>
</div>
<div class="book-progress"><div class="book-progress-fill" style="width:${progress}%;background:${b.color};"></div></div>
<span class="book-badge ${badgeClass}">${badgeText}</span>
</div>`;
}).join('') + `
<div class="book-card book-card-placeholder" onclick="showNewBookModal()">
<span class="plus-icon"><i class="fa-solid fa-plus"></i></span>
<span>创建新书籍</span>
</div>`;
document.getElementById('bookCount').textContent = books.length + '本';
// 最近编辑
const recentList = document.getElementById('recentList');
recentList.innerHTML = books.slice(0, 4).map(b =>
`<span style="padding:6px 12px;background:var(--bg-tertiary);border-radius:16px;font-size:11px;cursor:pointer;color:var(--text-secondary);" onclick="openBookTab(${b.id})">${b.icon} ${b.name} · ${b.lastEdit}</span>`
).join('');
}
function showNewBookModal() { document.getElementById('newBookModal').classList.remove('hidden'); }
function createNewBook() {
const name = document.getElementById('newBookName').value.trim();
if (!name) { showToast('请输入书名'); return; }
const category = document.getElementById('newBookCategory').value || '未分类';
const target = parseInt(document.getElementById('newBookTarget').value) || 300000;
const newBook = { id: Date.now(), name, category, words: 0, target, status: 'draft', icon: '📕',
color: '#7c8cf8', lastEdit: '刚刚', chapters: 0, vols: 1 };
books.unshift(newBook);
closeModal('newBookModal');
renderBookGrid();
openBookTab(newBook.id);
showToast('✅ 书籍已创建:' + name);
}
function showImportModal() { document.getElementById('importModal').classList.remove('hidden'); }
function showSearchModal() { document.getElementById('searchModal').classList.remove('hidden'); setTimeout(function() { var i = document.getElementById('searchInput'); if (i) i.focus(); }, 100); }
function showInspirationModal() { document.getElementById('inspirationModal').classList.remove('hidden'); setTimeout(function() { var i = document.getElementById('inspirationInput'); if (i) i.focus(); }, 100); }
function showLandmarksModal() { document.getElementById('landmarksModal').classList.remove('hidden'); }
function showBridgeModal() { document.getElementById('bridgeModal').classList.remove('hidden'); }
function saveInspiration() {
var t = document.getElementById('inspirationInput').value.trim();
if (!t) { showToast('请输入灵感内容'); return; }
closeModal('inspirationModal');
document.getElementById('inspirationInput').value = '';
const id = 'ins-' + (++inspirationSeq);
inspirationTitles[id] = t.slice(0, 60);
editorStore[editorKey('inspiration', id)] = t;
const list = document.getElementById('inspiration-list');
const item = document.createElement('div');
item.className = 'inspiration-item';
item.dataset.id = id;
item.onclick = function() { openInspirationItem(this, id); };
item.innerHTML = `<span class="item-title">${inspirationTitles[id]}</span>`;
bindTitleDblClick(item, 'inspiration', id, '.item-title');
list.appendChild(item);
switchSidebarPanel('inspiration-panel', document.querySelector('[data-sidebar-panel="inspiration-panel"]'));
openInspirationItem(item, id);
showToast('💡 灵感已保存并打开编辑');
}
function toggleReferencePanel() {
var panel = document.getElementById('reference-panel');
var btn = document.getElementById('btnReference');
panel.classList.toggle('open');
if (btn) btn.classList.toggle('active', panel.classList.contains('open'));
showToast(panel.classList.contains('open') ? '📎 引用面板已打开' : '引用面板已关闭');
}
function switchRefTab(type, el) {
document.querySelectorAll('.ref-tab').forEach(function(t) { t.classList.remove('active'); });
el.classList.add('active');
showToast('引用:' + (type === 'setting' ? '设定' : type === 'outline' ? '大纲' : '灵感'));
}
function setAiMode(mode, el) {
document.querySelectorAll('.ai-mode-tab').forEach(function(t) { t.classList.remove('active'); });
el.classList.add('active');
var labels = { chat: '对话', interactive: '交互写作', auto: '自动写作', wizard: '向导模式' };
showToast('AI 模式:' + (labels[mode] || mode));
}
function getEntryTitle(type, id) {
const sid = String(id);
if (type === 'chapter') return chapterTitles[sid] || document.querySelector(`.chapter-item[data-ch="${sid}"] .ch-title`)?.textContent?.trim() || '未命名';
if (type === 'outline') return outlineTitles[sid] || '未命名';
if (type === 'setting') return settingTitles[sid] || '未命名';
if (type === 'inspiration') return inspirationTitles[sid] || '新灵感…';
return '未命名';
}
function buildEditorLabel(type, id, title) {
const t = (title || '未命名').trim() || '未命名';
if (type === 'chapter') return `当前:第${id}章 · ${t}`;
if (type === 'outline') return `大纲:${t}`;
if (type === 'setting') {
const el = document.querySelector(`.setting-entry[data-id="${id}"]`);
const cat = el?.dataset.category || lastSettingCategory || 'char';
const catLabel = settingCategoryMeta[cat]?.label || '设定';
return `设定·${catLabel}${t}`;
}
if (type === 'inspiration') return `灵感:${t.length > 24 ? t.slice(0, 24) + '…' : t}`;
return t;
}
function updateEditorDocLabel() {
const labelEl = document.getElementById('editorDocLabel');
if (!labelEl || activeInlineEdit?.displayEl === labelEl) return;
labelEl.textContent = buildEditorLabel(currentEdit.type, currentEdit.id, getEntryTitle(currentEdit.type, currentEdit.id));
}
function updateOutlineItemTitle(id, title) {
const el = document.querySelector(`.outline-item[data-id="${id}"] .item-title`);
if (el && !el.querySelector('.inline-title-input')) el.textContent = title;
}
function updateInspirationItemTitle(id, title) {
const el = document.querySelector(`.inspiration-item[data-id="${id}"] .item-title`);
if (el && !el.querySelector('.inline-title-input')) el.textContent = title;
}
function bindTitleDblClick(el, type, id, selector) {
const titleEl = selector ? el.querySelector(selector) : el;
if (!titleEl || titleEl.dataset.titleBound) return;
titleEl.dataset.titleBound = '1';
titleEl.title = '双击修改标题';
titleEl.addEventListener('dblclick', function(e) {
e.stopPropagation();
e.preventDefault();
startSidebarTitleEdit(type, id, this);
});
}
function setEntryTitle(type, id, title) {
const sid = String(id);
const clean = (title || '').trim() || '未命名';
if (type === 'chapter') {
chapterTitles[sid] = clean;
const el = document.querySelector(`.chapter-item[data-ch="${sid}"] .ch-title`);
if (el && !el.querySelector('.inline-title-input')) el.textContent = clean;
} else if (type === 'outline') {
outlineTitles[sid] = clean;
updateOutlineItemTitle(sid, clean);
} else if (type === 'setting') {
settingTitles[sid] = clean;
const el = document.querySelector(`.setting-entry[data-id="${sid}"] .entry-name`);
if (el && !el.querySelector('.inline-title-input')) el.textContent = clean;
} else if (type === 'inspiration') {
inspirationTitles[sid] = clean;
updateInspirationItemTitle(sid, clean);
}
if (currentEdit.type === type && currentEdit.id === sid) updateEditorDocLabel();
}
function finishInlineTitleEdit(save) {
if (!activeInlineEdit) return;
const { displayEl, input, commitFn, restoreFn } = activeInlineEdit;
const newVal = input.value.trim();
const orig = displayEl.dataset.inlineOrig ?? '';
displayEl.removeAttribute('data-inline-orig');
if (restoreFn) {
if (save && newVal) commitFn(newVal);
restoreFn();
} else {
displayEl.textContent = save && newVal ? newVal : orig;
if (save && newVal && newVal !== orig) commitFn(newVal);
}
activeInlineEdit = null;
}
function startInlineTitleEdit(displayEl, commitFn, options) {
if (activeInlineEdit) finishInlineTitleEdit(true);
if (displayEl.querySelector('.inline-title-input')) return;
const originalText = (options?.initialValue ?? displayEl.textContent).trim();
const input = document.createElement('input');
input.type = 'text';
input.className = 'inline-title-input' + (options?.inputClass ? ' ' + options.inputClass : '');
input.value = originalText;
displayEl.dataset.inlineOrig = originalText;
displayEl.textContent = '';
displayEl.appendChild(input);
activeInlineEdit = {
displayEl,
input,
commitFn,
restoreFn: options?.restoreFn || null
};
input.focus();
input.select();
input.addEventListener('keydown', function(e) {
e.stopPropagation();
if (e.key === 'Enter') { e.preventDefault(); finishInlineTitleEdit(true); }
if (e.key === 'Escape') { e.preventDefault(); finishInlineTitleEdit(false); }
});
input.addEventListener('click', function(e) { e.stopPropagation(); });
input.addEventListener('dblclick', function(e) { e.stopPropagation(); });
}
function startSidebarTitleEdit(type, id, titleEl, e) {
const ev = e || window.event;
if (ev) { ev.stopPropagation(); ev.preventDefault(); }
startInlineTitleEdit(titleEl, function(newTitle) {
setEntryTitle(type, id, newTitle);
});
}
function startEditorLabelEdit(e) {
if (e) { e.stopPropagation(); e.preventDefault(); }
const labelEl = document.getElementById('editorDocLabel');
const { type, id } = currentEdit;
const title = getEntryTitle(type, id);
startInlineTitleEdit(labelEl, function(newTitle) {
setEntryTitle(type, id, newTitle);
}, {
initialValue: title,
inputClass: 'editor-label-input',
restoreFn: function() { updateEditorDocLabel(); }
});
}
// ============ EDITOR ============
function editorKey(type, id) { return type + ':' + id; }
function saveCurrentEditor() {
if (!currentEdit.type || currentEdit.id == null) return;
editorStore[editorKey(currentEdit.type, currentEdit.id)] =
document.getElementById('editor-content').innerText;
}
function clearListActiveStates() {
document.querySelectorAll('.chapter-item.active, .outline-item.active, .setting-entry.active, .inspiration-item.active')
.forEach(el => el.classList.remove('active'));
}
function openEditorEntry(type, id, label, fallbackContent) {
saveCurrentEditor();
currentEdit = { type, id: String(id) };
const key = editorKey(type, id);
const content = Object.prototype.hasOwnProperty.call(editorStore, key)
? editorStore[key]
: (fallbackContent ?? '');
editorStore[key] = content;
const editor = document.getElementById('editor-content');
editor.innerText = content;
updateEditorDocLabel();
updateWordCount();
setTimeout(() => {
editor.focus();
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}, 0);
}
function getChapterDefaultContent(chNum) {
if (chNum === 3) {
return `晨光透过稀疏的竹帘,在青石地面上投下细碎的光斑。
林远深吸一口气,推开木门走了出去。门外是一个宽阔的演武场,已经有数十名少年等在那里。他们的脸上有期待,有紧张,也有毫不掩饰的傲气。
"下一个,林远。"
听到自己的名字,他迈步走向那块通体漆黑的测灵石。周围的目光齐刷刷地落在他身上——这个来自偏远山村的少年,在入门考核中表现平平,谁也不曾把他放在眼里。
他将手掌贴上冰冷的石面。
刹那间,刺目的青光冲天而起,测灵石上浮现出三道清晰的光纹。全场鸦雀无声。
"三纹灵根……"负责测试的白发长老眯起眼睛,语气中带着一丝惊讶,"有意思。"`;
}
return '';
}
function getOutlineDefaultContent(id) {
const defaults = {
'ol-1': '第一卷主线:林远从山村少年成长为青云宗内门弟子,经历入门考验与灵根觉醒。',
'ol-2': '林远在入门考验中觉醒三纹灵根,引发宗门关注。',
'ol-3': '入门考验场景:演武场测灵,长老暗中观察林远天赋。',
'ol-4': '林远通过考验拜入青云宗,开启宗门修行线。'
};
return defaults[id] || '';
}
function getSettingDefaultContent(id) {
const defaults = {
'set-1': '林远\n身份:主角\n灵根:三纹灵根\n性格:沉稳内敛,遇事冷静',
'set-2': '白发长老\n身份:青云宗测试长老\n与林远:暗中关注其天赋',
'set-3': '柳青烟\n身份:女主\n与林远:同门师妹',
'set-4': '青云诀\n类型:核心功法\n阶段:入门、小成、大成',
'set-5': '玄火掌\n类型:攻击武技\n阶段:三阶段',
'set-6': '九州大陆\n范围:故事主世界地理设定',
'set-7': '灵根体系\n说明:灵根品阶决定修炼天赋上限',
'set-8': '青云宗演武场\n出现章节:第3章\n用途:入门考验场地',
'set-9': '青石村\n出现章节:第1章\n说明:林远故乡'
};
return defaults[id] || '';
}
function getInspirationDefaultContent(id) {
return inspirationTitles[id] || '';
}
// ============ SIDEBAR ============
function switchSidebarPanel(panelId, btn) {
document.querySelectorAll('#left-sidebar .sidebar-panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('#left-sidebar .nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById(panelId).classList.add('active');
btn.classList.add('active');
}
function toggleVolCollapse(header) {
header.classList.toggle('open');
header.nextElementSibling.classList.toggle('hidden');
}
function selectVolume(header) {
const volGroup = header.closest('.vol-group');
if (!volGroup) return;
const volId = volGroup.dataset.volId;
if (!header.classList.contains('open')) {
header.classList.add('open');
header.nextElementSibling.classList.remove('hidden');
}
document.querySelectorAll('.vol-header').forEach(h => h.classList.remove('active'));
header.classList.add('active');
activeVolId = volId;
const volChapters = volGroup.querySelector('.vol-chapters');
const chapters = volChapters.querySelectorAll('.chapter-item');
if (chapters.length === 0) {
showToast('该卷暂无章节,点击「新章」在此卷创建');
return;
}
const lastCh = volLastChapter[volId];
let target = lastCh ? volChapters.querySelector(`[data-ch="${lastCh}"]`) : null;
if (!target) target = chapters[0];
selectChapter(target, parseInt(target.dataset.ch, 10), { skipVolUpdate: true });
}
function selectChapter(el, chNum, opts) {
if (!el) return;
clearListActiveStates();
el.classList.add('active');
if (!opts?.skipVolUpdate) {
const volGroup = el.closest('.vol-group');
if (volGroup) {
activeVolId = volGroup.dataset.volId;
volLastChapter[activeVolId] = chNum;
document.querySelectorAll('.vol-header').forEach(h => h.classList.remove('active'));
const header = volGroup.querySelector('.vol-header');
if (header) header.classList.add('active');
}
}
const title = getEntryTitle('chapter', chNum);
openEditorEntry('chapter', chNum, buildEditorLabel('chapter', chNum, title), getChapterDefaultContent(chNum));
showToast(`已切换到第${chNum}`);
}
function getNextChapterNum() {
let max = 0;
document.querySelectorAll('.chapter-item').forEach(item => {
max = Math.max(max, parseInt(item.dataset.ch, 10) || 0);
});
return max + 1;
}
function getActiveVolChapters() {
const vol = document.querySelector(`.vol-group[data-vol-id="${activeVolId}"]`);
if (vol) return vol.querySelector('.vol-chapters');
return document.querySelector('.vol-chapters');
}
function addChapter() {
const volChapters = getActiveVolChapters();
if (!volChapters) { showToast('请先选择分卷'); return; }
const volGroup = volChapters.closest('.vol-group');
const volHeader = volGroup?.querySelector('.vol-header');
if (volHeader && !volHeader.classList.contains('open')) {
volHeader.classList.add('open');
volChapters.classList.remove('hidden');
}
const chNum = getNextChapterNum();
const newCh = document.createElement('div');
newCh.className = 'chapter-item';
newCh.setAttribute('data-ch', chNum);
newCh.setAttribute('tabindex', '0');
newCh.setAttribute('role', 'treeitem');
newCh.onclick = function() { selectChapter(this, chNum); };
newCh.innerHTML =
`<span class="ch-select" onclick="event.stopPropagation();toggleChapterSelect(this,${chNum})" aria-label="选择章节"></span>` +
`<span class="ch-num">第${chNum}章</span><span class="ch-title">新章节</span><span class="ch-badge draft">草稿</span>`;
volChapters.appendChild(newCh);
chapterTitles[String(chNum)] = '新章节';
bindTitleDblClick(newCh, 'chapter', chNum, '.ch-title');
if (volGroup) {
activeVolId = volGroup.dataset.volId;
volLastChapter[activeVolId] = chNum;
document.querySelectorAll('.vol-header').forEach(h => h.classList.remove('active'));
if (volHeader) volHeader.classList.add('active');
}
editorStore[editorKey('chapter', chNum)] = '';
selectChapter(newCh, chNum);
showToast(`已在当前卷创建第${chNum}`);
}
function addVolume() {
const tree = document.getElementById('chapter-tree');
const volNum = tree.querySelectorAll('.vol-group').length + 1;
const volId = 'vol-' + Date.now();
const volGroup = document.createElement('div');
volGroup.className = 'vol-group';
volGroup.dataset.volId = volId;
volGroup.innerHTML =
`<div class="vol-header open" onclick="selectVolume(this)" oncontextmenu="showVolContextMenu(event,this);return false;">` +
`<span class="vol-arrow" onclick="event.stopPropagation();toggleVolCollapse(this.parentElement)">▶</span> 第${volNum}卷·未命名` +
`<span class="vol-count">0章 · 0字</span>` +
`<span class="vol-menu-btn" onclick="event.stopPropagation();showVolContextMenu(event,this.parentElement);" aria-label="分卷菜单">⋯</span></div>` +
`<div class="vol-chapters"></div>`;
tree.appendChild(volGroup);
volLastChapter[volId] = null;
selectVolume(volGroup.querySelector('.vol-header'));
showToast(`已创建第${volNum}`);
}
function openOutlineItem(el, id) {
clearListActiveStates();
el.classList.add('active');
const title = getEntryTitle('outline', id);
openEditorEntry('outline', id, buildEditorLabel('outline', id, title), getOutlineDefaultContent(id));
showToast('正在编辑大纲:' + title);
}
function addOutlineItem() {
const id = 'ol-' + (++outlineSeq);
outlineTitles[id] = '新大纲条目';
editorStore[editorKey('outline', id)] = '';
const list = document.getElementById('outline-list');
const item = document.createElement('div');
item.className = 'outline-item uncovered';
item.dataset.id = id;
item.dataset.status = 'uncovered';
item.title = '新建条目,尚未关联章节';
item.onclick = function() { openOutlineItem(this, id); };
item.innerHTML = `<span class="item-prefix">⚠</span><span class="item-title">${outlineTitles[id]}</span>`;
list.appendChild(item);
bindTitleDblClick(item, 'outline', id, '.item-title');
switchSidebarPanel('outline-panel', document.querySelector('[data-sidebar-panel="outline-panel"]'));
openOutlineItem(item, id);
showToast('已创建并打开新大纲条目');
}
function openSettingItem(el, id) {
clearListActiveStates();
el.classList.add('active');
lastSettingCategory = el.dataset.category || 'char';
const title = getEntryTitle('setting', id);
const catLabel = settingCategoryMeta[lastSettingCategory]?.label || '设定';
openEditorEntry('setting', id, buildEditorLabel('setting', id, title), getSettingDefaultContent(id));
showToast('正在编辑设定:' + title);
}
function addSettingItem() {
const category = lastSettingCategory || 'char';
const meta = settingCategoryMeta[category];
const id = 'set-' + (++settingSeq);
settingTitles[id] = '新设定';
editorStore[editorKey('setting', id)] = meta.label + '\n\n';
const list = document.getElementById('setting-list');
let anchor = list.querySelector(`.setting-cat[data-category="${category}"]`);
if (!anchor) anchor = list.querySelector('.setting-cat');
const entry = document.createElement('div');
entry.className = 'setting-entry';
entry.dataset.id = id;
entry.dataset.category = category;
entry.onclick = function() { openSettingItem(this, id); };
entry.innerHTML =
`<span class="entry-icon ${meta.iconClass}">${meta.icon}</span>` +
`<span class="entry-name">${settingTitles[id]}</span><span class="entry-count">${meta.tag}</span>`;
bindTitleDblClick(entry, 'setting', id, '.entry-name');
if (anchor) {
let insertAfter = anchor;
while (insertAfter.nextElementSibling?.classList.contains('setting-entry')) {
insertAfter = insertAfter.nextElementSibling;
}
insertAfter.insertAdjacentElement('afterend', entry);
} else {
list.appendChild(entry);
}
switchSidebarPanel('setting-panel', document.querySelector('[data-sidebar-panel="setting-panel"]'));
openSettingItem(entry, id);
showToast('已创建并打开新设定条目');
}
function openInspirationItem(el, id) {
clearListActiveStates();
el.classList.add('active');
const title = getEntryTitle('inspiration', id);
openEditorEntry('inspiration', id, buildEditorLabel('inspiration', id, title), getInspirationDefaultContent(id));
showToast('正在编辑灵感');
}
function addInspirationItem() {
const id = 'ins-' + (++inspirationSeq);
inspirationTitles[id] = '新灵感…';
editorStore[editorKey('inspiration', id)] = '';
const list = document.getElementById('inspiration-list');
const item = document.createElement('div');
item.className = 'inspiration-item';
item.dataset.id = id;
item.onclick = function() { openInspirationItem(this, id); };
item.innerHTML = `<span class="item-title">${inspirationTitles[id]}</span>`;
bindTitleDblClick(item, 'inspiration', id, '.item-title');
list.appendChild(item);
switchSidebarPanel('inspiration-panel', document.querySelector('[data-sidebar-panel="inspiration-panel"]'));
openInspirationItem(item, id);
showToast('已创建并打开新灵感');
}
function toggleVol(header) { toggleVolCollapse(header); }
// ============ RIGHT PANEL ============
function switchRightPanel(panelId, btn) {
document.querySelectorAll('#right-panel .panel-content').forEach(p => p.classList.remove('active'));
document.querySelectorAll('#right-panel .panel-tab').forEach(b => b.classList.remove('active'));
document.getElementById(panelId).classList.add('active');
btn.classList.add('active');
}
// ============ SETTINGS PAGE NAV ============
function switchSettingsNav(section, btn) {
document.querySelectorAll('.settings-sidebar .settings-nav-item').forEach(i => i.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('#settingsContent .settings-section').forEach(s => s.classList.add('hidden'));
const target = document.getElementById('settings-' + section);
if (target) target.classList.remove('hidden');
}
// ============ AI CHAT ============
function sendMessage() {
const input = document.getElementById('chatInput');
const msg = input.value.trim();
if (!msg) return;
const chatMessages = document.getElementById('chatMessages');
const userMsg = document.createElement('div');
userMsg.className = 'chat-msg user';
userMsg.textContent = msg;
chatMessages.appendChild(userMsg);
input.value = '';
chatMessages.scrollTop = chatMessages.scrollHeight;
setTimeout(() => {
const aiMsg = document.createElement('div');
aiMsg.className = 'chat-msg ai';
if (msg.includes('续写') || msg.includes('继续')) {
aiMsg.innerHTML =
`<b>笔临AI</b> 正在续写...<br><br>林远收回手掌,测灵石上的光芒渐渐暗淡。周围少年的目光已经从轻视变成了复杂的打量。白发长老缓步上前,枯瘦的手指轻轻点了点他的眉心。"孩子,跟我来。"`;
} else {
aiMsg.innerHTML =
`<b>笔临AI</b> 收到。基于当前上下文,需要我提供接下来的剧情走向吗?<div class="plot-options"><div class="plot-opt" onclick="selectPlot('A')"><span class="opt-label">走向A</span> — 林远被长老单独召见。</div><div class="plot-opt" onclick="selectPlot('B')"><span class="opt-label">走向B</span> — 同门弟子开始拉拢林远。</div></div>`;
}
chatMessages.appendChild(aiMsg);
chatMessages.scrollTop = chatMessages.scrollHeight;
}, 600);
}
function quickCmd(cmd) { document.getElementById('chatInput').value = cmd + ' ';
document.getElementById('chatInput').focus(); }
function selectPlot(plotId) {
showToast(`已选择剧情走向 ${plotId}AI正在生成场景...`);
const chatMessages = document.getElementById('chatMessages');
const aiMsg = document.createElement('div');
aiMsg.className = 'chat-msg ai';
aiMsg.innerHTML =
`<b>笔临AI</b> 根据走向${plotId}生成场景:<br><br>白发长老领着林远穿过演武场侧门,走入一条幽深的长廊。"你的三纹灵根,"长老忽然停下脚步,"与百年前那位叛门而去的天才,一模一样。"`;
chatMessages.appendChild(aiMsg);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// ============ FOCUS MODE ============
function toggleFocusMode() {
isFocusMode = !isFocusMode;
const app = document.getElementById('app');
if (isFocusMode) { app.classList.add('focus-mode');
document.getElementById('editor-content').focus();
showToast('进入专注模式 · 按Esc退出'); } else { app.classList.remove('focus-mode');
showToast('已退出专注模式'); }
}
function saveSnapshot() { const name = prompt('为快照命名(可选):', '手动快照 ' + new Date()
.toLocaleTimeString()); if (name !== null) showToast('📸 快照已保存:' + (name || '未命名')); }
function updateWordCount() {
const content = document.getElementById('editor-content').innerText;
wordCount = content.replace(/\s/g, '').length;
saveCurrentEditor();
const display = document.getElementById('wordCountDisplay');
display.textContent = wordCount.toLocaleString() + ' 字';
display.classList.toggle('warn', currentEdit.type === 'chapter' && wordCount >= 3000);
const statusChapter = document.querySelector('#editor-statusbar span:nth-child(2)');
if (statusChapter) {
statusChapter.innerHTML = currentEdit.type === 'chapter'
? `📖 本章:<b>${wordCount.toLocaleString()}</b> / 提醒 3,000`
: `📖 编辑:<b>${currentEdit.type === 'outline' ? '大纲' : currentEdit.type === 'setting' ? '设定' : currentEdit.type === 'inspiration' ? '灵感' : '条目'}</b>`;
}
}
// ============ MODALS ============
function showVersionModal() { document.getElementById('versionModal').classList.remove('hidden'); }
function showNamingModal() { document.getElementById('namingModal').classList.remove('hidden'); }
function showDashboardModal() {
document.getElementById('dashboardModal').classList.remove('hidden');
const container = document.getElementById('heatmapContainer');
container.innerHTML = Array.from({ length: 30 }, (_, i) => {
const cls = Math.random() > 0.35 ? (Math.random() > 0.5 ? 'filled' : 'high') : '';
return `<span class="heatmap-cell ${cls}" title="6月${i+1}日"></span>`;
}).join('');
}
function closeModal(id) { document.getElementById(id).classList.add('hidden'); if (id === 'versionModal')
document.getElementById('diffViewArea')?.classList.add('hidden'); }
function showDiffView() { document.getElementById('diffViewArea').classList.remove('hidden');
document.getElementById('diffViewArea').scrollIntoView({ behavior: 'smooth' }); }
function selectName(name) { showToast(`✅ 已选择命名:${name}`);
closeModal('namingModal'); }
function showToast(msg) {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = msg;
container.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
}
// ============ GLOBAL SHORTCUTS ============
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
if (isFocusMode) toggleFocusMode();
if (document.getElementById('reading-mode').classList.contains('active')) exitReadingMode();
closeAllCtxMenus();
}
if (e.ctrlKey && e.key === 's') { e.preventDefault(); showToast('💾 已保存'); }
if (e.ctrlKey && e.key === 'k') { e.preventDefault(); document.getElementById('chatInput').focus(); }
if (e.ctrlKey && e.altKey && e.key === 'f') { e.preventDefault(); toggleFocusMode(); }
if (e.ctrlKey && e.shiftKey && e.key === 'R') { e.preventDefault(); toggleTTS(); }
if (e.ctrlKey && e.shiftKey && e.key === 'N') { e.preventDefault(); addChapter(); }
if (e.ctrlKey && e.shiftKey && e.key === 'F') { e.preventDefault(); showSearchModal(); }
if (e.ctrlKey && e.shiftKey && e.key === 'I') { e.preventDefault(); showInspirationModal(); }
if (e.ctrlKey && e.shiftKey && e.key === 'P') { e.preventDefault(); toggleReferencePanel(); }
if (e.ctrlKey && e.shiftKey && e.key === 'M') { e.preventDefault(); showLandmarksModal(); }
if (e.ctrlKey && e.shiftKey && e.key === 'L') { e.preventDefault(); showLandmarksModal(); }
});
// ============ V2.0 NEW FUNCTIONS ============
// Theme
function switchTheme(theme) {
document.documentElement.removeAttribute('data-theme');
if (theme !== 'default') document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('bilin-theme', theme);
}
(function() {
const t = localStorage.getItem('bilin-theme');
if (t && t !== 'default') document.documentElement.setAttribute('data-theme', t);
const sel = document.getElementById('themeSelect');
if (sel && t) sel.value = t;
})();
// Batch selection
function toggleChapterSelect(el, chNum) {
el.classList.toggle('checked');
const item = el.parentElement;
if (el.classList.contains('checked')) { selectedChapters.add(chNum); item.classList.add('selected'); }
else { selectedChapters.delete(chNum); item.classList.remove('selected'); }
updateBatchToolbar();
}
function updateBatchToolbar() {
const tb = document.getElementById('batchToolbar');
if (selectedChapters.size > 0) { tb.classList.add('show'); document.getElementById('batchCount').textContent = '已选 ' + selectedChapters.size + ' 章'; }
else tb.classList.remove('show');
}
function clearBatchSelection() {
selectedChapters.clear();
document.querySelectorAll('.chapter-item').forEach(i => { i.classList.remove('selected'); const s = i.querySelector('.ch-select'); if (s) s.classList.remove('checked'); });
updateBatchToolbar(); showToast('已取消选择');
}
function batchSetStatus(status) { showToast('已批量设为 ' + status + '' + selectedChapters.size + ' 章'); clearBatchSelection(); }
function batchExport() { showToast('批量导出 ' + selectedChapters.size + ' 章...'); clearBatchSelection(); }
// TTS
function toggleTTS() {
isTTSPlaying = !isTTSPlaying;
const btn = document.getElementById('btnTTS'), rbtn = document.getElementById('readingTTSBtn');
if (isTTSPlaying) { if (btn) btn.classList.add('tts-playing'); if (rbtn) rbtn.classList.add('tts-playing'); showToast('🔊 开始朗读...'); }
else { if (btn) btn.classList.remove('tts-playing'); if (rbtn) rbtn.classList.remove('tts-playing'); showToast('🔇 朗读已暂停'); }
}
// Spell check
function toggleSpellCheck() {
isSpellCheckOn = !isSpellCheckOn;
const btn = document.getElementById('btnSpellCheck');
if (isSpellCheckOn) { btn.classList.add('active'); showToast('✅ 拼写检查已开启'); }
else { btn.classList.remove('active'); showToast('拼写检查已关闭'); }
}
// Pomodoro
function togglePomodoro() { if (pomodoroRunning) { stopPomodoro(); return; } pomodoroRunning = true; pomodoroSeconds = 25*60; document.getElementById('pomodoroTimer').classList.add('running'); updatePomodoroDisplay(); showToast('🍅 番茄钟开始 · 25分钟'); pomodoroInterval = setInterval(function() { pomodoroSeconds--; updatePomodoroDisplay(); if (pomodoroSeconds <= 0) { stopPomodoro(); showToast('🍅 番茄钟完成!新增 320 字'); } }, 1000); }
function stopPomodoro() { pomodoroRunning = false; clearInterval(pomodoroInterval); document.getElementById('pomodoroTimer').classList.remove('running'); updatePomodoroDisplay(); }
function updatePomodoroDisplay() { var m = Math.floor(pomodoroSeconds / 60), s = pomodoroSeconds % 60; document.getElementById('pomodoroTimer').innerHTML = '🍅 ' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); }
// Sound
function toggleSoundPopover() { document.getElementById('soundPopover').classList.toggle('show'); }
function selectSound(sound, el) {
currentSound = sound;
document.querySelectorAll('.sound-item').forEach(function(i) { i.classList.remove('active'); });
el.classList.add('active'); document.getElementById('soundPopover').classList.remove('show');
var label = el.textContent.trim().split('\n')[0].trim();
document.getElementById('focusSoundLabel').textContent = sound === 'none' ? '无音效' : label;
showToast(sound === 'none' ? '🔇 音效已关闭' : '🎵 已选择音效:' + label);
}
document.addEventListener('click', function(e) {
var pop = document.getElementById('soundPopover'), btn = document.getElementById('btnSound');
if (pop && !pop.contains(e.target) && e.target !== btn && !btn.contains(e.target)) pop.classList.remove('show');
});
// Reading mode
function enterReadingMode() { document.getElementById('reading-mode').classList.add('active'); document.getElementById('readingContent').scrollTop = 0; closeAllCtxMenus(); showToast('进入阅读模式 · 按Esc或点击退出'); }
function exitReadingMode() { document.getElementById('reading-mode').classList.remove('active'); }
function setReadingDevice(device, btn) { var rm = document.getElementById('reading-mode'); rm.classList.remove('device-phone', 'device-tablet'); if (device !== 'desktop') rm.classList.add('device-' + device); btn.parentElement.querySelectorAll('button').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); }
function setReadingFont(font, btn) { var rm = document.getElementById('reading-mode'); rm.classList.remove('font-serif', 'font-sans', 'font-eyecare'); rm.classList.add('font-' + font); btn.parentElement.querySelectorAll('button').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); }
// Volume context menu
function showVolContextMenu(e, header) { e.preventDefault(); var menu = document.getElementById('volCtxMenu'); menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px'; menu.classList.add('show'); }
function closeAllCtxMenus() { document.getElementById('volCtxMenu').classList.remove('show'); }
document.addEventListener('click', function(e) { if (!e.target.closest('.ctx-menu') && !e.target.closest('.vol-menu-btn')) closeAllCtxMenus(); });
// Character graph (Cytoscape.js)
var cyInstance = null;
function showGraphModal() { document.getElementById('graphModal').classList.remove('hidden'); setTimeout(initGraph, 300); }
function initGraph() {
if (cyInstance) { cyInstance.resize(); return; }
var container = document.getElementById('graphContainer');
if (!container || typeof cytoscape === 'undefined') return;
cyInstance = cytoscape({
container: container,
style: [
{ selector: 'node', style: { 'background-color': '#6c63ff', 'label': 'data(label)', 'color': '#e0e0e0', 'font-size': '12px', 'text-valign': 'center', 'text-halign': 'center', 'width': 50, 'height': 50, 'border-width': 2, 'border-color': '#9da8fa' } },
{ selector: 'edge', style: { 'width': 2, 'line-color': '#4ea8de', 'target-arrow-color': '#4ea8de', 'target-arrow-shape': 'triangle', 'curve-style': 'bezier', 'label': 'data(label)', 'color': '#b0b0b8', 'font-size': '10px' } },
{ selector: '.high', style: { 'line-color': '#e0556a', 'target-arrow-color': '#e0556a', 'width': 4 } }
],
elements: [
{ data: { id: 'linyuan', label: '林远' } }, { data: { id: 'zhanglao', label: '白发长老' } },
{ data: { id: 'liuqingyan', label: '柳青烟' } }, { data: { id: 'zongmen', label: '青云宗' } },
{ data: { id: 'shixiong', label: '大师兄' } },
{ data: { id: 'e1', source: 'linyuan', target: 'zhanglao', label: '注意 (3)' } },
{ data: { id: 'e2', source: 'linyuan', target: 'liuqingyan', label: '暗恋 (4)' }, classes: 'high' },
{ data: { id: 'e3', source: 'linyuan', target: 'zongmen', label: '归属 (2)' } },
{ data: { id: 'e4', source: 'shixiong', target: 'linyuan', label: '嫉妒 (1)' } },
{ data: { id: 'e5', source: 'zhanglao', target: 'zongmen', label: '执掌 (3)' } }
],
layout: { name: 'cose-bilkent', animate: true, animationDuration: 1000 }
});
cyInstance.on('tap', 'node', function(evt) { document.getElementById('graphSidebar').classList.add('show'); document.getElementById('graphSidebar').querySelector('h4').textContent = '角色:' + evt.target.data('label'); });
cyInstance.on('tap', function(evt) { if (evt.target === cyInstance) document.getElementById('graphSidebar').classList.remove('show'); });
}
function setGraphLayout(layout) {
if (!cyInstance) return;
document.querySelectorAll('#graphModal .graph-controls button').forEach(function(b, i) { if (i < 3) b.classList.remove('active'); });
event.target.classList.add('active');
var layouts = { grid: { name: 'grid' }, circle: { name: 'circle' }, cose: { name: 'cose-bilkent', animate: true } };
cyInstance.layout(layouts[layout] || layouts.cose).run();
}
// Modals (new)
function showTimelineModal() { document.getElementById('timelineModal').classList.remove('hidden'); }
function showExportModal() { document.getElementById('exportModal').classList.remove('hidden'); }
function showExportAllModal() { document.getElementById('exportAllModal').classList.remove('hidden'); }
function showTemplateModal() { document.getElementById('templateModal').classList.remove('hidden'); }
function applyTemplate(type) { var title = document.getElementById('templateChapterTitle').value || '新章节'; showToast('📋 使用模板创建章节:' + title); addChapter(); }
function showWordFreqInPanel() { var tabBtn = document.querySelector('#right-panel .panel-tab:nth-child(3)'); if (tabBtn) switchRightPanel('wordfreq', tabBtn); }
function recordShortcut(el) { el.classList.add('recording'); el.textContent = '按下组合键...'; setTimeout(function() { el.classList.remove('recording'); el.textContent = 'Ctrl+N'; showToast('快捷键已录制'); }, 2000); }
// Onboarding
function showOnboarding() { onboardingStep = 0; document.getElementById('onboardingModal').classList.remove('hidden'); updateOnboardingSlide(); }
function updateOnboardingSlide() {
for (var i = 0; i < onboardingTotalSlides; i++) { var slide = document.getElementById('obSlide' + i); if (slide) slide.classList.toggle('hidden', i !== onboardingStep); }
document.querySelectorAll('#obDots .dot').forEach(function(d, i) { d.classList.toggle('active', i === onboardingStep); });
var nextBtn = document.getElementById('obNextBtn'), skipBtn = document.getElementById('obSkipBtn');
if (onboardingStep === onboardingTotalSlides - 1) { nextBtn.textContent = '开始创作'; skipBtn.style.display = 'none'; }
else { nextBtn.textContent = '下一步'; skipBtn.style.display = 'inline-block'; }
}
function nextOnboardingStep() { if (onboardingStep < onboardingTotalSlides - 1) { onboardingStep++; updateOnboardingSlide(); } else completeOnboarding(); }
function skipOnboarding() { completeOnboarding(); }
function completeOnboarding() {
var penName = (document.getElementById('obPenName') && document.getElementById('obPenName').value.trim()) || '未命名作者';
var createSample = document.getElementById('obCreateSample') && document.getElementById('obCreateSample').checked;
closeModal('onboardingModal'); showToast('🎉 欢迎,' + penName + '!开始你的创作之旅吧!');
if (createSample) { var b = { id: Date.now(), name: '我的第一本书', category: '未分类', words: 0, target: 100000, status: 'draft', icon: '📕', color: '#7c8cf8', lastEdit: '刚刚', chapters: 0, vols: 1 }; books.unshift(b); renderBookGrid(); }
}
// Update selectChapter for batch
var origSelectChapter = selectChapter;
selectChapter = function(el, chNum, opts) {
if (!opts?.skipVolUpdate && typeof event !== 'undefined' && event && (event.ctrlKey || event.metaKey || event.shiftKey)) {
toggleChapterSelect(el.querySelector('.ch-select'), chNum);
return;
}
origSelectChapter(el, chNum, opts);
};
// Update updateWordCount for daily goal
var origUpdateWordCount = updateWordCount;
updateWordCount = function() {
origUpdateWordCount();
var goalFill = document.getElementById('goalFill'), goalLabel = document.getElementById('goalLabel');
if (!goalFill || !goalLabel) return;
var dailyGoal = 3000, todayWords = 1280 + Math.floor(Math.random() * 200);
var pct = Math.min(100, Math.round((todayWords / dailyGoal) * 100));
goalFill.style.width = pct + '%';
goalFill.classList.toggle('warn', pct >= 80 && pct < 100);
goalFill.classList.toggle('done', pct >= 100);
goalLabel.textContent = todayWords.toLocaleString() + '/' + dailyGoal.toLocaleString();
};
// Override closeModal for graph cleanup
var origCloseModal = closeModal;
closeModal = function(id) {
origCloseModal(id);
if (id === 'graphModal' && cyInstance) { cyInstance.destroy(); cyInstance = null; }
};
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', function(e) { if (e.target === this) { this.classList.add('hidden'); if (
this.id === 'versionModal') document.getElementById('diffViewArea')?.classList.add(
'hidden'); } });
});
// ============ INIT ============
editorStore[editorKey('chapter', '3')] = document.getElementById('editor-content').innerText.trim();
document.querySelectorAll('.chapter-item').forEach(function(item) {
const ch = item.dataset.ch;
const titleEl = item.querySelector('.ch-title');
if (ch && titleEl) chapterTitles[ch] = titleEl.textContent.trim();
});
document.addEventListener('click', function(e) {
if (activeInlineEdit && !e.target.classList.contains('inline-title-input')) {
finishInlineTitleEdit(true);
}
});
renderBookGrid();
updateWordCount();
document.getElementById('editor-content').addEventListener('focus', function() {
if (this.textContent.trim() === '') this.innerHTML = '<span class="placeholder">开始书写你的故事...</span>';
});
document.getElementById('editor-content').addEventListener('blur', function() {
if (this.textContent.trim() === '') this.innerHTML = '<span class="placeholder">开始书写你的故事...</span>';
});
console.log('✒ 笔临 - 长篇创作智能协作平台 已就绪');
console.log(' 文人雅韵七色:墨砚 | 竹语 | 月夜 | 宣纸 | 晨雾 | 茶园 | 暮光');
// Show onboarding on first visit
if (!localStorage.getItem('bilin-onboarded')) { setTimeout(showOnboarding, 600); localStorage.setItem('bilin-onboarded', '1'); }
// 窗口控制按钮(模拟效果 + 实际浏览器窗口控制提示)
document.getElementById('btn-minimize').addEventListener('click', () => {
// Web应用中无法真正最小化,可提示或模拟
showToast('最小化功能需在客户端环境中使用');
});
document.getElementById('btn-maximize').addEventListener('click', () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {
showToast('全屏切换失败,请使用浏览器快捷键');
});
} else {
document.exitFullscreen();
}
});
document.getElementById('btn-close').addEventListener('click', () => {
// 浏览器中 window.close() 通常只能关闭由脚本打开的窗口
window.close();
// 如果无法关闭,给出提示
setTimeout(() => {
showToast('请使用 Ctrl+W 或浏览器关闭按钮退出');
}, 100);
});
</script>
</body>
</html>