aimdeepcafe commited on
Commit
301676d
·
verified ·
1 Parent(s): 44495ab

Update book.html

Browse files
Files changed (1) hide show
  1. book.html +25 -32
book.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>爱小说</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
9
  <script>
@@ -34,7 +34,7 @@
34
  }
35
  }
36
  @layer components {
37
- .btn { @apply px-4 py-2 rounded-lg font-semibold text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-dark-bg disabled:opacity-50 disabled:cursor-not-allowed; }
38
  .btn-primary { @apply btn bg-primary text-white hover:bg-primary-hover focus:ring-primary-focus; }
39
  .btn-secondary { @apply btn bg-slate-200 text-slate-700 hover:bg-slate-300 focus:ring-slate-400 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600 dark:focus:ring-slate-500; }
40
  .input-base { @apply w-full p-2 border border-slate-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-primary-focus focus:border-transparent dark:bg-slate-700 dark:border-slate-600 dark:text-dark-text; }
@@ -51,19 +51,19 @@
51
  <body class="bg-light-bg font-sans text-light-text min-h-screen flex flex-col dark:bg-dark-bg dark:text-dark-text">
52
 
53
  <header class="bg-light-card/80 dark:bg-dark-card/80 backdrop-blur-sm shadow-sm sticky top-0 z-50 border-b border-slate-200 dark:border-slate-800">
54
- <div class="container mx-auto px-4 py-3 flex justify-between items-center">
55
- <h1 class="text-2xl font-bold text-sky-600 dark:text-sky-500 flex items-center"><i class="fa fa-book mr-2"></i>爱小说</h1>
56
  <div class="hidden md:flex items-center space-x-2">
57
  <button id="custom-prompt-btn" class="btn-secondary" title="自定义情节"><i class="fa fa-magic mr-2"></i>自定义</button>
58
  <button id="regenerate-chapter-btn" class="btn-secondary" title="重新生成"><i class="fa fa-refresh mr-2"></i>重写</button>
59
  <button id="continue-story-btn" class="btn-primary" title="继续故事"><i class="fa fa-play mr-2"></i>继续创作</button>
60
- <span class="text-slate-300 dark:text-slate-600 mx-2">|</span>
61
  <button id="open-story-preset-btn" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700" title="故事设定"><i class="fa fa-file-text-o"></i></button>
62
  <button id="open-model-settings-btn" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700" title="模型设置"><i class="fa fa-cogs"></i></button>
63
  <button id="theme-toggle-btn" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"><i class="fa fa-moon-o"></i></button>
64
  </div>
65
  <div class="md:hidden">
66
- <button id="continue-story-btn-mobile" class="btn-primary" title="继续故事"><i class="fa fa-play"></i></button>
67
  <button id="mobile-menu-btn" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700 ml-2" title="更多操作"><i class="fa fa-ellipsis-v"></i></button>
68
  </div>
69
  </div>
@@ -78,33 +78,33 @@
78
  </div>
79
  </header>
80
 
81
- <main class="flex-grow container mx-auto px-4 py-8 flex flex-col">
82
- <div class="mb-6 flex justify-between items-center">
83
  <button id="prev-chapter-btn" class="btn-secondary"><i class="fa fa-arrow-left mr-2"></i>上章</button>
84
  <span id="chapter-info" class="text-sm text-light-subtext dark:text-dark-subtext">无内容</span>
85
  <button id="next-chapter-btn" class="btn-secondary">下章<i class="fa fa-arrow-right ml-2"></i></button>
86
  </div>
87
  <div class="bg-light-card rounded-xl shadow-lg flex-grow flex flex-col overflow-hidden border border-slate-200 dark:border-slate-700/50" style="background-color: var(--reading-bg-color);">
88
- <div id="reading-area" class="p-8 md:p-10 lg:p-12 min-h-[500px] relative flex-grow overflow-y-auto scrollbar-hide" style="color: var(--reading-text-color); font-family: var(--reading-font); font-size: var(--reading-font-size); line-height: var(--reading-line-height); letter-spacing: var(--reading-letter-spacing);">
89
  <div id="loading-state" class="absolute inset-0 flex items-center justify-center bg-light-card/80 dark:bg-dark-card/80 z-10 hidden">
90
  <div class="text-center">
91
- <div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary mb-4"></div>
92
  <p class="text-light-subtext dark:text-dark-subtext">AI创作中...</p>
93
  </div>
94
  </div>
95
  <div id="content-container" class="hidden">
96
- <h1 id="chapter-title-display" class="text-3xl md:text-4xl font-bold mb-8 text-center" style="color: var(--reading-text-color);"></h1>
97
  <div id="content-area" class="prose max-w-none"></div>
98
  </div>
99
- <div id="welcome-message" class="text-center p-10 flex flex-col items-center justify-center h-full">
100
- <i class="fa fa-magic text-6xl text-slate-300 dark:text-slate-600 mb-6"></i>
101
- <h2 class="text-2xl font-bold text-light-text dark:text-dark-text mb-2">欢迎使用 爱小说</h2>
102
- <p class="text-light-subtext dark:text-dark-subtext max-w-md mx-auto">请先点击顶部的 <i class="fa fa-cogs"></i> 图标配置AI模型,然后通过 <i class="fa fa-file-text-o"></i> 设定故事,或直接 <i class="fa fa-play"></i> 开始创作。</p>
103
  </div>
104
  </div>
105
- <div class="bg-light-card/80 border-t border-slate-200/80 p-3 flex justify-between items-center backdrop-blur-sm dark:bg-dark-card/80 dark:border-t-slate-700/50">
106
  <div class="flex items-center space-x-3 text-sm text-light-subtext dark:text-dark-subtext">
107
- <div class="flex items-center"><i class="fa fa-file-word-o mr-2"></i><span class="hidden sm:inline">字数: </span><span id="word-count">0</span></div>
108
  <div id="pagination-controls" class="flex items-center space-x-1 hidden">
109
  <button id="prev-page-btn" class="p-1 rounded-md hover:bg-slate-200 dark:hover:bg-slate-700 disabled:opacity-50" title="上一页"><i class="fa fa-chevron-left text-xs"></i></button>
110
  <span id="page-info" class="text-xs px-1"></span>
@@ -121,11 +121,11 @@
121
  </div>
122
  </main>
123
 
124
- <div id="reading-settings-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-md max-h-[90vh] flex flex-col dark:bg-dark-card"><div class="p-5 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">阅读设置</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-6 space-y-6 overflow-y-auto"><div><h4 class="text-md font-medium mb-3 text-light-text dark:text-dark-text"><i class="fa fa-font mr-2 text-primary"></i>字体样式</h4><div class="space-y-4"><div><label for="font-size-slider" class="block text-sm font-medium mb-1 text-light-subtext dark:text-dark-subtext">字体大小: <span id="font-size-value">18px</span></label><input type="range" id="font-size-slider" min="12" max="32" step="1" value="18" class="w-full"></div><div><label for="line-height-slider" class="block text-sm font-medium mb-1 text-light-subtext dark:text-dark-subtext">行间距: <span id="line-height-value">1.75</span></label><input type="range" id="line-height-slider" min="1.2" max="2.5" step="0.05" value="1.75" class="w-full"></div></div></div><div><h4 class="text-md font-medium mb-3 text-light-text dark:text-dark-text"><i class="fa fa-upload mr-2 text-primary"></i>自定义字体</h4><input type="file" id="font-upload-input" accept=".ttf,.otf,.woff,.woff2" class="block w-full text-sm text-light-subtext file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-sky-100 file:text-primary hover:file:bg-sky-200 cursor-pointer dark:file:bg-sky-900/50 dark:hover:file:bg-sky-900"></div><div><h4 class="text-md font-medium mb-3 text-light-text dark:text-dark-text"><i class="fa fa-comments-o mr-2 text-primary"></i>对话高亮</h4><div class="space-y-3"><label class="flex items-center space-x-3 cursor-pointer"><input type="checkbox" id="dialogue-bold-toggle" class="h-4 w-4 rounded border-slate-300 text-primary focus:ring-primary dark:bg-slate-700 dark:border-slate-600"><span class="text-sm font-medium">对话内容加粗</span></label><div class="flex items-center space-x-3"><label for="dialogue-color-picker" class="text-sm font-medium text-light-subtext dark:text-dark-subtext">对话文字颜色:</label><input type="color" id="dialogue-color-picker" value="#334155" class="w-10 h-8 p-1 border rounded-md cursor-pointer dark:bg-slate-700 dark:border-slate-600"><button id="reset-dialogue-color-btn" class="text-xs text-light-subtext hover:text-primary">重置</button></div></div></div></div></div></div>
125
- <div id="custom-prompt-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-lg dark:bg-dark-card"><div class="p-5 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">自定义生成</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-6"><textarea id="custom-prompt-input" class="input-base h-40" placeholder="请输入你希望接下来发生的情节..."></textarea><div class="mt-4 flex justify-end"><button id="submit-custom-prompt-btn" class="btn-primary">生成</button></div></div></div></div>
126
- <div id="edit-chapter-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col dark:bg-dark-card"><div class="p-5 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">编辑章节</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-6 flex-grow flex flex-col"><input type="text" id="edit-chapter-title-input" class="input-base mb-4" placeholder="章节标题"><textarea id="edit-chapter-content-input" class="input-base flex-grow w-full resize-none" style="min-height: 300px;"></textarea><div class="mt-4 flex justify-between items-center"><button id="delete-chapter-btn" class="btn bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 flex items-center"><i class="fa fa-trash mr-2"></i>删除章节</button><div><button id="save-edit-btn" class="btn-primary">保存</button></div></div></div></div></div>
127
- <div id="model-settings-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col dark:bg-dark-card"><div class="p-5 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">模型设置</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-6 space-y-6 overflow-y-auto"><div class="space-y-4"><h2 class="text-lg font-semibold flex items-center text-sky-600 dark:text-sky-500 -mb-2"><i class="fa fa-plug mr-2"></i>API 配置</h2><div><label for="api-type-select" class="block text-sm font-medium mb-1">API 类型</label><select id="api-type-select" class="input-base"><option value="openai">OpenAI</option><option value="gemini">Gemini</option></select></div><div><label for="api-url-input" class="block text-sm font-medium mb-1">API 地址</label><input type="url" id="api-url-input" class="input-base" placeholder="https://api.openai.com/v1/chat/completions"></div><div><label class="block text-sm font-medium mb-1">API Key 配置</label><div id="api-key-display" class="hidden p-3 border rounded-md bg-slate-100 dark:bg-slate-900/50 dark:border-slate-700"><div class="flex justify-between items-center"><ul id="masked-keys-list" class="space-y-1 text-sm font-mono text-light-subtext dark:text-dark-subtext"></ul><button id="edit-api-keys-btn" class="text-sm text-primary hover:underline">编辑</button></div></div><textarea id="api-key-input" rows="3" class="input-base font-mono text-sm" placeholder="可输入多个Key,用逗号或换行分隔..."></textarea><p class="mt-1 text-xs text-amber-600 dark:text-amber-500"><i class="fa fa-warning mr-1"></i>注意:API密钥将经过混淆后保存在您的浏览器本地。</p></div><button type="button" id="connect-api-btn" class="btn-primary w-full flex items-center justify-center"><i class="fa fa-link mr-2"></i>连接模型</button><div id="api-status" class="hidden text-sm py-2 px-3 rounded-md text-center"></div></div><div class="space-y-4 pt-6 border-t dark:border-slate-700"><h2 class="text-lg font-semibold -mb-2 flex items-center text-sky-600 dark:text-sky-500"><i class="fa fa-sliders mr-2"></i>生成参数</h2><div><label for="model-select" class="block text-sm font-medium mb-1">选择模型</label><select id="model-select" class="input-base" disabled><option value="">请先连接API</option></select></div><div><label for="context-memory-slider" class="block text-sm font-medium mb-1">上下文记忆: <span id="context-memory-value">3</span> 章</label><input type="range" id="context-memory-slider" min="0" max="20" value="3" class="w-full"></div><div><label for="temperature-slider" class="block text-sm font-medium mb-1">创意温度: <span id="temperature-value">0.7</span></label><input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.1" value="0.7" class="w-full"></div></div></div></div></div>
128
- <div id="story-preset-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col dark:bg-dark-card"><div class="p-5 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">故事预设</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-6 flex-grow flex flex-col overflow-hidden"><div class="flex-shrink-0 mb-4 flex justify-between items-center"><div id="preset-tabs-container" class="flex space-x-1 border border-slate-200 dark:border-slate-700 rounded-lg p-1"></div><button id="add-preset-btn" class="btn-secondary flex items-center"><i class="fa fa-plus mr-2"></i>新增</button></div><div id="preset-list-container" class="flex-grow overflow-y-auto space-y-2 pr-2"></div><div class="flex-shrink-0 mt-6 pt-4 border-t dark:border-slate-700 flex justify-center"><button id="generate-from-presets-btn" class="btn-primary flex items-center"><i class="fa fa-magic mr-2"></i>根据选择生成故事</button></div></div></div></div>
129
 
130
  <script src="https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js"></script>
131
  <script>
@@ -135,7 +135,6 @@
135
  let story = [], currentChapterIndex = -1, chapterPages = [], currentPageIndex = 0;
136
  let apiConfig = { type: 'openai', url: '', keys: [], currentKeyIndex: 0, isConnected: false, models: [] };
137
  let presetData = {}, selectedPresets = {}, activePresetTab = 'character';
138
- // NEW: Icons added to categories
139
  const PRESET_CATEGORIES = {
140
  character: { name: '角色', icon: 'fa-user-circle' },
141
  player: { name: '玩家', icon: 'fa-gamepad' },
@@ -197,15 +196,9 @@
197
  const highlightDialogue = (text) => text.replace(/(“[^”]+”|‘[^’]+’|"[^"]+"|\'[^\']+\'|「[^」]+」|『[^』]+』)/g, `<span class="dialogue-highlight">$1</span>`);
198
  const paginateContent = (htmlContent, limit) => { const pages = []; const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; const nodes = Array.from(tempDiv.childNodes); if (nodes.length === 0) return [htmlContent]; let currentPageHtml = ''; for (const node of nodes) { if (currentPageHtml && (currentPageHtml.length + (node.textContent || '').length) > limit) { pages.push(currentPageHtml); currentPageHtml = ''; } currentPageHtml += node.outerHTML || node.textContent; } if (currentPageHtml) pages.push(currentPageHtml); return pages; };
199
  const downloadAsTxt = (filename, text) => { const blob = new Blob(['\uFEFF' + text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); };
200
- const renderPresetTabs = () => {
201
- const isMobile = window.innerWidth < 768;
202
- elements.presetTabsContainer.innerHTML = Object.entries(PRESET_CATEGORIES).map(([key, {name, icon}]) => {
203
- const content = isMobile ? `<i class="fa ${icon} fa-fw"></i>` : name;
204
- return `<button data-tab="${key}" title="${name}" class="tab-button px-3 py-1 text-sm rounded-md transition-colors ${activePresetTab === key ? 'active' : ''}">${content}</button>`
205
- }).join('');
206
- };
207
- const renderPresetList = () => { const list = presetData[activePresetTab] || []; elements.presetListContainer.innerHTML = list.length === 0 ? `<p class="text-center text-light-subtext py-4">此分类下无预设。</p>` : list.map(item => `<div class="p-3 rounded-lg flex items-center justify-between cursor-pointer transition-colors ${selectedPresets[activePresetTab]?.includes(item.id) ? 'bg-sky-100/50 dark:bg-sky-900/20' : 'bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600'}"><div class="flex items-center flex-grow" data-action="toggle-select" data-id="${item.id}"><input type="checkbox" class="h-4 w-4 rounded border-slate-300 text-primary pointer-events-none" ${selectedPresets[activePresetTab]?.includes(item.id) ? 'checked' : ''}><div class="ml-3"><p class="font-semibold text-light-text dark:text-dark-text">${item.name}</p><p class="text-xs text-light-subtext">${item.desc}</p></div></div><div class="flex space-x-1 ml-2"><button data-action="edit-preset" data-id="${item.id}" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"><i class="fa fa-edit text-sm"></i></button><button data-action="delete-preset" data-id="${item.id}" class="p-2 rounded-full hover:bg-red-100 dark:hover:bg-red-800/50"><i class="fa fa-trash text-sm text-red-500"></i></button></div></div>`).join(''); };
208
- const showPresetEditor = (itemToEdit = null) => { const isEditing = !!itemToEdit; const modalId = 'dynamic-preset-editor'; $(`#${modalId}`)?.remove(); const modal = document.createElement('div'); modal.id = modalId; modal.className = 'fixed inset-0 bg-black/60 z-[60] flex items-center justify-center p-4'; modal.innerHTML = `<div class="bg-light-card dark:bg-dark-card rounded-xl shadow-xl w-full max-w-md"><div class="p-5 border-b dark:border-slate-700"><h3 class='text-lg font-semibold text-light-text dark:text-dark-text'>${isEditing ? '编辑' : '新增'} ${PRESET_CATEGORIES[activePresetTab].name} 预设</h3></div><div class="p-5 space-y-4"><input id='preset-edit-name' class='input-base' placeholder='名称' value='${itemToEdit?.name || ''}'/><textarea id='preset-edit-desc' class='input-base h-24' placeholder='描述'>${itemToEdit?.desc || ''}</textarea></div><div class='p-5 flex justify-end space-x-2 border-t dark:border-slate-700'><button id='preset-edit-cancel' class='btn-secondary'>取消</button><button id='preset-edit-save' class='btn-primary'>保存</button></div></div>`; document.body.appendChild(modal); modal.querySelector('#preset-edit-cancel').onclick = () => modal.remove(); modal.querySelector('#preset-edit-save').onclick = () => { const name = modal.querySelector('#preset-edit-name').value.trim(); const desc = modal.querySelector('#preset-edit-desc').value.trim(); if (!name) return alert('名称不能为空。'); if (isEditing) { itemToEdit.name = name; itemToEdit.desc = desc; } else { presetData[activePresetTab].push({ id: `p_${Date.now()}`, name, desc }); } savePresets(); renderPresetList(); modal.remove(); }; };
209
  const generatePromptFromSelectedPresets = () => { let prompt = ""; Object.entries(selectedPresets).forEach(([category, ids]) => { if (ids.length === 0) return; prompt += `### ${PRESET_CATEGORIES[category].name}\n`; ids.forEach(id => { const item = presetData[category].find(p => p.id === id); if (item) prompt += `- ${item.name}: ${item.desc}\n`; }); prompt += '\n'; }); return prompt || "无预设,请自由发挥。"; };
210
 
211
  function initializeEventListeners() {
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>爱小说 (重构版)</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
9
  <script>
 
34
  }
35
  }
36
  @layer components {
37
+ .btn { @apply px-3 py-2 rounded-md font-semibold text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-dark-bg disabled:opacity-50 disabled:cursor-not-allowed; }
38
  .btn-primary { @apply btn bg-primary text-white hover:bg-primary-hover focus:ring-primary-focus; }
39
  .btn-secondary { @apply btn bg-slate-200 text-slate-700 hover:bg-slate-300 focus:ring-slate-400 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600 dark:focus:ring-slate-500; }
40
  .input-base { @apply w-full p-2 border border-slate-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-primary-focus focus:border-transparent dark:bg-slate-700 dark:border-slate-600 dark:text-dark-text; }
 
51
  <body class="bg-light-bg font-sans text-light-text min-h-screen flex flex-col dark:bg-dark-bg dark:text-dark-text">
52
 
53
  <header class="bg-light-card/80 dark:bg-dark-card/80 backdrop-blur-sm shadow-sm sticky top-0 z-50 border-b border-slate-200 dark:border-slate-800">
54
+ <div class="container mx-auto px-4 py-2 flex justify-between items-center">
55
+ <h1 class="text-xl font-bold text-sky-600 dark:text-sky-500 flex items-center"><i class="fa fa-book mr-2"></i>爱小说</h1>
56
  <div class="hidden md:flex items-center space-x-2">
57
  <button id="custom-prompt-btn" class="btn-secondary" title="自定义情节"><i class="fa fa-magic mr-2"></i>自定义</button>
58
  <button id="regenerate-chapter-btn" class="btn-secondary" title="重新生成"><i class="fa fa-refresh mr-2"></i>重写</button>
59
  <button id="continue-story-btn" class="btn-primary" title="继续故事"><i class="fa fa-play mr-2"></i>继续创作</button>
60
+ <span class="text-slate-300 dark:text-slate-600 mx-1">|</span>
61
  <button id="open-story-preset-btn" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700" title="故事设定"><i class="fa fa-file-text-o"></i></button>
62
  <button id="open-model-settings-btn" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700" title="模型设置"><i class="fa fa-cogs"></i></button>
63
  <button id="theme-toggle-btn" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"><i class="fa fa-moon-o"></i></button>
64
  </div>
65
  <div class="md:hidden">
66
+ <button id="continue-story-btn-mobile" class="btn-primary px-3" title="继续故事"><i class="fa fa-play"></i></button>
67
  <button id="mobile-menu-btn" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700 ml-2" title="更多操作"><i class="fa fa-ellipsis-v"></i></button>
68
  </div>
69
  </div>
 
78
  </div>
79
  </header>
80
 
81
+ <main class="flex-grow container mx-auto px-4 py-6 flex flex-col">
82
+ <div class="mb-4 flex justify-between items-center">
83
  <button id="prev-chapter-btn" class="btn-secondary"><i class="fa fa-arrow-left mr-2"></i>上章</button>
84
  <span id="chapter-info" class="text-sm text-light-subtext dark:text-dark-subtext">无内容</span>
85
  <button id="next-chapter-btn" class="btn-secondary">下章<i class="fa fa-arrow-right ml-2"></i></button>
86
  </div>
87
  <div class="bg-light-card rounded-xl shadow-lg flex-grow flex flex-col overflow-hidden border border-slate-200 dark:border-slate-700/50" style="background-color: var(--reading-bg-color);">
88
+ <div id="reading-area" class="p-6 md:p-8 lg:p-10 min-h-[500px] relative flex-grow overflow-y-auto scrollbar-hide" style="color: var(--reading-text-color); font-family: var(--reading-font); font-size: var(--reading-font-size); line-height: var(--reading-line-height); letter-spacing: var(--reading-letter-spacing);">
89
  <div id="loading-state" class="absolute inset-0 flex items-center justify-center bg-light-card/80 dark:bg-dark-card/80 z-10 hidden">
90
  <div class="text-center">
91
+ <div class="inline-block animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary mb-3"></div>
92
  <p class="text-light-subtext dark:text-dark-subtext">AI创作中...</p>
93
  </div>
94
  </div>
95
  <div id="content-container" class="hidden">
96
+ <h1 id="chapter-title-display" class="text-2xl md:text-3xl font-bold mb-6 text-center" style="color: var(--reading-text-color);"></h1>
97
  <div id="content-area" class="prose max-w-none"></div>
98
  </div>
99
+ <div id="welcome-message" class="text-center p-8 flex flex-col items-center justify-center h-full">
100
+ <i class="fa fa-magic text-5xl text-slate-300 dark:text-slate-600 mb-4"></i>
101
+ <h2 class="text-xl font-bold text-light-text dark:text-dark-text mb-2">欢迎使用 爱小说</h2>
102
+ <p class="text-sm text-light-subtext dark:text-dark-subtext max-w-md mx-auto">请先点击 <i class="fa fa-cogs"></i> 配置模型,然后通过 <i class="fa fa-file-text-o"></i> 设定故事,或直接 <i class="fa fa-play"></i> 开始创作。</p>
103
  </div>
104
  </div>
105
+ <div class="bg-light-card/80 border-t border-slate-200/80 p-2 flex justify-between items-center backdrop-blur-sm dark:bg-dark-card/80 dark:border-t-slate-700/50">
106
  <div class="flex items-center space-x-3 text-sm text-light-subtext dark:text-dark-subtext">
107
+ <div class="flex items-center pl-2"><i class="fa fa-file-word-o mr-2"></i><span class="hidden sm:inline">字数: </span><span id="word-count">0</span></div>
108
  <div id="pagination-controls" class="flex items-center space-x-1 hidden">
109
  <button id="prev-page-btn" class="p-1 rounded-md hover:bg-slate-200 dark:hover:bg-slate-700 disabled:opacity-50" title="上一页"><i class="fa fa-chevron-left text-xs"></i></button>
110
  <span id="page-info" class="text-xs px-1"></span>
 
121
  </div>
122
  </main>
123
 
124
+ <div id="reading-settings-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-md max-h-[90vh] flex flex-col dark:bg-dark-card"><div class="p-4 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">阅读设置</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700 -mr-2"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-5 space-y-5 overflow-y-auto"><div><h4 class="text-md font-medium mb-3 text-light-text dark:text-dark-text"><i class="fa fa-font mr-2 text-primary"></i>字体样式</h4><div class="space-y-4"><div><label for="font-size-slider" class="block text-sm font-medium mb-1 text-light-subtext dark:text-dark-subtext">字体大小: <span id="font-size-value">18px</span></label><input type="range" id="font-size-slider" min="12" max="32" step="1" value="18" class="w-full"></div><div><label for="line-height-slider" class="block text-sm font-medium mb-1 text-light-subtext dark:text-dark-subtext">行间距: <span id="line-height-value">1.75</span></label><input type="range" id="line-height-slider" min="1.2" max="2.5" step="0.05" value="1.75" class="w-full"></div></div></div><div><h4 class="text-md font-medium mb-3 text-light-text dark:text-dark-text"><i class="fa fa-upload mr-2 text-primary"></i>自定义字体</h4><input type="file" id="font-upload-input" accept=".ttf,.otf,.woff,.woff2" class="block w-full text-sm text-light-subtext file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-sky-100 file:text-primary hover:file:bg-sky-200 cursor-pointer dark:file:bg-sky-900/50 dark:hover:file:bg-sky-900"></div><div><h4 class="text-md font-medium mb-3 text-light-text dark:text-dark-text"><i class="fa fa-comments-o mr-2 text-primary"></i>对话高亮</h4><div class="space-y-3"><label class="flex items-center space-x-3 cursor-pointer"><input type="checkbox" id="dialogue-bold-toggle" class="h-4 w-4 rounded border-slate-300 text-primary focus:ring-primary dark:bg-slate-700 dark:border-slate-600"><span class="text-sm font-medium">对话内容加粗</span></label><div class="flex items-center space-x-3"><label for="dialogue-color-picker" class="text-sm font-medium text-light-subtext dark:text-dark-subtext">对话文字颜色:</label><input type="color" id="dialogue-color-picker" value="#334155" class="w-10 h-8 p-1 border rounded-md cursor-pointer dark:bg-slate-700 dark:border-slate-600"><button id="reset-dialogue-color-btn" class="text-xs text-light-subtext hover:text-primary">重置</button></div></div></div></div></div></div>
125
+ <div id="custom-prompt-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-lg dark:bg-dark-card"><div class="p-4 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">自定义生成</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700 -mr-2"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-5"><textarea id="custom-prompt-input" class="input-base h-40" placeholder="请输入你希望接下来发生的情节..."></textarea><div class="mt-4 flex justify-end"><button id="submit-custom-prompt-btn" class="btn-primary">生成</button></div></div></div></div>
126
+ <div id="edit-chapter-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col dark:bg-dark-card"><div class="p-4 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">编辑章节</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700 -mr-2"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-5 flex-grow flex flex-col"><input type="text" id="edit-chapter-title-input" class="input-base mb-4" placeholder="章节标题"><textarea id="edit-chapter-content-input" class="input-base flex-grow w-full resize-none" style="min-height: 300px;"></textarea><div class="mt-4 flex justify-between items-center"><button id="delete-chapter-btn" class="btn bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 flex items-center"><i class="fa fa-trash mr-2"></i>删除章节</button><div><button id="save-edit-btn" class="btn-primary">保存</button></div></div></div></div></div>
127
+ <div id="model-settings-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col dark:bg-dark-card"><div class="p-4 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">模型设置</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700 -mr-2"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-5 space-y-5 overflow-y-auto"><div class="space-y-4"><h2 class="text-lg font-semibold flex items-center text-sky-600 dark:text-sky-500 -mb-2"><i class="fa fa-plug mr-2"></i>API 配置</h2><div><label for="api-type-select" class="block text-sm font-medium mb-1">API 类型</label><select id="api-type-select" class="input-base"><option value="openai">OpenAI</option><option value="gemini">Gemini</option></select></div><div><label for="api-url-input" class="block text-sm font-medium mb-1">API 地址</label><input type="url" id="api-url-input" class="input-base" placeholder="https://api.openai.com/v1/chat/completions"></div><div><label class="block text-sm font-medium mb-1">API Key 配置</label><div id="api-key-display" class="hidden p-3 border rounded-md bg-slate-100 dark:bg-slate-900/50 dark:border-slate-700"><div class="flex justify-between items-center"><ul id="masked-keys-list" class="space-y-1 text-sm font-mono text-light-subtext dark:text-dark-subtext"></ul><button id="edit-api-keys-btn" class="text-sm text-primary hover:underline">编辑</button></div></div><textarea id="api-key-input" rows="3" class="input-base font-mono text-sm" placeholder="可输入多个Key,用逗号或换行分隔..."></textarea><p class="mt-1 text-xs text-amber-600 dark:text-amber-500"><i class="fa fa-warning mr-1"></i>注意:API密钥将经过混淆后保存在您的浏览器本地。</p></div><button type="button" id="connect-api-btn" class="btn-primary w-full flex items-center justify-center"><i class="fa fa-link mr-2"></i>连接模型</button><div id="api-status" class="hidden text-sm py-2 px-3 rounded-md text-center"></div></div><div class="space-y-4 pt-5 border-t dark:border-slate-700"><h2 class="text-lg font-semibold -mb-2 flex items-center text-sky-600 dark:text-sky-500"><i class="fa fa-sliders mr-2"></i>生成参数</h2><div><label for="model-select" class="block text-sm font-medium mb-1">选择模型</label><select id="model-select" class="input-base" disabled><option value="">请先连接API</option></select></div><div><label for="context-memory-slider" class="block text-sm font-medium mb-1">上下文记忆: <span id="context-memory-value">3</span> 章</label><input type="range" id="context-memory-slider" min="0" max="20" value="3" class="w-full"></div><div><label for="temperature-slider" class="block text-sm font-medium mb-1">创意温度: <span id="temperature-value">0.7</span></label><input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.1" value="0.7" class="w-full"></div></div></div></div></div>
128
+ <div id="story-preset-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden p-4"><div class="bg-light-card rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col dark:bg-dark-card"><div class="p-4 border-b flex justify-between items-center dark:border-slate-700"><h3 class="text-lg font-semibold text-sky-600 dark:text-sky-500">故事预设</h3><button class="close-modal-btn p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700 -mr-2"><i class="fa fa-times text-light-subtext"></i></button></div><div class="p-5 flex-grow flex flex-col overflow-hidden"><div class="flex-shrink-0 mb-4 flex justify-between items-center"><div id="preset-tabs-container" class="flex space-x-0 border border-slate-200 dark:border-slate-700 rounded-lg p-1"></div><button id="add-preset-btn" class="btn-secondary flex items-center"><i class="fa fa-plus mr-2"></i>新增</button></div><div id="preset-list-container" class="flex-grow overflow-y-auto space-y-2 pr-2"></div><div class="flex-shrink-0 mt-4 pt-4 border-t dark:border-slate-700 flex justify-center"><button id="generate-from-presets-btn" class="btn-primary flex items-center"><i class="fa fa-magic mr-2"></i>根据选择生成故事</button></div></div></div></div>
129
 
130
  <script src="https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js"></script>
131
  <script>
 
135
  let story = [], currentChapterIndex = -1, chapterPages = [], currentPageIndex = 0;
136
  let apiConfig = { type: 'openai', url: '', keys: [], currentKeyIndex: 0, isConnected: false, models: [] };
137
  let presetData = {}, selectedPresets = {}, activePresetTab = 'character';
 
138
  const PRESET_CATEGORIES = {
139
  character: { name: '角色', icon: 'fa-user-circle' },
140
  player: { name: '玩家', icon: 'fa-gamepad' },
 
196
  const highlightDialogue = (text) => text.replace(/(“[^”]+”|‘[^’]+’|"[^"]+"|\'[^\']+\'|「[^」]+」|『[^』]+』)/g, `<span class="dialogue-highlight">$1</span>`);
197
  const paginateContent = (htmlContent, limit) => { const pages = []; const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; const nodes = Array.from(tempDiv.childNodes); if (nodes.length === 0) return [htmlContent]; let currentPageHtml = ''; for (const node of nodes) { if (currentPageHtml && (currentPageHtml.length + (node.textContent || '').length) > limit) { pages.push(currentPageHtml); currentPageHtml = ''; } currentPageHtml += node.outerHTML || node.textContent; } if (currentPageHtml) pages.push(currentPageHtml); return pages; };
198
  const downloadAsTxt = (filename, text) => { const blob = new Blob(['\uFEFF' + text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); };
199
+ const renderPresetTabs = () => { const isMobile = window.innerWidth < 768; elements.presetTabsContainer.innerHTML = Object.entries(PRESET_CATEGORIES).map(([key, {name, icon}]) => { const content = isMobile ? `<i class="fa ${icon} fa-fw"></i>` : name; return `<button data-tab="${key}" title="${name}" class="tab-button px-3 py-1.5 text-sm rounded-md transition-colors ${activePresetTab === key ? 'active' : ''}">${content}</button>`}).join('');};
200
+ const renderPresetList = () => { const list = presetData[activePresetTab] || []; elements.presetListContainer.innerHTML = list.length === 0 ? `<p class="text-center text-light-subtext py-4">暂无 ${PRESET_CATEGORIES[activePresetTab].name} 预设</p>` : list.map(item => `<div class="p-3 rounded-lg flex items-center justify-between cursor-pointer transition-colors ${selectedPresets[activePresetTab]?.includes(item.id) ? 'bg-sky-100/50 dark:bg-sky-900/20' : 'bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600'}"><div class="flex items-center flex-grow" data-action="toggle-select" data-id="${item.id}"><input type="checkbox" class="h-4 w-4 rounded border-slate-300 text-primary pointer-events-none" ${selectedPresets[activePresetTab]?.includes(item.id) ? 'checked' : ''}><div class="ml-3"><p class="font-semibold text-light-text dark:text-dark-text">${item.name}</p><p class="text-xs text-light-subtext">${item.desc}</p></div></div><div class="flex space-x-1 ml-2"><button data-action="edit-preset" data-id="${item.id}" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700"><i class="fa fa-edit text-sm"></i></button><button data-action="delete-preset" data-id="${item.id}" class="p-2 rounded-full hover:bg-red-100 dark:hover:bg-red-800/50"><i class="fa fa-trash text-sm text-red-500"></i></button></div></div>`).join(''); };
201
+ const showPresetEditor = (itemToEdit = null) => { const isEditing = !!itemToEdit; const modalId = 'dynamic-preset-editor'; $(`#${modalId}`)?.remove(); const modal = document.createElement('div'); modal.id = modalId; modal.className = 'fixed inset-0 bg-black/60 z-[60] flex items-center justify-center p-4'; modal.innerHTML = `<div class="bg-light-card dark:bg-dark-card rounded-xl shadow-xl w-full max-w-md"><div class="p-4 border-b dark:border-slate-700"><h3 class='text-lg font-semibold text-light-text dark:text-dark-text'>${isEditing ? '编辑' : '新增'} ${PRESET_CATEGORIES[activePresetTab].name} 预设</h3></div><div class="p-5 space-y-4"><input id='preset-edit-name' class='input-base' placeholder='名称' value='${itemToEdit?.name || ''}'/><textarea id='preset-edit-desc' class='input-base h-24' placeholder='描述'>${itemToEdit?.desc || ''}</textarea></div><div class='p-4 flex justify-end space-x-2 border-t dark:border-slate-700'><button id='preset-edit-cancel' class='btn-secondary'>取消</button><button id='preset-edit-save' class='btn-primary'>保存</button></div></div>`; document.body.appendChild(modal); modal.querySelector('#preset-edit-cancel').onclick = () => modal.remove(); modal.querySelector('#preset-edit-save').onclick = () => { const name = modal.querySelector('#preset-edit-name').value.trim(); const desc = modal.querySelector('#preset-edit-desc').value.trim(); if (!name) return alert('名称不能为空。'); if (isEditing) { itemToEdit.name = name; itemToEdit.desc = desc; } else { presetData[activePresetTab].push({ id: `p_${Date.now()}`, name, desc }); } savePresets(); renderPresetList(); modal.remove(); }; };
 
 
 
 
 
 
202
  const generatePromptFromSelectedPresets = () => { let prompt = ""; Object.entries(selectedPresets).forEach(([category, ids]) => { if (ids.length === 0) return; prompt += `### ${PRESET_CATEGORIES[category].name}\n`; ids.forEach(id => { const item = presetData[category].find(p => p.id === id); if (item) prompt += `- ${item.name}: ${item.desc}\n`; }); prompt += '\n'; }); return prompt || "无预设,请自由发挥。"; };
203
 
204
  function initializeEventListeners() {