码桶

发现社区成员的开源项目

版本历史

5 次提交

提交详情

0481b07
提交编号0481b071c133c0a2069c5f7551ba97bc72fa2326
提交作者DMFish<codebucket@localhost>
提交时间2026-07-31 11:51:40 +0800
更新说明更新项目文件

文件变更 12 个文件

文件新增删除
.env.example+8-0
DEPLOY.md+77-29
admin.html+1-0
api/data.js+104-0
contact.html+5-1
css/style.css+257-1
index.html+3-0
js/admin.js+50-31
js/data.js+7-3
js/main.js+4-1
js/sync.js+65-0
package.json+5-2

文件差异 逐行查看本次提交的修改

.env.example+8-0
@@ -4,3 +4,11 @@
44 # 管理後台密碼(必填)
55 # 設置後,密碼只在服務端驗證,前端代碼裡看不到
66 ADMIN_PASSWORD=your-secure-password-here
7+
8+# ===== Upstash Redis =====
9+# Redis 的環境變量會在 Vercel 安裝 Upstash 後自動注入,無需手動設置:
10+# KV_REST_API_URL=https://xxx.upstash.io
11+# KV_REST_API_TOKEN=xxx
12+# (部分集成也可能注入 UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN,代碼兩者都兼容)
13+#
14+# 安裝步驟:Vercel 項目 → Storage → Create Database → Upstash Redis
DEPLOY.md+77-29
@@ -1,20 +1,22 @@
1-# 域名米表 - Vercel 部署指南
1+# 域名米表 - Vercel 部署指南(Upstash Redis 版)
22 ## 項目結構
33 ```
44 domain-market/
55 ├── api/
6-│ └── auth.js ← Vercel Serverless Function(密碼驗證)
7-├── index.html ← 前台域名展示頁
8-├── contact.html ← 聯絡我們頁面
9-├── admin.html ← 後台管理頁
10-├── css/style.css ← 共享樣式
6+│ ├── auth.js ← 密碼驗證(環境變量)
7+│ └── data.js ← Redis 數據讀寫(GET 公開 / POST 認證)
8+├── index.html ← 前台域名展示頁
9+├── contact.html ← 聯絡我們頁面
10+├── admin.html ← 後台管理頁
11+├── css/style.css ← 共享樣式
1112 ├── js/
12-│ ├── data.js ← 數據層(localStorage + SiteConfig)
13-│ ├── main.js ← 前台邏輯
14-│ └── admin.js ← 後台邏輯(API 登入 + 本地兜底)
15-├── package.json
13+│ ├── sync.js ← 數據同步層(init 拉取 + save 推送)
14+│ ├── data.js ← 數據層(localStorage + Redis 雙寫)
15+│ ├── main.js ← 前台邏輯
16+│ └── admin.js ← 後台邏輯
17+├── package.json ← 含 @upstash/redis 依賴
1618 ├── vercel.json
1719 └── .env.example
1820 ```
@@ -32,39 +34,85 @@ domain-market/
3234 5. Build Command 留空(純靜態,不需要構建)
3335 6. 點 Deploy
34-### 3. 設置環境變量(關鍵步驟)
35-部署完成後:
36+### 3. 安裝 Upstash Redis(關鍵步驟)
37+
38+> ⚠️ Vercel KV 已於 2024 年底廢棄,現在用 Upstash Redis 替代。
39+
40+1. 進入 Vercel 項目 → 點頂部 **Storage** 標籤頁
41+2. 點 **Create Database** → 選 **Upstash Redis**(如果看不到,直接訪問 [Vercel Marketplace](https://vercel.com/marketplace?category=storage) 搜索 "redis")
42+3. 取個名字(如 `domain-market-db`),選擇區域(建議選離用戶近的)
43+4. 點 **Connect to Project** / **Create & Connect**
44+5. 安裝完成後,Vercel 會自動把以下環境變量注入到項目:
45+ - `UPSTASH_REDIS_REST_URL`
46+ - `UPSTASH_REDIS_REST_TOKEN`
47+
48+### 4. 設置密碼環境變量
3649 1. 進入項目 Settings → Environment Variables
3750 2. 添加變量:
3851 - Name: `ADMIN_PASSWORD`
3952 - Value: `你的管理密碼`(例如 `MySecurePass2024!`)
4053 3. 點 Save
41-4. 回到 Deployments,點最新一條旁邊的 `...` → Redeploy
42-### 4. 完成驗證
54+### 5. 重新部署
55+回到 Deployments,點最新一條旁邊的 `...` → **Redeploy**
56+
57+### 6. 完成驗證
4358 - 打開 `https://你的項目.vercel.app/admin.html`
4459 - 輸入你設置的 `ADMIN_PASSWORD` 密碼
45-- 成功登入 = 部署完成
60+- 成功登入後,添加幾個域名
61+- 打開 `https://你的自定義域名/` → 應該能看到剛添加的域名
62+
63+## 數據存儲架構
64+
65+```
66+管理員添加域名
67+ ↓
68+DomainDB.save() → localStorage(即時) + POST /api/data → Upstash Redis(持久化)
69+ ↓
70+訪客打開任意域名 → DataSync.init() → GET /api/data → Redis 讀取 → 覆蓋 localStorage → 顯示
71+```
46-## 密碼安全說明
72+| 特性 | localStorage | Upstash Redis |
73+|------|-------------|-----------|
74+| 跨域名共享 | 否 | ✅ 是 |
75+| 跨瀏覽器共享 | 否 | ✅ 是 |
76+| 換設備保留 | 否 | ✅ 是 |
77+| 清緩存丟失 | 是 | 否,Redis 裡還有 |
78+| 免費額度 | 無限 | 10,000 命令/天 |
79+
80+## 安全說明
4781 | 模式 | 密碼位置 | 前端可見? |
4882 |------|----------|-----------|
49-| Vercel 部署 | 環境變量 `ADMIN_PASSWORD` | 否,密碼只在服務端驗證 |
50-| 純靜態部署(VPS/CDN) | localStorage | 是,默認密碼 `admin888` 寫在 JS 裡 |
83+| Vercel 部署 | 環境變量 `ADMIN_PASSWORD` | 否 |
84+| 純靜態部署(VPS/CDN) | localStorage | 是,默認 `admin888` |
85+
86+### API 權限
87+- `GET /api/data` — **公開**,任何人可讀取域名列表和網站設置
88+- `POST /api/data` — **需認證**,只有登入後的管理員才能寫入
51-部署在 Vercel 上時,登入邏輯:
52-1. 前端發送密碼到 `/api/auth`
53-2. Serverless Function 用環境變量比對
54-3. 返回成功/失敗,密碼本身不會返回前端
55-4. 登入狀態存在 sessionStorage(關閉瀏覽器後失效)
89+## 修改密碼
56-## 數據說明
90+到 Vercel 項目 Settings → Environment Variables 修改 `ADMIN_PASSWORD`,然後 Redeploy。
57-域名數據和網站設置仍存在瀏覽器的 localStorage 中(按瀏覽器隔離)。
58-換電腦前請用後台的「導出 JSON」備份,在新電腦「匯入 JSON」恢復。
91+## 從舊版升級(已有數據遷移)
59-## 修改密碼
92+如果之前已在 vercel.app 域名上添加過域名(存在瀏覽器 localStorage):
60-- **Vercel 模式**:到 Vercel 項目 Settings → Environment Variables 修改 `ADMIN_PASSWORD`,然後 Redeploy
61-- **靜態模式**:後台 → 修改密碼(僅影響當前瀏覽器)
93+1. 打開 `https://舊域名.vercel.app/admin.html`
94+2. 點「導出 JSON」下載備份
95+3. 確認 Upstash Redis 已安裝並連接到項目
96+4. 重新部署(Redeploy)使新版代碼生效
97+5. 打開 `https://你的域名/admin.html`,登入
98+6. 點「匯入 JSON」上傳剛下載的備份
99+7. 數據自動寫入 Redis,之後所有域名都能看到
100+
101+## 本地開發
102+
103+不裝 Redis 也能跑。DataSync 會檢測到 Redis 不可用,自動回退到純 localStorage 模式:
104+
105+```bash
106+cd domain-market
107+python -m http.server 8765
108+# 打開 http://localhost:8765/
109+```
admin.html+1-0
@@ -272,6 +272,7 @@
272272 </div>
273273 </div>
274+<script src="js/sync.js"></script>
274275 <script src="js/data.js"></script>
275276 <script src="js/admin.js"></script>
276277 </body>
contact.html+5-1
@@ -8,6 +8,8 @@
88 </head>
99 <body>
10+<button class="nav-toggle" onclick="var s=document.querySelector('.sidebar');s.classList.toggle('sidebar-open');this.querySelector('.hamburger').textContent=s.classList.contains('sidebar-open')?'✕':'☰';"><span class="hamburger">☰</span> 菜單</button>
11+
1012 <table class="main-table">
1113 <tr>
1214 <!-- 側邊導航 -->
@@ -39,9 +41,11 @@
3941 </tr>
4042 </table>
43+<script src="js/sync.js"></script>
4144 <script src="js/data.js"></script>
4245 <script>
43-(function() {
46+(async function() {
47+ await DataSync.init();
4448 const cfg = SiteConfig.get();
4549 // 標題
css/style.css+257-1
@@ -516,7 +516,16 @@ body {
516516 to { transform: translateX(0); opacity: 1; }
517517 }
518-/* 響應式 */
518+/* ============================================
519+ 響應式 - 手機端適配
520+ ============================================ */
521+
522+/* ---- 導航開關按鈕(僅手機端顯示) ---- */
523+.nav-toggle {
524+ display: none;
525+}
526+
527+/* ---- 平板及以下 ---- */
519528 @media (max-width: 920px) {
520529 .main-table {
521530 width: 100%;
@@ -541,3 +550,250 @@ body {
541550 padding: 15px;
542551 }
543552 }
553+
554+/* ---- 手機端 ---- */
555+@media (max-width: 768px) {
556+
557+ /* ===== 通用 ===== */
558+ body {
559+ font-size: 14px;
560+ }
561+
562+ /* ===== 導航開關按鈕 ===== */
563+ .nav-toggle {
564+ display: block;
565+ width: 100%;
566+ background: #cc0066;
567+ color: #FFF;
568+ padding: 10px 16px;
569+ border: none;
570+ font-size: 15px;
571+ font-family: inherit;
572+ font-weight: bold;
573+ cursor: pointer;
574+ text-align: left;
575+ position: sticky;
576+ top: 0;
577+ z-index: 100;
578+ }
579+ .nav-toggle .hamburger {
580+ display: inline-block;
581+ margin-right: 8px;
582+ font-size: 18px;
583+ }
584+
585+ /* ===== 主佈局 → block 模式 ===== */
586+ .main-table,
587+ .main-table > tbody,
588+ .main-table > tbody > tr,
589+ .main-table > tbody > tr > td {
590+ display: block;
591+ width: 100%;
592+ }
593+ .main-table {
594+ border: none;
595+ }
596+ .main-table > tbody > tr > td.content {
597+ padding: 8px;
598+ }
599+
600+ /* ===== 側邊導航 → 預設收起,展開時下拉 ===== */
601+ .main-table > tbody > tr > td.sidebar {
602+ display: none;
603+ width: 100%;
604+ border-right: none;
605+ border-bottom: 2px solid #FF00FF;
606+ padding: 8px 0;
607+ background: #FF99FF;
608+ }
609+ .main-table > tbody > tr > td.sidebar.sidebar-open {
610+ display: block;
611+ }
612+
613+ .nav-btn {
614+ display: inline-block;
615+ margin: 4px;
616+ font-size: 16px;
617+ padding: 8px 14px;
618+ border-width: 3px;
619+ }
620+
621+ /* ===== 標題區 ===== */
622+ .title-en { font-size: 18px; }
623+ .title-zh { font-size: 18px; }
624+ .intro-text { font-size: 13px; margin: 10px 0; }
625+ .feature-list { font-size: 13px; margin: 10px 0 10px 20px; }
626+
627+ /* ===== 搜尋列 ===== */
628+ .search-bar {
629+ flex-wrap: wrap;
630+ gap: 6px;
631+ padding: 0 4px;
632+ }
633+ .search-bar select {
634+ flex: 1;
635+ min-width: 80px;
636+ font-size: 13px;
637+ }
638+ .search-bar input[type="text"] {
639+ flex: 2;
640+ min-width: 120px;
641+ font-size: 13px;
642+ }
643+ .search-bar button {
644+ font-size: 13px;
645+ padding: 6px 14px;
646+ }
647+
648+ /* ===== 域名表格 ===== */
649+ .domain-table {
650+ font-size: 12px;
651+ }
652+ .domain-table th {
653+ padding: 6px 4px;
654+ font-size: 12px;
655+ }
656+ .domain-table td {
657+ padding: 5px 6px;
658+ font-size: 12px;
659+ }
660+ .domain-table td.cat {
661+ width: 60px;
662+ font-size: 12px;
663+ }
664+ .result-count {
665+ font-size: 12px;
666+ padding: 0 4px;
667+ }
668+
669+ /* ===== 聯絡頁 ===== */
670+ .contact-info {
671+ font-size: 13px;
672+ padding: 0 8px;
673+ }
674+
675+ /* ===== 後台管理 ===== */
676+ .admin-header {
677+ padding: 10px 12px;
678+ font-size: 16px;
679+ }
680+ .admin-header a {
681+ font-size: 12px;
682+ }
683+ .admin-container {
684+ margin: 0;
685+ padding: 12px;
686+ border-radius: 0;
687+ }
688+
689+ /* 統計卡片 → 2列 */
690+ .stats-bar {
691+ gap: 8px;
692+ }
693+ .stat-card {
694+ min-width: calc(50% - 4px);
695+ flex: 1 1 calc(50% - 4px);
696+ padding: 10px;
697+ }
698+ .stat-card .num { font-size: 22px; }
699+ .stat-card .label { font-size: 12px; }
700+
701+ /* 表單 → 標籤和輸入框上下排列 */
702+ .form-row {
703+ flex-direction: column;
704+ align-items: stretch;
705+ gap: 4px;
706+ }
707+ .form-row label {
708+ width: auto;
709+ text-align: left;
710+ font-size: 13px;
711+ }
712+ .form-row input,
713+ .form-row select {
714+ width: 100% !important;
715+ min-width: 0 !important;
716+ font-size: 14px;
717+ padding: 8px 10px;
718+ }
719+ .form-row input[type="text"] { min-width: 0 !important; }
720+ .form-row label[style] { margin-left: 0 !important; }
721+
722+ /* 工具列 */
723+ .toolbar {
724+ gap: 6px;
725+ }
726+ .toolbar input[type="text"],
727+ .toolbar select {
728+ width: 100% !important;
729+ font-size: 13px;
730+ }
731+ .toolbar .spacer { display: none; }
732+ .toolbar .btn { flex: 1; min-width: 70px; }
733+ .btn-sm { padding: 6px 8px; font-size: 12px; }
734+
735+ /* 管理表格 → 橫向滾動 */
736+ .admin-table {
737+ display: block;
738+ overflow-x: auto;
739+ white-space: nowrap;
740+ -webkit-overflow-scrolling: touch;
741+ }
742+ .admin-table th,
743+ .admin-table td {
744+ font-size: 12px;
745+ padding: 6px 8px;
746+ }
747+
748+ /* 登入頁 */
749+ .login-box {
750+ width: 90%;
751+ max-width: 320px;
752+ padding: 20px;
753+ }
754+ .login-box h2 { font-size: 18px; }
755+ .login-box input { font-size: 15px; }
756+ .login-box button { font-size: 15px; }
757+
758+ /* 批量匯入 */
759+ #batchInput {
760+ font-size: 12px !important;
761+ }
762+
763+ /* 設置區表單 */
764+ #settingsArea { padding: 12px !important; }
765+ #settingsArea .form-row input,
766+ #settingsArea .form-row select {
767+ width: 100% !important;
768+ }
769+
770+ /* Toast 提示 */
771+ .toast {
772+ top: 10px;
773+ right: 10px;
774+ left: 10px;
775+ font-size: 13px;
776+ text-align: center;
777+ }
778+}
779+
780+/* ---- 小手機 ---- */
781+@media (max-width: 480px) {
782+ .nav-btn {
783+ font-size: 14px;
784+ padding: 6px 10px;
785+ margin: 3px;
786+ border-width: 2px;
787+ }
788+ .domain-table {
789+ font-size: 11px;
790+ }
791+ .domain-table td { padding: 4px 3px; }
792+ .domain-table td.cat { width: 50px; }
793+ .stats-bar { gap: 6px; }
794+ .stat-card { padding: 8px; }
795+ .stat-card .num { font-size: 18px; }
796+ .stat-card .label { font-size: 11px; }
797+ .admin-header { font-size: 14px; padding: 8px 10px; }
798+ .admin-header a { font-size: 11px; }
799+}
index.html+3-0
@@ -10,6 +10,8 @@
1010 </head>
1111 <body>
12+<button class="nav-toggle" onclick="var s=document.querySelector('.sidebar');s.classList.toggle('sidebar-open');this.querySelector('.hamburger').textContent=s.classList.contains('sidebar-open')?'✕':'☰';"><span class="hamburger">☰</span> 菜單</button>
13+
1214 <table class="main-table">
1315 <tr>
1416 <!-- 側邊導航 -->
@@ -74,6 +76,7 @@
7476 </tr>
7577 </table>
78+<script src="js/sync.js"></script>
7679 <script src="js/data.js"></script>
7780 <script src="js/main.js"></script>
7881 </body>
js/admin.js+50-31
@@ -27,13 +27,14 @@ function hideLogin() {
2727 }
2828 // 檢查會話(刷新頁面時自動登入)
29-function checkSession() {
29+async function checkSession() {
3030 const token = sessionStorage.getItem('admin_token');
3131 if (!token) return false;
3232 isApiMode = sessionStorage.getItem('admin_api_mode') === '1';
3333 isLoggedIn = true;
3434 hideLogin();
3535 document.getElementById('mainContent').style.display = '';
36+ await DataSync.init();
3637 loadStats();
3738 loadDomainList();
3839 initCategoryOptions();
@@ -66,7 +67,7 @@ async function doLogin() {
6667 isApiMode = true;
6768 sessionStorage.setItem('admin_token', data.token || '1');
6869 sessionStorage.setItem('admin_api_mode', '1');
69- onLoginSuccess();
70+ await onLoginSuccess();
7071 return;
7172 }
@@ -78,7 +79,7 @@ async function doLogin() {
7879 isApiMode = false;
7980 sessionStorage.setItem('admin_token', 'local_' + Date.now());
8081 sessionStorage.setItem('admin_api_mode', '0');
81- onLoginSuccess();
82+ await onLoginSuccess();
8283 return;
8384 }
8485 showToast('密碼錯誤!', 'error');
@@ -89,11 +90,12 @@ async function doLogin() {
8990 }
9091 }
91-function onLoginSuccess() {
92+async function onLoginSuccess() {
9293 isLoggedIn = true;
9394 hideLogin();
9495 document.getElementById('mainContent').style.display = '';
9596 showToast('登入成功!', 'success');
97+ await DataSync.init();
9698 loadStats();
9799 loadDomainList();
98100 initCategoryOptions();
@@ -120,13 +122,22 @@ function updatePasswordSection() {
120122 if (!pwdArea) return;
121123 if (isApiMode) {
122- pwdArea.innerHTML =
123- '<div style="padding:15px;background:#FFF;border:1px solid #FF99FF;border-radius:5px;">' +
124- '<p style="color:#990000;font-weight:bold;margin-bottom:8px;">密碼由 Vercel 環境變量管理</p>' +
125- '<p style="font-size:13px;color:#666;">當前為 Vercel 部署模式,管理密碼由環境變量 ' +
126- '<code style="background:#FFF0FF;padding:2px 6px;border-radius:3px;">ADMIN_PASSWORD</code> 設定。<br>' +
127- '如需修改密碼,請到 Vercel 項目設置 &rarr; Environment Variables 中修改。</p>' +
128- '</div>';
124+ // 隱藏密碼表單(不銷毀,避免影響事件綁定)
125+ const form = pwdArea.querySelector('#pwdForm');
126+ if (form) form.style.display = 'none';
127+
128+ // 插入提示信息(只插一次)
129+ if (!pwdArea.querySelector('.api-mode-msg')) {
130+ const msg = document.createElement('div');
131+ msg.className = 'api-mode-msg';
132+ msg.style.cssText = 'padding:15px;background:#FFF;border:1px solid #FF99FF;border-radius:5px;';
133+ msg.innerHTML =
134+ '<p style="color:#990000;font-weight:bold;margin-bottom:8px;">密碼由 Vercel 環境變量管理</p>' +
135+ '<p style="font-size:13px;color:#666;">當前為 Vercel 部署模式,管理密碼由環境變量 ' +
136+ '<code style="background:#FFF0FF;padding:2px 6px;border-radius:3px;">ADMIN_PASSWORD</code> 設定。<br>' +
137+ '如需修改密碼,請到 Vercel 項目設置 &rarr; Environment Variables 中修改。</p>';
138+ pwdArea.insertBefore(msg, form || null);
139+ }
129140 }
130141 }
@@ -489,54 +500,62 @@ function resetSettings() {
489500 }
490501 // 頁面初始化
491-document.addEventListener('DOMContentLoaded', function() {
502+document.addEventListener('DOMContentLoaded', async function() {
503+ // 安全綁定事件(元素不存在時跳過,不會中斷後續綁定)
504+ function bind(id, event, handler) {
505+ const el = document.getElementById(id);
506+ if (el) el.addEventListener(event, handler);
507+ else console.warn('Element not found:', id);
508+ }
509+
492510 // 先檢查會話,已登入則跳過登入頁
493- if (!checkSession()) {
511+ const hadSession = await checkSession();
512+ if (!hadSession) {
494513 showLogin();
495514 }
496- document.getElementById('loginBtn').addEventListener('click', doLogin);
497- document.getElementById('loginPwd').addEventListener('keypress', function(e) {
515+ bind('loginBtn', 'click', doLogin);
516+ bind('loginPwd', 'keypress', function(e) {
498517 if (e.key === 'Enter') doLogin();
499518 });
500- document.getElementById('domainForm').addEventListener('submit', submitDomain);
501- document.getElementById('cancelBtn').addEventListener('click', cancelEdit);
519+ bind('domainForm', 'submit', submitDomain);
520+ bind('cancelBtn', 'click', cancelEdit);
502- document.getElementById('adminSearch').addEventListener('input', loadDomainList);
503- document.getElementById('adminStatusFilter').addEventListener('change', loadDomainList);
521+ bind('adminSearch', 'input', loadDomainList);
522+ bind('adminStatusFilter', 'change', loadDomainList);
504- document.getElementById('batchBtn').addEventListener('click', function() {
523+ bind('batchBtn', 'click', function() {
505524 const area = document.getElementById('batchArea');
506525 area.style.display = area.style.display === 'none' ? '' : 'none';
507526 });
508- document.getElementById('batchSubmit').addEventListener('click', batchImport);
527+ bind('batchSubmit', 'click', batchImport);
509- document.getElementById('pwdBtn').addEventListener('click', function() {
528+ bind('pwdBtn', 'click', function() {
510529 const area = document.getElementById('pwdArea');
511530 area.style.display = area.style.display === 'none' ? '' : 'none';
512531 });
513- document.getElementById('pwdForm').addEventListener('submit', function(e) {
532+ bind('pwdForm', 'submit', function(e) {
514533 e.preventDefault();
515534 changePassword();
516535 });
517- document.getElementById('exportBtn').addEventListener('click', exportData);
518- document.getElementById('importFile').addEventListener('change', importData);
519- document.getElementById('clearBtn').addEventListener('click', clearAllData);
520- document.getElementById('logoutBtn').addEventListener('click', doLogout);
521- document.getElementById('viewSiteBtn').addEventListener('click', function() {
536+ bind('exportBtn', 'click', exportData);
537+ bind('importFile', 'change', importData);
538+ bind('clearBtn', 'click', clearAllData);
539+ bind('logoutBtn', 'click', doLogout);
540+ bind('viewSiteBtn', 'click', function() {
522541 window.open('index.html', '_blank');
523542 });
524543 // 網站設置
525- document.getElementById('settingsBtn').addEventListener('click', function() {
544+ bind('settingsBtn', 'click', function() {
526545 const area = document.getElementById('settingsArea');
527546 area.style.display = area.style.display === 'none' ? '' : 'none';
528547 if (area.style.display !== 'none') {
529548 loadSettings();
530549 }
531550 });
532- document.getElementById('settingsForm').addEventListener('submit', saveSettings);
533- document.getElementById('resetSettingsBtn').addEventListener('click', resetSettings);
551+ bind('settingsForm', 'submit', saveSettings);
552+ bind('resetSettingsBtn', 'click', resetSettings);
534553 });
js/data.js+7-3
@@ -25,9 +25,10 @@ const DomainDB = {
2525 }
2626 },
27- // 保存所有域名
27+ // 保存所有域名(同步到 localStorage + KV)
2828 save(domains) {
2929 localStorage.setItem(this.STORAGE_KEY, JSON.stringify(domains));
30+ if (typeof DataSync !== 'undefined') DataSync.save('domains', domains);
3031 },
3132 // 新增域名
@@ -151,6 +152,7 @@ const DomainDB = {
151152 // 清空所有數據
152153 clearAll() {
153154 localStorage.removeItem(this.STORAGE_KEY);
155+ if (typeof DataSync !== 'undefined') DataSync.save('domains', []);
154156 },
155157 // 密碼管理
@@ -241,6 +243,7 @@ const SiteConfig = {
241243 save(config) {
242244 localStorage.setItem(this.STORAGE_KEY, JSON.stringify(config));
245+ if (typeof DataSync !== 'undefined') DataSync.save('config', config);
243246 },
244247 update(partial) {
@@ -251,7 +254,8 @@ const SiteConfig = {
251254 },
252255 reset() {
253- localStorage.removeItem(this.STORAGE_KEY);
254- return this.getDefaults();
256+ const defaults = this.getDefaults();
257+ this.save(defaults);
258+ return defaults;
255259 }
256260 };
js/main.js+4-1
@@ -149,7 +149,10 @@ function initContactInfo() {
149149 }
150150 // 頁面初始化
151-document.addEventListener('DOMContentLoaded', function() {
151+document.addEventListener('DOMContentLoaded', async function() {
152+ // 先從 KV 同步最新數據到 localStorage
153+ await DataSync.init();
154+
152155 renderHeader();
153156 initCategoryFilter();
154157 initContactInfo();
package.json+5-2
@@ -1,6 +1,9 @@
11 {
22 "name": "domain-market",
3- "version": "1.0.0",
3+ "version": "2.0.0",
44 "private": true,
5- "description": "域名展示米表 - 66.hk 風格復刻版"
5+ "description": "域名展示米表 - 66.hk 風格復刻版(Upstash Redis 版)",
6+ "dependencies": {
7+ "@upstash/redis": "^1.34.0"
8+ }
69 }