Lashtw commited on
Commit
07df01b
·
verified ·
1 Parent(s): 123f479

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +325 -19
index.html CHANGED
@@ -1,19 +1,325 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-Hant">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>博客來書單抓取工具</title>
7
+ <!-- 載入 Tailwind CSS -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ /* 使用 Inter 字體 */
11
+ body {
12
+ font-family: 'Inter', sans-serif;
13
+ }
14
+ /* textarea 有基本的樣式 */
15
+ textarea {
16
+ font-family: monospace;
17
+ }
18
+ </style>
19
+ </head>
20
+ <body class="bg-gray-100 min-h-screen flex items-center justify-center p-4">
21
+ <div class="bg-white p-8 rounded-lg shadow-xl w-full max-w-4xl">
22
+ <h1 class="text-3xl font-bold text-center text-blue-600 mb-6">博客來書單抓取工具</h1>
23
+
24
+ <!-- 步驟說明 -->
25
+ <div class="mb-6 bg-blue-50 border border-blue-200 p-4 rounded-lg text-blue-700">
26
+ <h2 class="font-bold text-lg mb-2">如何使用:</h2>
27
+ <ol class="list-decimal list-inside space-y-1">
28
+ <li>在瀏覽器中打開您要抓取的博客來書籍頁面。</li>
29
+ <li>在頁面空白處按右鍵,選擇「<b>檢視網頁原始碼</b>」(View Page Source)。</li>
30
+ <li>複製「所有」的網頁原始碼 (通常是 Ctrl+A, Ctrl+C)。</li>
31
+ <li>將原始碼貼到下方的「網頁原始碼」輸入框中。</li>
32
+ <li>點擊「<b>解析並加入清單</b>」按鈕。</li>
33
+ </ol>
34
+ </div>
35
+
36
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
37
+ <!-- 左側:輸入區域 -->
38
+ <div>
39
+ <label for="sourceHtml" class="block text-sm font-medium text-gray-700 mb-2">
40
+ 1. 貼入網頁原始碼 (HTML):
41
+ </label>
42
+ <textarea id="sourceHtml" rows="15" class="w-full p-3 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" placeholder="請在此貼上博客來網頁的完整原始碼..."></textarea>
43
+ <div class="mt-4 flex space-x-2">
44
+ <button id="parseButton" class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 font-semibold shadow-md transition-colors">
45
+ 解析並加入清單
46
+ </button>
47
+ <button id="clearSourceButton" class="w-1/3 bg-gray-300 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-400 font-semibold shadow-md transition-colors">
48
+ 清除
49
+ </button>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- 右側:結果區域 -->
54
+ <div>
55
+ <label for="resultList" class="block text-sm font-medium text-gray-700 mb-2">
56
+ 2. 您的購書清單 (CSV 格式):
57
+ </label>
58
+ <textarea id="resultList" rows="15" class="w-full p-3 border border-gray-300 rounded-md shadow-sm bg-gray-50" readonly placeholder="解析結果將會顯示在這裡..."></textarea>
59
+ <div class="mt-4 flex space-x-2">
60
+ <button id="copyButton" class="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 font-semibold shadow-md transition-colors">
61
+ 複製清單
62
+ </button>
63
+ <!-- *** 新增匯出按鈕 *** -->
64
+ <button id="exportCsvButton" class="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 font-semibold shadow-md transition-colors">
65
+ 匯出 CSV
66
+ </button>
67
+ <button id="clearListButton" class="w-1/3 bg-red-500 text-white py-2 px-4 rounded-md hover:bg-red-600 font-semibold shadow-md transition-colors">
68
+ 清空
69
+ </button>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- 訊息提示框 -->
75
+ <div id="messageBox" class="hidden fixed top-5 right-5 p-4 rounded-md shadow-lg transition-all">
76
+ <span id="messageText"></span>
77
+ </div>
78
+
79
+ </div>
80
+
81
+ <script>
82
+ const sourceHtmlEl = document.getElementById('sourceHtml');
83
+ const parseButtonEl = document.getElementById('parseButton');
84
+ const clearSourceButtonEl = document.getElementById('clearSourceButton');
85
+ const resultListEl = document.getElementById('resultList');
86
+ const copyButtonEl = document.getElementById('copyButton');
87
+ <!-- *** 取得新按鈕 *** -->
88
+ const exportCsvButtonEl = document.getElementById('exportCsvButton');
89
+ const clearListButtonEl = document.getElementById('clearListButton');
90
+ const messageBoxEl = document.getElementById('messageBox');
91
+ const messageTextEl = document.getElementById('messageText');
92
+
93
+ // 顯示提示訊息
94
+ function showMessage(text, type = 'success') {
95
+ messageTextEl.textContent = text;
96
+ if (type === 'success') {
97
+ messageBoxEl.className = 'fixed top-5 right-5 p-4 rounded-md shadow-lg bg-green-500 text-white transition-all';
98
+ } else {
99
+ messageBoxEl.className = 'fixed top-5 right-5 p-4 rounded-md shadow-lg bg-red-500 text-white transition-all';
100
+ }
101
+ messageBoxEl.classList.remove('hidden');
102
+ setTimeout(() => {
103
+ messageBoxEl.classList.add('hidden');
104
+ }, 3000);
105
+ }
106
+
107
+ // 解析 HTML 並抓取資料
108
+ function parseHtml() {
109
+ const htmlString = sourceHtmlEl.value;
110
+ if (!htmlString) {
111
+ showMessage('原始碼為空!', 'error');
112
+ return;
113
+ }
114
+
115
+ try {
116
+ const parser = new DOMParser();
117
+ const doc = parser.parseFromString(htmlString, 'text/html');
118
+
119
+ // *** 新增:優先解析 meta description ***
120
+ const metaDescContent = doc.querySelector('meta[name="description"]')?.getAttribute('content') || '';
121
+ const metaData = {};
122
+
123
+ if (metaDescContent) {
124
+ // 使用全形逗號 ',' 分割
125
+ const parts = metaDescContent.split(',');
126
+ parts.forEach(part => {
127
+ // 使用全形冒號 ':' 分割
128
+ const pieces = part.split(':');
129
+ if (pieces.length === 2) {
130
+ const key = pieces[0].trim(); // e.g., "作者"
131
+ const value = pieces[1].trim(); // e.g., "款款"
132
+ metaData[key] = value;
133
+ }
134
+ });
135
+ }
136
+ // *** meta description 解析完畢 ***
137
+
138
+ // 1. 書名 (og:title 優先,metaData['書名'] 為備案)
139
+ const title = doc.querySelector('meta[property="og:title"]')?.getAttribute('content') || metaData['書名'] || '未找到書名';
140
+
141
+ // 輔助函數:從 li 列表中尋找特定開頭的項目
142
+ const allListItems = Array.from(doc.querySelectorAll('li'));
143
+
144
+ const findLiText = (prefix) => {
145
+ const li = allListItems.find(item => item.textContent.trim().startsWith(prefix));
146
+ return li ? li : null;
147
+ };
148
+
149
+ // 2. 作者/譯者
150
+ // *** 修正:優先從 metaData 抓取 ***
151
+ let author = metaData['作者'] || '';
152
+ let translator = metaData['譯者'] || '';
153
+
154
+ // 如果 metaData 找不到作者,才使用 li 尋找
155
+ if (!author) {
156
+ const authorLi = findLiText('作者:');
157
+ // *** 修正:改用更穩健的方式抓取作者 ***
158
+ if (authorLi) {
159
+ const authorLinks = Array.from(authorLi.querySelectorAll('a'));
160
+ if (authorLinks.length > 0) {
161
+ // 方案 A:抓取所有 <a> 標籤的文字,用 / 分隔
162
+ author = authorLinks.map(a => a.textContent.trim()).join('/');
163
+ } else {
164
+ // 方案 B:如果沒有 <a>,直接取 li 的文字並移除前綴
165
+ author = authorLi.textContent.replace('作者:', '').trim();
166
+ }
167
+ }
168
+ }
169
+
170
+ // 如果 metaData 找不到譯者,才使用 li 尋找
171
+ if (!translator) {
172
+ const translatorLi = findLiText('譯者:');
173
+ // *** 修正:改用更穩健的方式抓取譯者 ***
174
+ if (translatorLi) {
175
+ const translatorLinks = Array.from(translatorLi.querySelectorAll('a'));
176
+ if (translatorLinks.length > 0) {
177
+ translator = translatorLinks.map(a => a.textContent.trim()).join('/');
178
+ } else {
179
+ translator = translatorLi.textContent.replace('譯者:', '').trim();
180
+ }
181
+ }
182
+ }
183
+
184
+ // *** 修正點:將 const 改為 let ***
185
+ let authorTranslator = [author, translator].filter(Boolean).join('/');
186
+ if (!authorTranslator) {
187
+ authorTranslator = '未找到作者';
188
+ }
189
+
190
+ // 3. 出版社
191
+ // *** 修正:優先從 metaData 抓取 ***
192
+ let publisher = metaData['出版社'] || '';
193
+ if (!publisher) {
194
+ const publisherLi = findLiText('出版社:');
195
+ publisher = publisherLi?.querySelector('a')?.textContent.trim() || '未找到出版社';
196
+ }
197
+
198
+ // 4. ISBN
199
+ // *** 修正:優先從 metaData 抓取 ***
200
+ let isbn = metaData['ISBN'] || '';
201
+ if (!isbn) {
202
+ const isbnLi = findLiText('ISBN:');
203
+ isbn = isbnLi ? isbnLi.textContent.replace('ISBN:', '').trim() : '未找到ISBN';
204
+ }
205
+
206
+ // 5. 數量
207
+ const quantity = 1;
208
+
209
+ // 6. 定價 (*** 修正:改為優先抓取 "定價",而不是優惠價 ***)
210
+ let price = '未找到價格';
211
+ const priceLi = findLiText('定價:');
212
+
213
+ if (priceLi) {
214
+ // 抓取 li 內的文字,例如 "定價:$450" 或 "定價:450元"
215
+ price = priceLi.textContent
216
+ .replace('定價:', '') // 移除 "定價:"
217
+ .replace('$', '') // 移除 $ 符號
218
+ .replace('元', '') // 移除 "元"
219
+ .trim(); // 移除空白
220
+ } else {
221
+ // 備用方案 (如果找不到 "定價:" li):嘗試抓取 meta tag (可能是優惠價)
222
+ const metaPrice = doc.querySelector('meta[property="product:price:amount"]')?.getAttribute('content');
223
+ if (metaPrice) {
224
+ price = metaPrice;
225
+ }
226
+ // (移除了 .price_e strong b 的備用方案,因為那確定是優惠價)
227
+ }
228
+
229
+ // *** 修正點:將 ISBN 格式化以防止 Excel 轉為科學記號 ***
230
+ // 透過 `="<ISBN>"` 格式強制 Excel 將其視為文字
231
+ const excelSafeIsbn = `="${isbn}"`;
232
+
233
+ // 組合 CSV 字串
234
+ const csvRow = [title, authorTranslator, publisher, excelSafeIsbn, quantity, price].join(',');
235
+
236
+ // 加入到結果列表
237
+ const currentList = resultListEl.value;
238
+ resultListEl.value = currentList ? (currentList + '\n' + csvRow) : csvRow;
239
+
240
+ // 清空來源框
241
+ sourceHtmlEl.value = '';
242
+ showMessage('成功解析並加入!', 'success');
243
+
244
+ } catch (error) {
245
+ console.error('解析失敗:', error);
246
+ showMessage('解析失敗,請確認貼上完整的 HTML 原始碼。', 'error');
247
+ }
248
+ }
249
+
250
+ // 複製到剪貼簿
251
+ function copyToClipboard() {
252
+ const text = resultListEl.value;
253
+ if (!text) {
254
+ showMessage('清單是空的!', 'error');
255
+ return;
256
+ }
257
+
258
+ // 使用 document.execCommand (因為 navigator.clipboard 在 iframe 中可能受限)
259
+ const textArea = document.createElement('textarea');
260
+ textArea.value = text;
261
+ document.body.appendChild(textArea);
262
+ textArea.select();
263
+ try {
264
+ document.execCommand('copy');
265
+ showMessage('已複製到剪貼簿!', 'success');
266
+ } catch (err) {
267
+ showMessage('複製失敗。', 'error');
268
+ }
269
+ document.body.removeChild(textArea);
270
+ }
271
+
272
+ // *** 新增匯出 CSV 函式 ***
273
+ function exportAsCsv() {
274
+ const text = resultListEl.value;
275
+ if (!text) {
276
+ showMessage('清單是空的!', 'error');
277
+ return;
278
+ }
279
+
280
+ // 加上 CSV 標頭
281
+ const header = "書名,作者/譯者,出版社,ISBN,數量,定價\n";
282
+ // 加上 BOM (UFEFF) 確保 Excel 正確讀取 UTF-8 (尤其是中文)
283
+ const bom = "\ufeff";
284
+ const csvContent = bom + header + text;
285
+
286
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
287
+
288
+ // 產生下載連結
289
+ const link = document.createElement("a");
290
+ const url = URL.createObjectURL(blob);
291
+ link.setAttribute("href", url);
292
+
293
+ // 產生檔案名稱 (包含日期時間)
294
+ const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '_');
295
+ link.setAttribute("download", `book_list_${timestamp}.csv`);
296
+ link.style.visibility = 'hidden';
297
+
298
+ document.body.appendChild(link);
299
+ link.click();
300
+ document.body.removeChild(link);
301
+
302
+ showMessage('CSV 檔案已開始下載!', 'success');
303
+ }
304
+
305
+ // 綁定事件
306
+ parseButtonEl.addEventListener('click', parseHtml);
307
+
308
+ clearSourceButtonEl.addEventListener('click', () => {
309
+ sourceHtmlEl.value = '';
310
+ showMessage('已清除原始碼。', 'success');
311
+ });
312
+
313
+ copyButtonEl.addEventListener('click', copyToClipboard);
314
+
315
+ <!-- *** 綁定新按鈕事件 *** -->
316
+ exportCsvButtonEl.addEventListener('click', exportAsCsv);
317
+
318
+ clearListButtonEl.addEventListener('click', () => {
319
+ resultListEl.value = '';
320
+ showMessage('已清空列表。', 'success');
321
+ });
322
+
323
+ </script>
324
+ </body>
325
+ </html>