mingyang22 commited on
Commit
d8ddeb8
·
verified ·
1 Parent(s): e5e777c

refactor: use Asia/Shanghai timezone for note timestamps

Browse files
Files changed (1) hide show
  1. app.py +409 -404
app.py CHANGED
@@ -1,404 +1,409 @@
1
- import gradio as gr
2
- import os
3
- import json
4
- from huggingface_hub import HfApi, hf_hub_download
5
- from datetime import datetime
6
- import shutil
7
- from pathlib import Path
8
- from uuid import uuid4
9
-
10
- # --- 配置 (优先从环境变量读取) ---
11
- DATASET_REPO_ID = os.environ.get("DATASET_REPO_ID", "mingyang22/huggingface-notes")
12
- HF_TOKEN = os.environ.get("HF_TOKEN") # 必须在 Space 设置中配置
13
- REMOTE_NOTES_PATH = "db/notes.json"
14
-
15
- PWA_HEAD = """
16
- <meta name="theme-color" content="#3b82f6" />
17
- <style>
18
- /* 玻璃质感与高级深色主题 CSS */
19
- :root {
20
- --bg-dark: #0a0a0a;
21
- --accent: #3b82f6;
22
- --border: #333333;
23
- --text-main: #eeeeee;
24
- --text-dim: #888888;
25
- }
26
-
27
- body, .gradio-container {
28
- background-color: var(--bg-dark) !important;
29
- color: var(--text-main) !important;
30
- }
31
-
32
- /* 隐藏 Gradio 默认页脚 */
33
- footer { display: none !important; }
34
-
35
- /* 侧边栏按钮美化 */
36
- .nav-btn button {
37
- background: transparent !important;
38
- border: none !important;
39
- text-align: left !important;
40
- padding-left: 20px !important;
41
- font-size: 16px !important;
42
- color: var(--text-dim) !important;
43
- }
44
- .nav-btn button:hover {
45
- background: #1a1a1a !important;
46
- color: white !important;
47
- }
48
- .active-nav button {
49
- background: #252525 !important;
50
- color: white !important;
51
- border-left: 3px solid var(--accent) !important;
52
- }
53
-
54
- /* 列表美化 */
55
- .note-list-item {
56
- border-bottom: 1px solid #1a1a1a !important;
57
- padding: 15px !important;
58
- cursor: pointer;
59
- }
60
- .note-list-item:hover {
61
- background: #1a1a1a !important;
62
- }
63
-
64
- /* 编辑器美化 */
65
- #note_title textarea {
66
- font-size: 24px !important;
67
- font-weight: bold !important;
68
- background: transparent !important;
69
- border: none !important;
70
- color: #e0e0e0 !important;
71
- }
72
- #note_content textarea {
73
- font-size: 16px !important;
74
- background: transparent !important;
75
- border: none !important;
76
- color: #cccccc !important;
77
- }
78
-
79
- /* AI 按钮渐变 */
80
- #ai_btn {
81
- background: linear-gradient(135deg, #7c3aed 0%, #db2777 100%) !important;
82
- border: none !important;
83
- font-weight: bold !important;
84
- }
85
-
86
- /* 滚动条美化 */
87
- ::-webkit-scrollbar { width: 6px; }
88
- ::-webkit-scrollbar-track { background: transparent; }
89
- ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
90
- ::-webkit-scrollbar-thumb:hover { background: #444; }
91
- </style>
92
- """
93
-
94
- def get_default_data_dir():
95
- # 如果在 Space 环境,优先使用当前目录下的 cache_data 文件夹,避免 /root 权限问题
96
- if os.environ.get("SPACE_ID") or os.environ.get("HF_SPACE"):
97
- return str(Path.cwd() / "cache_data")
98
-
99
- custom_dir = os.environ.get("HF_NOTES_DATA_DIR")
100
- if custom_dir: return custom_dir
101
- if os.name == "nt":
102
- base = os.environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
103
- else:
104
- base = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
105
- return str(Path(base) / "hf-note-app-pro")
106
-
107
- DATA_DIR = get_default_data_dir()
108
- LOCAL_NOTES_PATH = str(Path(DATA_DIR) / "notes.json")
109
-
110
- def ensure_local_notes():
111
- Path(DATA_DIR).mkdir(parents=True, exist_ok=True)
112
- p = Path(LOCAL_NOTES_PATH)
113
- if not p.exists():
114
- p.write_text("[]", encoding="utf-8")
115
-
116
- def read_notes():
117
- ensure_local_notes()
118
- try:
119
- data = json.loads(Path(LOCAL_NOTES_PATH).read_text(encoding="utf-8-sig"))
120
- if isinstance(data, list):
121
- valid_notes = []
122
- for item in data:
123
- if not isinstance(item, dict): continue
124
- # 关键修复:同时支持 C# 风格 (Uppercase) 和 Python 风格 (Lowercase) 的键名
125
- n_id = item.get("Id") or item.get("id", "")
126
- n_title = item.get("Title") or item.get("title", "")
127
- n_content = item.get("Content") or item.get("content", "")
128
- n_updated = item.get("UpdatedAt") or item.get("updated_at", "")
129
- n_pinned = item.get("IsPinned") if "IsPinned" in item else item.get("is_pinned", False)
130
- n_deleted = item.get("IsDeleted") if "IsDeleted" in item else item.get("is_deleted", False)
131
-
132
- valid_notes.append({
133
- "id": str(n_id),
134
- "title": str(n_title),
135
- "content": str(n_content),
136
- "updated_at": str(n_updated),
137
- "is_pinned": bool(n_pinned),
138
- "is_deleted": bool(n_deleted)
139
- })
140
- return valid_notes
141
- except Exception as e:
142
- print(f"读取笔记失败: {e}")
143
- return []
144
-
145
- def write_notes(notes):
146
- ensure_local_notes()
147
- Path(LOCAL_NOTES_PATH).write_text(
148
- json.dumps(notes, ensure_ascii=False, indent=2),
149
- encoding="utf-8",
150
- )
151
-
152
- # --- 持久化管理 ---
153
- class CloudSync:
154
- def __init__(self):
155
- self.api = HfApi(token=HF_TOKEN)
156
-
157
- def pull(self):
158
- try:
159
- ensure_local_notes()
160
- print(f"��� 正在从 Dataset {DATASET_REPO_ID} 拉取 {REMOTE_NOTES_PATH}...")
161
- downloaded_path = hf_hub_download(
162
- repo_id=DATASET_REPO_ID,
163
- filename=REMOTE_NOTES_PATH,
164
- repo_type="dataset",
165
- token=HF_TOKEN,
166
- force_download=True,
167
- revision="main",
168
- )
169
- shutil.copy(downloaded_path, LOCAL_NOTES_PATH)
170
- return True, f"✅ 云端拉取同步完成"
171
- except Exception as e:
172
- msg = str(e)
173
- print(f"拉取失败详情: {msg}")
174
- # 如果是 401/404,通常是 Token 没设或权限问题
175
- if "401" in msg or "404" in msg:
176
- return False, f"⚠️ 拉取失败: 请检查 Space 的 HF_TOKEN 是否已正确配置 (Dataset 可能为私有)"
177
- return False, f"⚠️ 拉取失败: {msg}"
178
-
179
- def push(self):
180
- ensure_local_notes()
181
- if not os.path.exists(LOCAL_NOTES_PATH): return False, " 文件丢失"
182
- try:
183
- self.api.upload_file(
184
- path_or_fileobj=LOCAL_NOTES_PATH,
185
- path_in_repo=REMOTE_NOTES_PATH,
186
- repo_id=DATASET_REPO_ID,
187
- repo_type="dataset",
188
- commit_message=f"Web Update Pro at {datetime.now().strftime('%H:%M:%S')}"
189
- )
190
- return True, "✅ 已备份至云端"
191
- except Exception as e:
192
- return False, f"❌ 备份失败: {e}"
193
-
194
- sync_manager = CloudSync()
195
-
196
- # --- 业务逻辑 ---
197
- def load_notes_list(filter_type="all", search_query=""):
198
- notes = read_notes()
199
- query = search_query.lower() if search_query else ""
200
-
201
- filtered = []
202
- for n in notes:
203
- # Tab 过滤
204
- is_deleted = n.get("is_deleted", False)
205
- is_pinned = n.get("is_pinned", False)
206
-
207
- if filter_type == "trash":
208
- if not is_deleted: continue
209
- else:
210
- if is_deleted: continue
211
- if filter_type == "pinned" and not is_pinned: continue
212
-
213
- # 搜索过滤
214
- if query and query not in n["title"].lower() and query not in n["content"].lower():
215
- continue
216
-
217
- filtered.append(n)
218
-
219
- # 排序:置顶优先,时间倒序
220
- sorted_notes = sorted(filtered, key=lambda x: (x.get("is_pinned", False), x.get("updated_at", "")), reverse=True)
221
-
222
- return [
223
- [n["id"], f"{'📌 ' if n.get('is_pinned') else ''}{n['title'] or '未命名'}", n["updated_at"]]
224
- for n in sorted_notes
225
- ]
226
-
227
- def get_note_detail(note_id):
228
- if not note_id: return "", "", ""
229
- notes = read_notes()
230
- for n in notes:
231
- if n["id"] == note_id:
232
- return n["title"], n["content"], n["updated_at"]
233
- return "", "", ""
234
-
235
- def handle_save(note_id, title, content, push_cloud=False):
236
- if not title and not content:
237
- return "无内容可保存", load_notes_list(), note_id
238
-
239
- notes = read_notes()
240
- now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
241
-
242
- found = False
243
- for n in notes:
244
- if n["id"] == note_id:
245
- n["title"], n["content"], n["updated_at"] = title, content, now
246
- found = True
247
- break
248
-
249
- if not found:
250
- new_id = uuid4().hex
251
- new_note = {
252
- "id": new_id,
253
- "title": title or "新笔记",
254
- "content": content,
255
- "updated_at": now,
256
- "is_pinned": False,
257
- "is_deleted": False
258
- }
259
- notes.insert(0, new_note)
260
- note_id = new_id
261
-
262
- write_notes(notes)
263
-
264
- if push_cloud:
265
- _, msg = sync_manager.push()
266
- status = f"已保存并同步 | {msg}"
267
- else:
268
- status = "已自动保存到本地"
269
-
270
- return status, load_notes_list(), note_id
271
-
272
- def handle_delete(note_id, current_filter):
273
- if not note_id: return "未选择笔记", load_notes_list(current_filter), ""
274
- notes = read_notes()
275
- for n in notes:
276
- if n["id"] == note_id:
277
- if current_filter == "trash":
278
- notes.remove(n)
279
- else:
280
- n["is_deleted"] = True
281
- n["is_pinned"] = False
282
- break
283
- write_notes(notes)
284
- sync_manager.push()
285
- return "已移至回收站" if current_filter != "trash" else "已彻底删除", load_notes_list(current_filter), ""
286
-
287
- def handle_pin(note_id, current_filter):
288
- if not note_id: return load_notes_list(current_filter)
289
- notes = read_notes()
290
- for n in notes:
291
- if n["id"] == note_id:
292
- n["is_pinned"] = not n.get("is_pinned", False)
293
- break
294
- write_notes(notes)
295
- sync_manager.push()
296
- return load_notes_list(current_filter)
297
-
298
- # --- Gradio UI ---
299
- with gr.Blocks(theme=gr.themes.Default(), head=PWA_HEAD) as demo:
300
- current_filter_state = gr.State("all")
301
- selected_note_id = gr.State("")
302
-
303
- with gr.Row(equal_height=True):
304
- # 1. 导航栏 (ClassNote 风格)
305
- with gr.Column(scale=1, min_width=150):
306
- gr.HTML("<div style='font-size: 20px; font-weight: bold; margin-bottom: 30px; color: white;'>HF Note</div>")
307
- btn_all = gr.Button("全部笔记", variant="secondary", elem_classes=["nav-btn", "active-nav"])
308
- btn_pinned = gr.Button("已置顶", variant="secondary", elem_classes=["nav-btn"])
309
- btn_trash = gr.Button("回收站", variant="secondary", elem_classes=["nav-btn"])
310
-
311
- gr.Markdown("---")
312
- btn_new = gr.Button("➕ 新建笔记", variant="secondary")
313
- btn_sync_pull = gr.Button("🔄 同步云端", variant="secondary")
314
-
315
- # 2. 列表栏
316
- with gr.Column(scale=2, min_width=250):
317
- search_box = gr.Textbox(placeholder="搜索笔记...", show_label=False, elem_id="search_box")
318
- note_list = gr.Dataframe(
319
- headers=["ID", "标题", "时间"],
320
- datatype=["str", "str", "str"],
321
- col_count=(3, "fixed"),
322
- interactive=False,
323
- label=None,
324
- )
325
- status_text = gr.Markdown("就绪")
326
-
327
- # 3. 编辑器栏
328
- with gr.Column(scale=4):
329
- with gr.Row():
330
- btn_save = gr.Button("💾 保存", variant="primary", size="sm")
331
- btn_pin = gr.Button("📌 置顶", variant="secondary", size="sm")
332
- btn_del = gr.Button("🗑️ 删除", variant="stop", size="sm")
333
- btn_ai = gr.Button("AI 润色", variant="primary", size="sm", elem_id="ai_btn")
334
-
335
- edit_title = gr.Textbox(placeholder="无标题笔记", show_label=False, elem_id="note_title")
336
- edit_content = gr.TextArea(placeholder="暂无内容,开始输入...", show_label=False, lines=25, elem_id="note_content")
337
- edit_date = gr.Markdown("", elem_id="note_date")
338
-
339
- # --- 交互事件 ---
340
-
341
- def on_note_select(evt: gr.SelectData, filt):
342
- curr_list = load_notes_list(filt)
343
- if not curr_list or not evt.index or evt.index[0] >= len(curr_list):
344
- return "", "", "", ""
345
- note_id = curr_list[evt.index[0]][0]
346
- title, content, date = get_note_detail(note_id)
347
- return note_id, title, content, f"最后修改: {date}"
348
-
349
- def handle_autosave(note_id, title, content):
350
- return handle_save(note_id, title, content, push_cloud=False)
351
-
352
- def handle_manual_save(note_id, title, content):
353
- return handle_save(note_id, title, content, push_cloud=True)
354
-
355
- def switch_filter(filter_type, search_query):
356
- return (
357
- filter_type,
358
- load_notes_list(filter_type, search_query),
359
- "",
360
- "",
361
- "",
362
- "",
363
- f"已切换到:{filter_type}"
364
- )
365
-
366
- def switch_all(search_query):
367
- return switch_filter("all", search_query)
368
-
369
- def switch_pinned(search_query):
370
- return switch_filter("pinned", search_query)
371
-
372
- def switch_trash(search_query):
373
- return switch_filter("trash", search_query)
374
-
375
- note_list.select(on_note_select, [current_filter_state], [selected_note_id, edit_title, edit_content, edit_date])
376
-
377
- search_box.change(load_notes_list, [current_filter_state, search_box], [note_list])
378
-
379
- # 离开焦点时保存
380
- edit_title.blur(handle_autosave, [selected_note_id, edit_title, edit_content], [status_text, note_list, selected_note_id])
381
- edit_content.blur(handle_autosave, [selected_note_id, edit_title, edit_content], [status_text, note_list, selected_note_id])
382
- # 显式保存按钮(PWA/移动端更可靠)
383
- btn_save.click(handle_manual_save, [selected_note_id, edit_title, edit_content], [status_text, note_list, selected_note_id])
384
-
385
- btn_all.click(switch_all, [search_box], [current_filter_state, note_list, selected_note_id, edit_title, edit_content, edit_date, status_text])
386
- btn_pinned.click(switch_pinned, [search_box], [current_filter_state, note_list, selected_note_id, edit_title, edit_content, edit_date, status_text])
387
- btn_trash.click(switch_trash, [search_box], [current_filter_state, note_list, selected_note_id, edit_title, edit_content, edit_date, status_text])
388
-
389
- btn_new.click(lambda: ("", "新笔记", "", ""), None, [selected_note_id, edit_title, edit_content, edit_date])
390
- btn_pin.click(handle_pin, [selected_note_id, current_filter_state], [note_list])
391
- btn_del.click(handle_delete, [selected_note_id, current_filter_state], [status_text, note_list, selected_note_id])
392
-
393
- btn_sync_pull.click(lambda: (sync_manager.pull()[1], load_notes_list()), None, [status_text, note_list])
394
-
395
- def ai_polish(content):
396
- if not content: return content
397
- return f"✨ [AI 润色已模拟完成]\n\n{content}\n\n(请在本地动作中使用完整的 DeepSeek 润色服务)"
398
- btn_ai.click(ai_polish, [edit_content], [edit_content])
399
-
400
- # 启动拉取
401
- demo.load(lambda: (sync_manager.pull()[1], load_notes_list()), None, [status_text, note_list])
402
-
403
- if __name__ == "__main__":
404
- demo.launch()
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import json
4
+ from huggingface_hub import HfApi, hf_hub_download
5
+ from datetime import datetime
6
+ import shutil
7
+ from pathlib import Path
8
+ from uuid import uuid4
9
+ from zoneinfo import ZoneInfo
10
+
11
+ # --- 配置 (优先从环境变量读取) ---
12
+ DATASET_REPO_ID = os.environ.get("DATASET_REPO_ID", "mingyang22/huggingface-notes")
13
+ HF_TOKEN = os.environ.get("HF_TOKEN") # 必须在 Space 设置中配置
14
+ REMOTE_NOTES_PATH = "db/notes.json"
15
+ BEIJING_TZ = ZoneInfo("Asia/Shanghai")
16
+
17
+ PWA_HEAD = """
18
+ <meta name="theme-color" content="#3b82f6" />
19
+ <style>
20
+ /* 玻璃质感与高级深色主题 CSS */
21
+ :root {
22
+ --bg-dark: #0a0a0a;
23
+ --accent: #3b82f6;
24
+ --border: #333333;
25
+ --text-main: #eeeeee;
26
+ --text-dim: #888888;
27
+ }
28
+
29
+ body, .gradio-container {
30
+ background-color: var(--bg-dark) !important;
31
+ color: var(--text-main) !important;
32
+ }
33
+
34
+ /* 隐藏 Gradio 默认页脚 */
35
+ footer { display: none !important; }
36
+
37
+ /* 侧边栏按钮美化 */
38
+ .nav-btn button {
39
+ background: transparent !important;
40
+ border: none !important;
41
+ text-align: left !important;
42
+ padding-left: 20px !important;
43
+ font-size: 16px !important;
44
+ color: var(--text-dim) !important;
45
+ }
46
+ .nav-btn button:hover {
47
+ background: #1a1a1a !important;
48
+ color: white !important;
49
+ }
50
+ .active-nav button {
51
+ background: #252525 !important;
52
+ color: white !important;
53
+ border-left: 3px solid var(--accent) !important;
54
+ }
55
+
56
+ /* 列表美化 */
57
+ .note-list-item {
58
+ border-bottom: 1px solid #1a1a1a !important;
59
+ padding: 15px !important;
60
+ cursor: pointer;
61
+ }
62
+ .note-list-item:hover {
63
+ background: #1a1a1a !important;
64
+ }
65
+
66
+ /* 编辑器美化 */
67
+ #note_title textarea {
68
+ font-size: 24px !important;
69
+ font-weight: bold !important;
70
+ background: transparent !important;
71
+ border: none !important;
72
+ color: #e0e0e0 !important;
73
+ }
74
+ #note_content textarea {
75
+ font-size: 16px !important;
76
+ background: transparent !important;
77
+ border: none !important;
78
+ color: #cccccc !important;
79
+ }
80
+
81
+ /* AI 按钮渐变 */
82
+ #ai_btn {
83
+ background: linear-gradient(135deg, #7c3aed 0%, #db2777 100%) !important;
84
+ border: none !important;
85
+ font-weight: bold !important;
86
+ }
87
+
88
+ /* 滚动条美化 */
89
+ ::-webkit-scrollbar { width: 6px; }
90
+ ::-webkit-scrollbar-track { background: transparent; }
91
+ ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
92
+ ::-webkit-scrollbar-thumb:hover { background: #444; }
93
+ </style>
94
+ """
95
+
96
+ def get_default_data_dir():
97
+ # 如果在 Space 环境,优先使用当前目录下的 cache_data 文件夹,避免 /root 权限问题
98
+ if os.environ.get("SPACE_ID") or os.environ.get("HF_SPACE"):
99
+ return str(Path.cwd() / "cache_data")
100
+
101
+ custom_dir = os.environ.get("HF_NOTES_DATA_DIR")
102
+ if custom_dir: return custom_dir
103
+ if os.name == "nt":
104
+ base = os.environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
105
+ else:
106
+ base = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
107
+ return str(Path(base) / "hf-note-app-pro")
108
+
109
+ DATA_DIR = get_default_data_dir()
110
+ LOCAL_NOTES_PATH = str(Path(DATA_DIR) / "notes.json")
111
+
112
+ def ensure_local_notes():
113
+ Path(DATA_DIR).mkdir(parents=True, exist_ok=True)
114
+ p = Path(LOCAL_NOTES_PATH)
115
+ if not p.exists():
116
+ p.write_text("[]", encoding="utf-8")
117
+
118
+ def read_notes():
119
+ ensure_local_notes()
120
+ try:
121
+ data = json.loads(Path(LOCAL_NOTES_PATH).read_text(encoding="utf-8-sig"))
122
+ if isinstance(data, list):
123
+ valid_notes = []
124
+ for item in data:
125
+ if not isinstance(item, dict): continue
126
+ # 关键修复:同时支持 C# 风格 (Uppercase) Python 风格 (Lowercase) 的键名
127
+ n_id = item.get("Id") or item.get("id", "")
128
+ n_title = item.get("Title") or item.get("title", "")
129
+ n_content = item.get("Content") or item.get("content", "")
130
+ n_updated = item.get("UpdatedAt") or item.get("updated_at", "")
131
+ n_pinned = item.get("IsPinned") if "IsPinned" in item else item.get("is_pinned", False)
132
+ n_deleted = item.get("IsDeleted") if "IsDeleted" in item else item.get("is_deleted", False)
133
+
134
+ valid_notes.append({
135
+ "id": str(n_id),
136
+ "title": str(n_title),
137
+ "content": str(n_content),
138
+ "updated_at": str(n_updated),
139
+ "is_pinned": bool(n_pinned),
140
+ "is_deleted": bool(n_deleted)
141
+ })
142
+ return valid_notes
143
+ except Exception as e:
144
+ print(f"读取笔记失败: {e}")
145
+ return []
146
+
147
+ def write_notes(notes):
148
+ ensure_local_notes()
149
+ Path(LOCAL_NOTES_PATH).write_text(
150
+ json.dumps(notes, ensure_ascii=False, indent=2),
151
+ encoding="utf-8",
152
+ )
153
+
154
+ def now_beijing():
155
+ return datetime.now(BEIJING_TZ)
156
+
157
+ # --- 持久化管理 ---
158
+ class CloudSync:
159
+ def __init__(self):
160
+ self.api = HfApi(token=HF_TOKEN)
161
+
162
+ def pull(self):
163
+ try:
164
+ ensure_local_notes()
165
+ print(f"🔄 正在从 Dataset {DATASET_REPO_ID} 拉取 {REMOTE_NOTES_PATH}...")
166
+ downloaded_path = hf_hub_download(
167
+ repo_id=DATASET_REPO_ID,
168
+ filename=REMOTE_NOTES_PATH,
169
+ repo_type="dataset",
170
+ token=HF_TOKEN,
171
+ force_download=True,
172
+ revision="main",
173
+ )
174
+ shutil.copy(downloaded_path, LOCAL_NOTES_PATH)
175
+ return True, f" 云端拉取同步完成"
176
+ except Exception as e:
177
+ msg = str(e)
178
+ print(f"拉取失败详情: {msg}")
179
+ # 如果是 401/404,通常是 Token 没设或权限问题
180
+ if "401" in msg or "404" in msg:
181
+ return False, f"⚠️ 拉取败: 请检查 Space 的 HF_TOKEN 是否已正确配置 (Dataset 可能为私有)"
182
+ return False, f"⚠️ 拉取失败: {msg}"
183
+
184
+ def push(self):
185
+ ensure_local_notes()
186
+ if not os.path.exists(LOCAL_NOTES_PATH): return False, "❌ 文件丢失"
187
+ try:
188
+ self.api.upload_file(
189
+ path_or_fileobj=LOCAL_NOTES_PATH,
190
+ path_in_repo=REMOTE_NOTES_PATH,
191
+ repo_id=DATASET_REPO_ID,
192
+ repo_type="dataset",
193
+ commit_message=f"Web Update Pro at {now_beijing().strftime('%Y-%m-%d %H:%M:%S %z')}"
194
+ )
195
+ return True, "✅ 已备份至云端"
196
+ except Exception as e:
197
+ return False, f"❌ 备份失败: {e}"
198
+
199
+ sync_manager = CloudSync()
200
+
201
+ # --- 业务逻辑 ---
202
+ def load_notes_list(filter_type="all", search_query=""):
203
+ notes = read_notes()
204
+ query = search_query.lower() if search_query else ""
205
+
206
+ filtered = []
207
+ for n in notes:
208
+ # Tab 过滤
209
+ is_deleted = n.get("is_deleted", False)
210
+ is_pinned = n.get("is_pinned", False)
211
+
212
+ if filter_type == "trash":
213
+ if not is_deleted: continue
214
+ else:
215
+ if is_deleted: continue
216
+ if filter_type == "pinned" and not is_pinned: continue
217
+
218
+ # 搜索过滤
219
+ if query and query not in n["title"].lower() and query not in n["content"].lower():
220
+ continue
221
+
222
+ filtered.append(n)
223
+
224
+ # 排序:置顶优先,时间倒序
225
+ sorted_notes = sorted(filtered, key=lambda x: (x.get("is_pinned", False), x.get("updated_at", "")), reverse=True)
226
+
227
+ return [
228
+ [n["id"], f"{'📌 ' if n.get('is_pinned') else ''}{n['title'] or '未命名'}", n["updated_at"]]
229
+ for n in sorted_notes
230
+ ]
231
+
232
+ def get_note_detail(note_id):
233
+ if not note_id: return "", "", ""
234
+ notes = read_notes()
235
+ for n in notes:
236
+ if n["id"] == note_id:
237
+ return n["title"], n["content"], n["updated_at"]
238
+ return "", "", ""
239
+
240
+ def handle_save(note_id, title, content, push_cloud=False):
241
+ if not title and not content:
242
+ return "无内容可保存", load_notes_list(), note_id
243
+
244
+ notes = read_notes()
245
+ now = now_beijing().isoformat(timespec="seconds")
246
+
247
+ found = False
248
+ for n in notes:
249
+ if n["id"] == note_id:
250
+ n["title"], n["content"], n["updated_at"] = title, content, now
251
+ found = True
252
+ break
253
+
254
+ if not found:
255
+ new_id = uuid4().hex
256
+ new_note = {
257
+ "id": new_id,
258
+ "title": title or "新笔记",
259
+ "content": content,
260
+ "updated_at": now,
261
+ "is_pinned": False,
262
+ "is_deleted": False
263
+ }
264
+ notes.insert(0, new_note)
265
+ note_id = new_id
266
+
267
+ write_notes(notes)
268
+
269
+ if push_cloud:
270
+ _, msg = sync_manager.push()
271
+ status = f"已保存并同步 | {msg}"
272
+ else:
273
+ status = "已自动保存到本地"
274
+
275
+ return status, load_notes_list(), note_id
276
+
277
+ def handle_delete(note_id, current_filter):
278
+ if not note_id: return "未选择笔记", load_notes_list(current_filter), ""
279
+ notes = read_notes()
280
+ for n in notes:
281
+ if n["id"] == note_id:
282
+ if current_filter == "trash":
283
+ notes.remove(n)
284
+ else:
285
+ n["is_deleted"] = True
286
+ n["is_pinned"] = False
287
+ break
288
+ write_notes(notes)
289
+ sync_manager.push()
290
+ return "已移至回收站" if current_filter != "trash" else "已彻底删除", load_notes_list(current_filter), ""
291
+
292
+ def handle_pin(note_id, current_filter):
293
+ if not note_id: return load_notes_list(current_filter)
294
+ notes = read_notes()
295
+ for n in notes:
296
+ if n["id"] == note_id:
297
+ n["is_pinned"] = not n.get("is_pinned", False)
298
+ break
299
+ write_notes(notes)
300
+ sync_manager.push()
301
+ return load_notes_list(current_filter)
302
+
303
+ # --- Gradio UI ---
304
+ with gr.Blocks(theme=gr.themes.Default(), head=PWA_HEAD) as demo:
305
+ current_filter_state = gr.State("all")
306
+ selected_note_id = gr.State("")
307
+
308
+ with gr.Row(equal_height=True):
309
+ # 1. 导航栏 (ClassNote 风格)
310
+ with gr.Column(scale=1, min_width=150):
311
+ gr.HTML("<div style='font-size: 20px; font-weight: bold; margin-bottom: 30px; color: white;'>HF Note</div>")
312
+ btn_all = gr.Button("全部笔记", variant="secondary", elem_classes=["nav-btn", "active-nav"])
313
+ btn_pinned = gr.Button("已置顶", variant="secondary", elem_classes=["nav-btn"])
314
+ btn_trash = gr.Button("回收站", variant="secondary", elem_classes=["nav-btn"])
315
+
316
+ gr.Markdown("---")
317
+ btn_new = gr.Button("➕ 新建笔记", variant="secondary")
318
+ btn_sync_pull = gr.Button("🔄 同步云端", variant="secondary")
319
+
320
+ # 2. 列表栏
321
+ with gr.Column(scale=2, min_width=250):
322
+ search_box = gr.Textbox(placeholder="搜索笔记...", show_label=False, elem_id="search_box")
323
+ note_list = gr.Dataframe(
324
+ headers=["ID", "标题", "时间"],
325
+ datatype=["str", "str", "str"],
326
+ col_count=(3, "fixed"),
327
+ interactive=False,
328
+ label=None,
329
+ )
330
+ status_text = gr.Markdown("就绪")
331
+
332
+ # 3. 编辑器栏
333
+ with gr.Column(scale=4):
334
+ with gr.Row():
335
+ btn_save = gr.Button("💾 保存", variant="primary", size="sm")
336
+ btn_pin = gr.Button("📌 置顶", variant="secondary", size="sm")
337
+ btn_del = gr.Button("🗑️ 删除", variant="stop", size="sm")
338
+ btn_ai = gr.Button("AI 润色", variant="primary", size="sm", elem_id="ai_btn")
339
+
340
+ edit_title = gr.Textbox(placeholder="无标题笔记", show_label=False, elem_id="note_title")
341
+ edit_content = gr.TextArea(placeholder="暂无内容,开始输入...", show_label=False, lines=25, elem_id="note_content")
342
+ edit_date = gr.Markdown("", elem_id="note_date")
343
+
344
+ # --- 交互事件 ---
345
+
346
+ def on_note_select(evt: gr.SelectData, filt):
347
+ curr_list = load_notes_list(filt)
348
+ if not curr_list or not evt.index or evt.index[0] >= len(curr_list):
349
+ return "", "", "", ""
350
+ note_id = curr_list[evt.index[0]][0]
351
+ title, content, date = get_note_detail(note_id)
352
+ return note_id, title, content, f"最后修改: {date}"
353
+
354
+ def handle_autosave(note_id, title, content):
355
+ return handle_save(note_id, title, content, push_cloud=False)
356
+
357
+ def handle_manual_save(note_id, title, content):
358
+ return handle_save(note_id, title, content, push_cloud=True)
359
+
360
+ def switch_filter(filter_type, search_query):
361
+ return (
362
+ filter_type,
363
+ load_notes_list(filter_type, search_query),
364
+ "",
365
+ "",
366
+ "",
367
+ "",
368
+ f"已切换到:{filter_type}"
369
+ )
370
+
371
+ def switch_all(search_query):
372
+ return switch_filter("all", search_query)
373
+
374
+ def switch_pinned(search_query):
375
+ return switch_filter("pinned", search_query)
376
+
377
+ def switch_trash(search_query):
378
+ return switch_filter("trash", search_query)
379
+
380
+ note_list.select(on_note_select, [current_filter_state], [selected_note_id, edit_title, edit_content, edit_date])
381
+
382
+ search_box.change(load_notes_list, [current_filter_state, search_box], [note_list])
383
+
384
+ # 离开焦点时保存
385
+ edit_title.blur(handle_autosave, [selected_note_id, edit_title, edit_content], [status_text, note_list, selected_note_id])
386
+ edit_content.blur(handle_autosave, [selected_note_id, edit_title, edit_content], [status_text, note_list, selected_note_id])
387
+ # 显式保存按钮(PWA/移动端更可靠)
388
+ btn_save.click(handle_manual_save, [selected_note_id, edit_title, edit_content], [status_text, note_list, selected_note_id])
389
+
390
+ btn_all.click(switch_all, [search_box], [current_filter_state, note_list, selected_note_id, edit_title, edit_content, edit_date, status_text])
391
+ btn_pinned.click(switch_pinned, [search_box], [current_filter_state, note_list, selected_note_id, edit_title, edit_content, edit_date, status_text])
392
+ btn_trash.click(switch_trash, [search_box], [current_filter_state, note_list, selected_note_id, edit_title, edit_content, edit_date, status_text])
393
+
394
+ btn_new.click(lambda: ("", "新笔记", "", ""), None, [selected_note_id, edit_title, edit_content, edit_date])
395
+ btn_pin.click(handle_pin, [selected_note_id, current_filter_state], [note_list])
396
+ btn_del.click(handle_delete, [selected_note_id, current_filter_state], [status_text, note_list, selected_note_id])
397
+
398
+ btn_sync_pull.click(lambda: (sync_manager.pull()[1], load_notes_list()), None, [status_text, note_list])
399
+
400
+ def ai_polish(content):
401
+ if not content: return content
402
+ return f"✨ [AI 润色已模拟完成]\n\n{content}\n\n(请在本地动作中使用完整的 DeepSeek 润色服务)"
403
+ btn_ai.click(ai_polish, [edit_content], [edit_content])
404
+
405
+ # 启动拉取
406
+ demo.load(lambda: (sync_manager.pull()[1], load_notes_list()), None, [status_text, note_list])
407
+
408
+ if __name__ == "__main__":
409
+ demo.launch()