首页
文章导航
导航
壁纸
留言板
更多
直播
友链
统计
关于
Search
1
经典老电影10部
1 阅读
2
【白嫖攻略】在cloudflare搭建域名邮箱并转发
1 阅读
3
春天来了清新图片
1 阅读
4
Caddy新一代轻量web服务器 配置简单 自动HTTPS 反向代理建站
1 阅读
5
免费二级域名,包括可托管到cf的二级域名
1 阅读
默认
日常
学习
技术
登录
Search
标签搜索
cloudflare
白嫖
安装
脚本
CF
壁纸
图片
docker
Linux
Caddy
代码
哪吒
节点
域名
桌面壁纸
手机壁纸
NAT
LXC
邮箱
优选
ws01
累计撰写
115
篇文章
累计收到
43
条评论
首页
栏目
默认
日常
学习
技术
页面
文章导航
导航
壁纸
留言板
直播
友链
统计
关于
搜索到
16
篇与
的结果
2025-08-05
Cloud Mail 简略部署教程
Cloud Mail 简略部署教程,如遇到不懂的建议看看原作者,这里只是一个简单的记录一下参考转自 冰糖雪梨's Blog 视频参考: 看看 一、创建worker项目1、到项目仓库fork项目代码到你的仓库2、回到cloudflare控制台,找到 workers 页面导入你的github仓库代码【 不是pages,是workers中的导入存储库 】设置启动目录,设置好下面3项A. 去掉 非生产分支的打钩B.部署命令npx wrangler deployC.路径/mail-worker耐心等待构建部署完成,进入项目设置D.环境变量添加变量名值类型作用domain["你的域名"], 示例:["mornglow.top"]JSON网站会使用这个域名作为邮箱域名admin管理员的邮箱,示例:admin@mornglow.top纯文本该邮箱用户注册后会成为网站管理员jwt_secret密码,随便输入一串字符串纯文本登录身份令牌的安全密钥E.绑定数据库kv和d1创建kv数据库,数据库名字随意创建d1数据库,数据库名字随意 位置选择离你近的地方d1数据库 变量名为 dbkv数据库 变量名为 kv浏览器输入https://你的自定义域名/api/init/你绑定的的jwt_secret 会自动初始化数据库(如果之前部署过只会更新不会覆盖原有数据)浏览器输入你绑定的自定义域名网站已经能打开了,注册上面邦定的管理员帐号和密码,以管理员身份登录并设置了F.启用邮件接收回到cloudflare控制台,点击菜单账户主页,再点你的域名,进入左边的 电子邮件 ,启用 电子邮件路由 ,编辑后发送到你的 这个 workers 项目 最后看看设置是否生效和正常,测试接收邮件,能接收才行G.使用第三方服务resend发送邮件浏览器输入resend.com注册一个帐号a.添加域名,选择最近的地区【如果是托管到CF的一级域名,授权抂会自动跳转,如果是二级域名,只能一项项添加DNS记录,并且在添加时特别注意 去掉值的最后一页字符 】b.设置API Key,并记录下来到你的邮箱设置中添加c.回调接口 https://你的域名/api/webhooks,并在 下面四项打钩 email.bounced、email.complained、email.delivered、email.delivery_delayedH.附件收发I.人机验证
2025年08月05日
0 阅读
0 评论
0 点赞
2025-07-26
Cloudflare Workers + KV 搭建一个带后台管理的blog
本文转载 参考自 一、相关链接: Github原项目 Github修改项目 cf blog是一个运行在cloudflare workers 上的博客程序,使用 cloudflare KV作为数据库,无其他依赖. 兼容静态博客的速度,以及动态博客的灵活性,您可以通过访问项目仓库了解更多详情。二、项目特点:使用workers提供的KV作为数据库,可达到wordpress的灵活性使用cloudflare缓存html来降低KV的读写,使其可达到静态博客的速度后台使用markdown语法,方便快捷,一键发布(页面重构+缓存清理)cfblog-plus新增了 文章置顶、后台首页选择、文章隐藏、静态搜索 等功能三、部署1、注册Cloudflare账号并登录,将域名托管到CloudFlare中获取区域ID:进入域名站点管理页面,页面向下滑动,记录右侧的“区域ID”的值,后续会用到。获取清除缓存 API 令牌:打开 API令牌管理页面 ,点击“创建令牌”按钮,页面拉到最下面,点击“创建自定义令牌”后面的“开始使用”按钮,按以下方式填写。令牌名字:CFBlog-plus 缓存权限:区域 -> 缓存清除 -> 清除 区域资源:包括 -> 特定区域 -> 【您的域名】token留下备用2、新建KV命名空间打开CloudFlare主页,点击左侧的“Workers”,然后点击“KV”项,即可进入“Worker KV”管理页面。在命名空间名称输入框里任意输入一个名称,为了见名知义,这里最好使用CFBLOG作为命名空间名称,点击添加即可。【可能遇到的问题】文章id为ID000nan且访问会报错误,解决方案:在创建的KV中修改SYSTEM_INDEX_NUM的值为03、创建Workers进入Workers 和 Pages 概述,点击“创建应用程序”,点击“创建Workers”,点击“编辑代码”将 index_plus.js 中的内容替换workers的内容【也可以用作者修改好的,后补】。先不修改配置,然后点击“保存并部署”。返回该workers的配置页面,添加“KV 命名空间绑定”,变量名用CFBLOG,下拉选择新添加的KV命名空间,点击“保存按钮”。再将页面滑动上面,点击“快速编辑”,重新打开workers的编辑页面开始添加各类配置。以上配置项均根据配置说明进行配置即可,不再详细说明。要记得“保存并部署”可以点击右侧的“预览”选项卡进行预览,能成功出现页面就说明配置成功了。4、配置域名访问【非必须】保存后可以看到列表里有刚添加的信息,自此,所有配置已经完成,可以愉快的写博客了5、扩展添加评论、阅读量、内容分类、页脚、自定义页面和图标、链接等,请自行发掘6、承载能力:KV基本不存在瓶颈,因为使用了缓存,读写很少唯一瓶颈是 workers的日访问量10w,大约能承受2万IP /日文章数:1G存储空间,几万篇问题不大
2025年07月26日
0 阅读
0 评论
0 点赞
2025-07-21
MoonTV 完整部署教程|免费搭建影视聚合平台!支持 Cloudflare Pages + 自动更新 + 多资源接口
MoonTV 完整部署教程|免费搭建影视聚合平台!支持 Cloudflare Pages + 自动更新 + 多资源接口一、本文参考: Hans汉斯 MoonTV 是一个基于 Next.js 构建的影视聚合搜索和播放平台,支持多家资源站的聚合内容,拥有简洁的前端、自动适配的播放接口、移动端优化,适合部署在 Cloudflare Pages 上,无需服务器、数据库或中转后端,即可免费运行。二、MoonTV Cloudflare Pages 快速部署指南1、Forkgithub项目 ,Fork 到你自己的 GitHub 账户2、创建 Cloudflare Pages 项目登录 Cloudflare,进入「Workers & Pages」> Pages 点击 导入现有Git存储库,选择 Connect to Git 授权你的 GitHub 仓库, 选择刚刚 Fork 的 MoonTV 项目 填写构建信息:框架预设:选择 无 构建命令:pnpm install --frozen-lockfile && pnpm run pages:build构建输出目录:.vercel/output/static保持默认设置完成首次部署 。进入设置,将兼容性标志设置为【不要选择 nodejs_compa 开头的有两个那两个,直接复制 nodejs_compa 查找, 特别重要 】nodejs_compat3、添加环境变量(推荐设置)点击 Pages 项目 > 设置 > 变量和机密,添加以下变量:名称示例值说明PASSWORDaa123456启用访问密码SITE_NAMEXXX影视页面标题ANNOUNCEMENT仅提供影视搜索服务公告栏说明NEXT_PUBLIC_STORAGE_TYPElocalstorage必须填写,否则出错NEXT_PUBLIC_SEARCH_MAX_PAGE5搜索最大页数NEXT_PUBLIC_AGGREGATE_SEARCH_RESULTtrue聚合同名搜索结果NEXT_PUBLIC_ENABLE_REGISTERfalse静态部署不建议开启注册表格表格表格设置完成后,回到 Overview 页面,点击 重试部署 以使环境变量生效。4、绑定自定义域名(可选但推荐)
2025年07月21日
0 阅读
1 评论
0 点赞
2025-07-16
cloudflare上免费部署随机地址生成器
cloudflare免费部署随机地址生成器github项目 这是一个基于 Cloudflare Workers 的随机地址生成器,可以生成全球多个国家的真实地址、姓名和电话号码。 本项目基于 Real-Address-Generator 做了一些样式和逻辑上的调整和优化。一、 主要功能 ,爱好者可以自己修改部署支持多个国家/地区的地址生成根据不同国家生成符合当地特色的姓名生成符合各国格式的电话号码实时地图预览地址保存和管理一键复制信息二、部署到 Cloudflare Workers1、原项目代码// 国家坐标数据 const countryCoordinates = { "US": [{ lat: 37.7749, lng: -122.4194 }, { lat: 34.0522, lng: -118.2437 }], "UK": [{ lat: 51.5074, lng: -0.1278 }, { lat: 53.4808, lng: -2.2426 }], "FR": [{ lat: 48.8566, lng: 2.3522 }, { lat: 45.7640, lng: 4.8357 }], "DE": [{ lat: 52.5200, lng: 13.4050 }, { lat: 48.1351, lng: 11.5820 }], "CN": [{ lat: 39.9042, lng: 116.4074 }, { lat: 31.2304, lng: 121.4737 }], "TW": [{ lat: 25.0330, lng: 121.5654 }, { lat: 22.6273, lng: 120.3014 }], "HK": [{ lat: 22.3193, lng: 114.1694 },{ lat: 22.3964, lng: 114.1095 }], "JP": [{ lat: 35.6895, lng: 139.6917 }, { lat: 34.6937, lng: 135.5023 }], "IN": [{ lat: 28.6139, lng: 77.2090 }, { lat: 19.0760, lng: 72.8777 }], "AU": [{ lat: -33.8688, lng: 151.2093 }, { lat: -37.8136, lng: 144.9631 }], "BR": [{ lat: -23.5505, lng: -46.6333 }, { lat: -22.9068, lng: -43.1729 }], "CA": [{ lat: 43.651070, lng: -79.347015 }, { lat: 45.501690, lng: -73.567253 }], "RU": [{ lat: 55.7558, lng: 37.6173 }, { lat: 59.9343, lng: 30.3351 }], "ZA": [{ lat: -33.9249, lng: 18.4241 }, { lat: -26.2041, lng: 28.0473 }], "MX": [{ lat: 19.4326, lng: -99.1332 }, { lat: 20.6597, lng: -103.3496 }], "KR": [{ lat: 37.5665, lng: 126.9780 }, { lat: 35.1796, lng: 129.0756 }], "IT": [{ lat: 41.9028, lng: 12.4964 }, { lat: 45.4642, lng: 9.1900 }], "ES": [{ lat: 40.4168, lng: -3.7038 }, { lat: 41.3851, lng: 2.1734 }], "TR": [{ lat: 41.0082, lng: 28.9784 }, { lat: 39.9334, lng: 32.8597 }], "SA": [{ lat: 24.7136, lng: 46.6753 }, { lat: 21.3891, lng: 39.8579 }], "AR": [{ lat: -34.6037, lng: -58.3816 }, { lat: -31.4201, lng: -64.1888 }], "EG": [{ lat: 30.0444, lng: 31.2357 }, { lat: 31.2156, lng: 29.9553 }], "NG": [{ lat: 6.5244, lng: 3.3792 }, { lat: 9.0579, lng: 7.4951 }], "ID": [{ lat: -6.2088, lng: 106.8456 }, { lat: -7.7956, lng: 110.3695 }] }; // 姓名数据 const namesByCountry = { "CN": { first: ["Li", "Wang", "Zhang", "Liu", "Chen", "Yang", "Huang", "Zhao", "Wu", "Zhou", "Xu", "Sun", "Ma", "Zhu", "Hu", "Guo", "He", "Gao", "Lin", "Zheng"], last: ["Wei", "Fang", "Na", "Xiuying", "Min", "Jing", "Li", "Qiang", "Lei", "Jun", "Yang", "Yong", "Yan", "Jie", "Tao", "Ming", "Chao", "Xiulan", "Xia", "Ping"] }, "JP": { first: ["Sato", "Suzuki", "Takahashi", "Tanaka", "Watanabe", "Ito", "Yamamoto", "Nakamura", "Kobayashi", "Kato"], last: ["Shota", "Ren", "Hina", "Yui", "Hiroto", "Sota", "Yota", "Misaki", "Nanami", "Yuto"] }, "KR": { first: ["Kim", "Lee", "Park", "Choi", "Jung", "Kang", "Jo", "Yoon", "Jang", "Lim"], last: ["Minjun", "Seojun", "Doyun", "Jiho", "Jihun", "Seoyeon", "Seoyun", "Jiwoo", "Seohyun", "Minseo"] }, "TW": { first: ["Chen", "Lin", "Huang", "Chang", "Lee", "Wang", "Wu", "Liu", "Tsai", "Yang"], last: ["Zhiming", "Jianhong", "Junjie", "Yijun", "Shufen", "Meiling", "Yating", "Jiahao", "Zhihao", "Shuhui"] }, "HK": { first: ["Chan", "Lee", "Wong", "Cheung", "Lau", "Wang", "Ng", "Cheng", "Leung", "Ho"], last: ["Chiming", "Kayan", "Junjie", "Wingsze", "Kaming", "Meiling", "Kahao", "Winger", "Chihao", "Shukfan"] }, "US": { first: ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"], last: ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Christopher"] }, "UK": { first: ["Smith", "Jones", "Williams", "Taylor", "Brown", "Davies", "Evans", "Wilson", "Thomas", "Roberts"], last: ["Oliver", "Jack", "Harry", "George", "Noah", "Charlie", "Jacob", "Oscar", "William", "Leo"] }, "FR": { first: ["Martin", "Bernard", "Dubois", "Thomas", "Robert", "Richard", "Petit", "Durand", "Leroy", "Moreau"], last: ["Lucas", "Louis", "Gabriel", "Arthur", "Jules", "Hugo", "Leo", "Adam", "Raphael", "Paul"] }, "DE": { first: ["Mueller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Schulz", "Hoffmann"], last: ["Ben", "Paul", "Leon", "Noah", "Luis", "Finn", "Felix", "Jonas", "Maximilian", "Henry"] }, "IT": { first: ["Rossi", "Ferrari", "Russo", "Bianchi", "Romano", "Gallo", "Costa", "Fontana", "Conti", "Esposito"], last: ["Leonardo", "Francesco", "Alessandro", "Lorenzo", "Matteo", "Andrea", "Gabriele", "Marco", "Antonio", "Giuseppe"] }, "ES": { first: ["Garcia", "Rodriguez", "Gonzalez", "Fernandez", "Lopez", "Martinez", "Sanchez", "Perez", "Martin", "Gomez"], last: ["Antonio", "Jose", "Manuel", "Francisco", "David", "Juan", "Miguel", "Javier", "Rafael", "Carlos"] }, "BR": { first: ["Silva", "Santos", "Oliveira", "Souza", "Rodrigues", "Ferreira", "Alves", "Pereira", "Lima", "Gomes"], last: ["Miguel", "Arthur", "Heitor", "Pedro", "Davi", "Gabriel", "Bernardo", "Lucas", "Matheus", "Rafael"] }, "RU": { first: ["Ivanov", "Smirnov", "Kuznetsov", "Popov", "Vasiliev", "Petrov", "Sokolov", "Mikhailov", "Fedorov", "Morozov"], last: ["Alexander", "Dmitry", "Maxim", "Ivan", "Andrey", "Mikhail", "Artem", "Daniel", "Roman", "Sergey"] }, "IN": { first: ["Kumar", "Singh", "Sharma", "Patel", "Gupta", "Shah", "Verma", "Rao", "Reddy", "Joshi"], last: ["Aarav", "Vihaan", "Vivaan", "Aditya", "Arjun", "Reyansh", "Ayaan", "Sai", "Krishna", "Ishaan"] }, "AU": { first: ["Smith", "Jones", "Williams", "Brown", "Wilson", "Taylor", "Johnson", "White", "Anderson", "Thompson"], last: ["Oliver", "William", "Jack", "Noah", "Thomas", "James", "Lucas", "Henry", "Ethan", "Alexander"] }, "CA": { first: ["Smith", "Brown", "Tremblay", "Martin", "Roy", "Wilson", "MacDonald", "Taylor", "Campbell", "Anderson"], last: ["Liam", "Noah", "Oliver", "William", "James", "Benjamin", "Lucas", "Henry", "Theodore", "Jack"] }, "MX": { first: ["Garcia", "Rodriguez", "Martinez", "Lopez", "Gonzalez", "Perez", "Sanchez", "Ramirez", "Torres", "Flores"], last: ["Santiago", "Mateo", "Sebastian", "Leonardo", "Diego", "Daniel", "Gabriel", "Adrian", "David", "Alexander"] }, "TR": { first: ["Yilmaz", "Kaya", "Demir", "Sahin", "Celik", "Yildiz", "Erdogan", "Ozturk", "Aydin", "Ozdemir"], last: ["Yusuf", "Eymen", "Ömer", "Mustafa", "Ali", "Mehmet", "Ahmet", "Emir", "Hamza", "Ibrahim"] }, "SA": { first: ["Al-Saud", "Al-Sheikh", "Al-Rashid", "Al-Qahtani", "Al-Ghamdi", "Al-Zahrani", "Al-Dossari", "Al-Shammari", "Al-Otaibi", "Al-Harbi"], last: ["Mohammed", "Abdullah", "Ahmed", "Ali", "Omar", "Ibrahim", "Khalid", "Hassan", "Fahad", "Abdul"] }, "AR": { first: ["Gonzalez", "Rodriguez", "Garcia", "Fernandez", "Lopez", "Martinez", "Perez", "Romero", "Sanchez", "Diaz"], last: ["Mateo", "Thiago", "Benjamin", "Valentino", "Santiago", "Juan", "Lucas", "Martin", "Nicolas", "Joaquin"] }, "EG": { first: ["Mohamed", "Ahmed", "Mahmoud", "Ibrahim", "Ali", "Hassan", "Hussein", "Mostafa", "Kamal", "Samir"], last: ["Omar", "Youssef", "Adam", "Malik", "Zain", "Hamza", "Kareem", "Hassan", "Ali", "Ibrahim"] }, "NG": { first: ["Okafor", "Adebayo", "Okonkwo", "Eze", "Oluwaseun", "Adegoke", "Afolabi", "Ogunleye", "Adeniyi", "Adesina"], last: ["Oluwadamilare", "Oluwatobiloba", "Ayomide", "Temitope", "Oluwaseun", "Adebayo", "Chibuike", "Chisom", "Chidi", "Obinna"] }, "ID": { first: ["Wijaya", "Kusuma", "Suryanto", "Halim", "Santoso", "Tanaka", "Wibowo", "Susanto", "Hidayat", "Putra"], last: ["Muhammad", "Ahmad", "Abdul", "Aditya", "Budi", "Dimas", "Eko", "Fajar", "Gading", "Hadi"] }, "ZA": { first: ["Nkosi", "Van der Merwe", "Botha", "Mkhize", "Khumalo", "Pretorius", "Venter", "Ndlovu", "Fourie", "Nel"], last: ["Bandile", "Themba", "Sipho", "Thabo", "Jabu", "Mandla", "Blessing", "Gift", "Lucky", "Precious"] } }; // 电话号码格式配置 const phoneFormats = { "US": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[201, 989]] }, "CN": { format: "+86 1XX-XXXX-XXXX", mobilePrefix: ["30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "50", "51", "52", "53", "55", "56", "57", "58", "59", "66", "70", "71", "72", "73", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89"] }, "JP": { format: "+81 XX-XXXX-XXXX", mobilePrefix: ["70", "80", "90"] }, "KR": { format: "+82 10-XXXX-XXXX" }, "UK": { format: "+44 7XXX XXXXXX", mobilePrefix: ["7"] }, "FR": { format: "+33 6 XX XX XX XX", mobilePrefix: ["6", "7"] }, "DE": { format: "+49 15X XXXXXXXX", mobilePrefix: ["15", "16", "17"] }, "TW": { format: "+886 9XX-XXX-XXX" }, "HK": { format: "+852 XXXX XXXX", mobilePrefix: ["5", "6", "9"] }, "AU": { format: "+61 4XX XXX XXX", mobilePrefix: ["4"] }, "CA": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[204, 989]] }, "MX": { format: "+52 1XX XXX XXXX" }, "TR": { format: "+90 5XX XXX XXXX", mobilePrefix: ["5"] }, "SA": { format: "+966 5XX XXX XXXX", mobilePrefix: ["5"] }, "AR": { format: "+54 9XX XXXX-XXXX" }, "EG": { format: "+20 1XX XXX XXXX", mobilePrefix: ["1"] }, "NG": { format: "+234 8XX XXX XXXX", mobilePrefix: ["7", "8", "9"] }, "ID": { format: "+62 8XX-XXXX-XXXX", mobilePrefix: ["8"] }, "ZA": { format: "+27 8X XXX XXXX", mobilePrefix: ["6", "7", "8"] } }; // 工具函数 function getRandomLocation(country) { const coordsArray = countryCoordinates[country]; const randomCity = coordsArray[Math.floor(Math.random() * coordsArray.length)]; const lat = randomCity.lat + (Math.random() - 0.5) * 0.1; const lng = randomCity.lng + (Math.random() - 0.5) * 0.1; return { lat, lng }; } function getRandomName(country) { if (!namesByCountry[country]) { return null; } const names = namesByCountry[country]; const firstName = names.first[Math.floor(Math.random() * names.first.length)]; const lastName = names.last[Math.floor(Math.random() * names.last.length)]; return `${firstName} ${lastName}`; } function generateAreaCode(ranges) { const range = ranges[Math.floor(Math.random() * ranges.length)]; const [min, max] = range; return Math.floor(min + Math.random() * (max - min + 1)); } function getRandomPhoneNumber(country) { const format = phoneFormats[country] || phoneFormats["US"]; let phone = format.format; if (format.areaCodeRanges) { const areaCode = generateAreaCode(format.areaCodeRanges); phone = phone.replace("XXX", areaCode); phone = phone.replace(/X/g, () => Math.floor(Math.random() * 10)); } else if (format.mobilePrefix) { const prefix = format.mobilePrefix[Math.floor(Math.random() * format.mobilePrefix.length)]; // 先替换前缀 if (prefix.length === 2) { phone = phone.replace(/XX/, prefix); } else { phone = phone.replace(/X/, prefix); } // 然后替换剩余的X phone = phone.replace(/X/g, () => Math.floor(Math.random() * 10)); } else { phone = phone.replace(/X/g, () => Math.floor(Math.random() * 10)); } return phone; } function isValidAddress(data) { return data && data.address && data.address.house_number && data.address.road && (data.address.city || data.address.town); } // 处理CORS请求的headers const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; // HTML 模板 const htmlContent = `<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>随机地址生成器</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet"> <script> tailwind.config = { theme: { extend: { animation: { 'gradient': 'gradient 8s linear infinite', }, keyframes: { gradient: { '0%, 100%': { 'background-size': '200% 200%', 'background-position': 'left center' }, '50%': { 'background-size': '200% 200%', 'background-position': 'right center' } } } } } } </script> </head> <body class="bg-gradient-to-br from-blue-50 via-white to-blue-50 text-gray-800 min-h-screen font-['Noto_Sans_SC']"> <!-- 头部 --> <header class="bg-gradient-to-r from-blue-500 via-sky-500 to-blue-500 animate-gradient w-full p-6 shadow-lg"> <div class="max-w-4xl mx-auto flex items-center justify-between"> <div class="flex items-center gap-3"> <img src="https://img.freepik.com/premium-vector/minimal-location-map-icon-logo-symbol-vector-design-transparent_965979-613.jpg?w=2000" alt="Logo" class="w-12 h-12 transform hover:scale-105 transition-transform"> <h1 class="text-2xl font-bold text-white">随机地址生成器</h1> </div> <a href="https://github.com/jiangnan1224/AddressGenerator/" target="_blank" class="flex items-center gap-2 text-white hover:text-gray-200 transition-colors"> <svg viewBox="0 0 16 16" class="w-6 h-6 fill-current" aria-hidden="true"> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path> </svg> <span class="font-medium">GitHub</span> </a> </div> </header> <!-- 主要内容 --> <main class="container mx-auto px-4 py-8 max-w-5xl"> <!-- 加载动画 --> <div id="loading" class="hidden fixed inset-0 bg-white bg-opacity-75 backdrop-blur-sm flex items-center justify-center z-50"> <div class="bg-white rounded-2xl p-8 flex flex-col items-center shadow-2xl"> <div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mb-4"></div> <div class="text-gray-800 text-lg font-medium">正在加载...</div> </div> </div> <div id="copied" class="hidden fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-40 transform transition-transform duration-300"> 已复制到剪贴板! </div> <!-- 国家选择 --> <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200 mb-8"> <div class="flex flex-col sm:flex-row items-start sm:items-center gap-4"> <div class="w-full sm:w-auto flex flex-col sm:flex-row items-start sm:items-center gap-2 flex-grow"> <label for="country" class="text-blue-600 font-bold whitespace-nowrap">选择国家/地区:</label> <select id="country" onchange="changeCountry(this.value)" class="w-full bg-gray-50 border border-gray-200 rounded-xl p-3 text-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"> </select> </div> <button onclick="generateNewAddress(document.getElementById('country').value)" class="w-full sm:w-auto bg-gradient-to-r from-blue-500 to-sky-500 hover:from-blue-600 hover:to-sky-600 text-white font-bold py-3 px-6 rounded-xl transition-all transform hover:scale-105 hover:shadow-lg focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"> 获取新地址 </button> </div> </div> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <!-- 左侧面板 --> <div class="space-y-6"> <!-- 信息卡片 --> <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200"> <h2 class="text-xl font-bold mb-6 text-blue-600">个人信息</h2> <div class="space-y-4"> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">姓名:</strong><span id="name" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">性别:</strong><span id="gender" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">电话:</strong><span id="phone" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">地址:</strong><span id="address" class="ml-2"></span> </div> </div> </div> <!-- 保存按钮 --> <button onclick="saveAddress()" class="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white font-bold py-3 px-6 rounded-xl transition-all transform hover:scale-105 hover:shadow-lg focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"> 保存地址 </button> </div> <!-- 右侧面板 --> <div class="space-y-6"> <!-- 地图 --> <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200"> <h2 class="text-xl font-bold mb-6 text-blue-600">地图预览</h2> <iframe id="map" class="w-full h-[400px] rounded-xl border border-gray-200"></iframe> </div> </div> </div> <!-- 已保存的地址表格 --> <div class="mt-8 bg-white rounded-2xl shadow-xl p-6 border border-gray-200"> <h2 class="text-xl font-bold mb-6 text-blue-600">已保存的地址</h2> <div class="overflow-x-auto"> <table class="w-full border-collapse" id="savedAddressesTable"> <thead> <tr class="bg-gradient-to-r from-blue-500 to-sky-500 text-white"> <th class="p-4 text-left rounded-tl-lg">姓名</th> <th class="p-4 text-left">性别</th> <th class="p-4 text-left">电话号码</th> <th class="p-4 text-left">地址</th> <th class="p-4 text-left">备注</th> <th class="p-4 text-left rounded-tr-lg">操作</th> </tr> </thead> <tbody></tbody> </table> </div> </div> </main> <!-- 页脚 --> <footer class="text-center py-8 text-gray-600 text-sm mt-8 bg-gray-50 border-t border-gray-200"> <p class="max-w-4xl mx-auto px-4"> All right reserved <a href="https://github.com/jiangnan1224/AddressGenerator/" target="_blank" class="inline-flex items-center hover:text-blue-600 transition-colors"> <img src="https://pic.imgdb.cn/item/66e7ab36d9c307b7e9cefd24.png" alt="GitHub" class="w-5 h-5 ml-1"> </a> </p> </footer> <script> // 国家数据 const countries = [ { name: "美国", code: "US" }, { name: "英国", code: "UK" }, { name: "法国", code: "FR" }, { name: "德国", code: "DE" }, { name: "中国", code: "CN" }, { name: "中国台湾", code: "TW" }, { name: "中国香港", code: "HK" }, { name: "日本", code: "JP" }, { name: "印度", code: "IN" }, { name: "澳大利亚", code: "AU" }, { name: "巴西", code: "BR" }, { name: "加拿大", code: "CA" }, { name: "俄罗斯", code: "RU" }, { name: "南非", code: "ZA" }, { name: "墨西哥", code: "MX" }, { name: "韩国", code: "KR" }, { name: "意大利", code: "IT" }, { name: "西班牙", code: "ES" }, { name: "土耳其", code: "TR" }, { name: "沙特阿拉伯", code: "SA" }, { name: "阿根廷", code: "AR" }, { name: "埃及", code: "EG" }, { name: "尼日利亚", code: "NG" }, { name: "印度尼西亚", code: "ID" } ]; // 初始化国家选择下拉框 function initCountrySelect() { const select = document.getElementById('country'); countries.forEach(country => { const option = document.createElement('option'); option.value = country.code; option.textContent = country.name; select.appendChild(option); }); } // 复制到剪贴板 function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { const copied = document.getElementById('copied'); copied.classList.remove('hidden'); copied.classList.add('translate-y-0'); copied.classList.remove('translate-y-[-100%]'); setTimeout(() => { copied.classList.add('translate-y-[-100%]'); copied.classList.remove('translate-y-0'); setTimeout(() => { copied.classList.add('hidden'); }, 300); }, 2000); }); } // 显示/隐藏加载动画 function toggleLoading(show) { const loading = document.getElementById('loading'); if (show) { loading.classList.remove('hidden'); } else { loading.classList.add('hidden'); } } // 更改国家 function changeCountry(country) { toggleLoading(true); generateNewAddress(country); } // 保存地址 function saveAddress() { const note = prompt('请输入备注(可以留空)') || ''; const savedAddresses = JSON.parse(localStorage.getItem('savedAddresses') || '[]'); const newEntry = { note: note, name: document.getElementById('name').textContent, gender: document.getElementById('gender').textContent, phone: document.getElementById('phone').textContent, address: document.getElementById('address').textContent }; savedAddresses.push(newEntry); localStorage.setItem('savedAddresses', JSON.stringify(savedAddresses)); renderSavedAddresses(); } // 渲染保存的地址 function renderSavedAddresses() { const savedAddresses = JSON.parse(localStorage.getItem('savedAddresses') || '[]'); const tbody = document.querySelector('#savedAddressesTable tbody'); tbody.innerHTML = ''; savedAddresses.forEach((entry, index) => { const row = tbody.insertRow(); row.className = 'border-t border-gray-200 hover:bg-gray-50 transition-colors'; const cells = ['name', 'gender', 'phone', 'address', 'note', '']; cells.forEach((cell, i) => { const td = row.insertCell(); td.className = 'p-4'; if (i === cells.length - 1) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.className = 'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-all transform hover:scale-105 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50'; deleteBtn.onclick = () => { if (confirm('确定要删除这条记录吗?')) { savedAddresses.splice(index, 1); localStorage.setItem('savedAddresses', JSON.stringify(savedAddresses)); renderSavedAddresses(); } }; td.appendChild(deleteBtn); } else { td.textContent = entry[cell]; } }); }); } // 生成新地址 async function generateNewAddress(country) { if (!country) { country = document.getElementById('country').value; } toggleLoading(true); try { const response = await fetch(\`\${window.location.href}api?country=\${country}\`); const data = await response.json(); if (data.error) { alert(data.error); return; } document.getElementById('name').textContent = data.name; document.getElementById('gender').textContent = data.gender; document.getElementById('phone').textContent = data.phone; document.getElementById('address').textContent = data.address; // 更新地图 document.getElementById('map').src = \`https://www.google.com/maps?q=\${encodeURIComponent(data.address)}&output=embed\`; } catch (error) { console.error('Error fetching address:', error); alert('获取地址时发生错误,请重试'); } finally { toggleLoading(false); } } // 页面加载时初始化 window.onload = function() { initCountrySelect(); generateNewAddress(); renderSavedAddresses(); }; </script> </body> </html>`; async function handleRequest(request) { const url = new URL(request.url); const path = url.pathname; // 处理 API 请求 if (path === '/api') { // 原有的 API 处理逻辑 return handleApiRequest(request); } // 处理根路径请求,返回 HTML 页面 return new Response(htmlContent, { headers: { 'Content-Type': 'text/html;charset=UTF-8', ...corsHeaders } }); } // 将原有的处理逻辑移到单独的函数中 async function handleApiRequest(request) { if (request.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } if (request.method !== 'GET') { return new Response('Method not allowed', { status: 405 }); } const url = new URL(request.url); const country = url.searchParams.get('country') || 'US'; if (!countryCoordinates[country]) { return new Response(JSON.stringify({ error: 'Invalid country code' }), { status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } let attempts = 0; const maxAttempts = 20; while (attempts < maxAttempts) { try { const location = getRandomLocation(country); const apiUrl = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${location.lat}&lon=${location.lng}&zoom=18&addressdetails=1`; const response = await fetch(apiUrl, { headers: { 'User-Agent': 'Cloudflare Worker Random Address Generator' } }); const data = await response.json(); if (isValidAddress(data)) { let city = data.address.city || data.address.town || ''; city = city.split(';')[0].trim(); const address = `${data.address.house_number} ${data.address.road}, ${city}, ${data.address.postcode || ''}, ${country}`.replace(/\s+/g, ' ').trim(); const name = getRandomName(country); const gender = Math.random() > 0.5 ? 'Male' : 'Female'; const phone = getRandomPhoneNumber(country); const result = { name, gender, phone, address, coordinates: { lat: location.lat, lng: location.lng } }; return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } attempts++; if (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 100)); } } catch (error) { attempts++; if (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 100)); } } } return new Response(JSON.stringify({ error: 'Failed to generate valid address after multiple attempts' }), { status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } // 注册事件监听器 addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); }); {dotted startColor="#ff6c6c" endColor="#1989fa"/}2、修改后的代码,各有所好,当然也可以找一下其它人修改的代码2025年07月16日更新,界面调整,添加多个国家和地区// 国家坐标数据 const countryCoordinates = { "US": [{ lat: 37.7749, lng: -122.4194 }, { lat: 34.0522, lng: -118.2437 }, { lat: 32.7767, lng: -96.7970 }, { lat: 41.8781, lng: -87.6298 }, { lat: 29.7604, lng: -95.3698 }, { lat: 33.4484, lng: -112.0740 }, { lat: 39.9526, lng: -75.1652 }, { lat: 29.4241, lng: -98.4936 }, { lat: 32.7157, lng: -117.1611 }, { lat: 47.6062, lng: -122.3321 }], "NY US": [{ lat: 40.7128, lng: -74.0060 }], "California US": [{ lat: 34.0522, lng: -118.2437 }], "Texas US": [{ lat: 32.7767, lng: -96.7970 }], "CA US": [{ lat: 37.3382, lng: -121.8863 }], "Illinois US": [{ lat: 41.8781, lng: -87.6298 }], // 添加的城市坐标:休斯敦、菲尼克斯、费城、圣安东尼奥、圣地亚哥、西雅图 "TX US": [{ lat: 29.7604, lng: -95.3698 }], "Arizona US": [{ lat: 33.4484, lng: -112.0740 }], "Pennsylvania US": [{ lat: 39.9526, lng: -75.1652 }], "Tx US": [{ lat: 29.4241, lng: -98.4936 }], "Ca US": [{ lat: 32.7157, lng: -117.1611 }], "Washington US": [{ lat: 47.6062, lng: -122.3321 }], "UK": [{ lat: 51.5074, lng: -0.1278 }, { lat: 53.4808, lng: -2.2426 }], "FR": [{ lat: 48.8566, lng: 2.3522 }, { lat: 45.7640, lng: 4.8357 }, { lat: 43.2965, lng: 5.3698 }], "DE": [{ lat: 52.5200, lng: 13.4050 }, { lat: 48.1351, lng: 11.5820 }], "CN": [{ lat: 39.9042, lng: 116.4074 }, { lat: 31.2304, lng: 121.4737 }], "TW": [{ lat: 25.0330, lng: 121.5654 }, { lat: 22.6273, lng: 120.3014 }], "HK": [{ lat: 22.3193, lng: 114.1694 },{ lat: 22.3964, lng: 114.1095 }], "JP": [{ lat: 35.6895, lng: 139.6917 }, { lat: 34.6937, lng: 135.5023 }], "IN": [{ lat: 28.6139, lng: 77.2090 }, { lat: 19.0760, lng: 72.8777 }], "AU": [{ lat: -33.8688, lng: 151.2093 }, { lat: -37.8136, lng: 144.9631 }], "BR": [{ lat: -23.5505, lng: -46.6333 }, { lat: -22.9068, lng: -43.1729 }], "CA": [{ lat: 43.651070, lng: -79.347015 }, { lat: 45.501690, lng: -73.567253 }], "RU": [{ lat: 55.7558, lng: 37.6173 }, { lat: 59.9343, lng: 30.3351 }], "ZA": [{ lat: -33.9249, lng: 18.4241 }, { lat: -26.2041, lng: 28.0473 }], "MX": [{ lat: 19.4326, lng: -99.1332 }, { lat: 20.6597, lng: -103.3496 }], "KR": [{ lat: 37.5665, lng: 126.9780 }, { lat: 35.1796, lng: 129.0756 }], "IT": [{ lat: 41.9028, lng: 12.4964 }, { lat: 45.4642, lng: 9.1900 }], "ES": [{ lat: 40.4168, lng: -3.7038 }, { lat: 41.3851, lng: 2.1734 }], "TR": [{ lat: 41.0082, lng: 28.9784 }, { lat: 39.9334, lng: 32.8597 }], "SA": [{ lat: 24.7136, lng: 46.6753 }, { lat: 21.3891, lng: 39.8579 }], "AR": [{ lat: -34.6037, lng: -58.3816 }, { lat: -31.4201, lng: -64.1888 }], "EG": [{ lat: 30.0444, lng: 31.2357 }, { lat: 31.2156, lng: 29.9553 }], "NG": [{ lat: 6.5244, lng: 3.3792 }, { lat: 9.0579, lng: 7.4951 }], "ID": [{ lat: -6.2088, lng: 106.8456 }, { lat: -7.7956, lng: 110.3695 }], "VN": [{ lat: 21.0285, lng: 105.8048 }, { lat: 10.7626, lng: 106.6602 }], // 添加越南坐标数据 // 新增城市坐标 "Berlin DE": [{ lat: 52.5200, lng: 13.4050 }], "Bavaria DE": [{ lat: 48.1351, lng: 11.5820 }], "Île-de-France FR": [{ lat: 48.8566, lng: 2.3522 }], "Provence-Alpes-Côte d'Azur FR": [{ lat: 43.2965, lng: 5.3698 }], "London UK": [{ lat: 51.5074, lng: -0.1278 }], "Seoul KR": [{ lat: 37.5665, lng: 126.9780 }], "Ontario CA": [{ lat: 43.651070, lng: -79.347015 }], "Comunidad de Madrid ES": [{ lat: 40.4168, lng: -3.7038 }], "Cataluña ES": [{ lat: 41.3851, lng: 2.1734 }], "Tokyo JP": [{ lat: 35.6895, lng: 139.6917 }], "Osaka JP": [{ lat: 34.6937, lng: 135.5023 }], }; // 姓名数据 const namesByCountry = { "US": { first: ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"], last: ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Christopher"] }, "CN": { first: ["李", "王", "张", "刘", "陈", "杨", "黄", "赵", "吴", "周", "孙", "苏", "马", "朱", "候", "郭", "黑", "高", "林", "钱", "冯", "卫", "蒋", "许", "吕", "施", "曹", "严", "魏", "陶", "方", "雷"], last: ["力", "斌", "那", "真", "杰", "京", "丽", "晴", "来", "东", "勇", "仕", "云", "洁", "山", "伟", "明", "亮", "刚", "飞", "英", "兴", "浩", "曙光", "平", "志远", "志珍", "第一", "建国", "玉仙", "现红", "瑞山", "一山", "新丽", "思思", "青凤", "秀美"] }, "JP": { first: ["Sato", "Suzuki", "Takahashi", "Tanaka", "Watanabe", "Ito", "Yamamoto", "Nakamura", "Kobayashi", "Kato"], last: ["Shota", "Ren", "Hina", "Yui", "Hiroto", "Sota", "Yota", "Misaki", "Nanami", "Yuto"] }, "KR": { first: ["Kim", "Lee", "Park", "Choi", "Jung", "Kang", "Jo", "Yoon", "Jang", "Lim"], last: ["Minjun", "Seojun", "Doyun", "Jiho", "Jihun", "Seoyeon", "Seoyun", "Jiwoo", "Seohyun", "Minseo"] }, "TW": { first: ["Chen", "Lin", "Huang", "Chang", "Lee", "Wang", "Wu", "Liu", "Tsai", "Yang"], last: ["Zhiming", "Jianhong", "Junjie", "Yijun", "Shufen", "Meiling", "Yating", "Jiahao", "Zhihao", "Shuhui"] }, "HK": { first: ["Chan", "Lee", "Wong", "Cheung", "Lau", "Wang", "Ng", "Cheng", "Leung", "Ho"], last: ["Chiming", "Kayan", "Junjie", "Wingsze", "Kaming", "Meiling", "Kahao", "Winger", "Chihao", "Shukfan"] }, "UK": { first: ["Smith", "Jones", "Williams", "Taylor", "Brown", "Davies", "Evans", "Wilson", "Thomas", "Roberts"], last: ["Oliver", "Jack", "Harry", "George", "Noah", "Charlie", "Jacob", "Oscar", "William", "Leo"] }, "FR": { first: ["Martin", "Bernard", "Dubois", "Thomas", "Robert", "Richard", "Petit", "Durand", "Leroy", "Moreau"], last: ["Lucas", "Louis", "Gabriel", "Arthur", "Jules", "Hugo", "Leo", "Adam", "Raphael", "Paul"] }, "DE": { first: ["Mueller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Schulz", "Hoffmann"], last: ["Ben", "Paul", "Leon", "Noah", "Luis", "Finn", "Felix", "Jonas", "Maximilian", "Henry"] }, "IT": { first: ["Rossi", "Ferrari", "Russo", "Bianchi", "Romano", "Gallo", "Costa", "Fontana", "Conti", "Esposito"], last: ["Leonardo", "Francesco", "Alessandro", "Lorenzo", "Matteo", "Andrea", "Gabriele", "Marco", "Antonio", "Giuseppe"] }, "ES": { first: ["Garcia", "Rodriguez", "Gonzalez", "Fernandez", "Lopez", "Martinez", "Sanchez", "Perez", "Martin", "Gomez"], last: ["Antonio", "Jose", "Manuel", "Francisco", "David", "Juan", "Miguel", "Javier", "Rafael", "Carlos"] }, "BR": { first: ["Silva", "Santos", "Oliveira", "Souza", "Rodrigues", "Ferreira", "Alves", "Pereira", "Lima", "Gomes"], last: ["Miguel", "Arthur", "Heitor", "Pedro", "Davi", "Gabriel", "Bernardo", "Lucas", "Matheus", "Rafael"] }, "RU": { first: ["Ivanov", "Smirnov", "Kuznetsov", "Popov", "Vasiliev", "Petrov", "Sokolov", "Mikhailov", "Fedorov", "Morozov"], last: ["Alexander", "Dmitry", "Maxim", "Ivan", "Andrey", "Mikhail", "Artem", "Daniel", "Roman", "Sergey"] }, "IN": { first: ["Kumar", "Singh", "Sharma", "Patel", "Gupta", "Shah", "Verma", "Rao", "Reddy", "Joshi"], last: ["Aarav", "Vihaan", "Vivaan", "Aditya", "Arjun", "Reyansh", "Ayaan", "Sai", "Krishna", "Ishaan"] }, "AU": { first: ["Smith", "Jones", "Williams", "Brown", "Wilson", "Taylor", "Johnson", "White", "Anderson", "Thompson"], last: ["Oliver", "William", "Jack", "Noah", "Thomas", "James", "Lucas", "Henry", "Ethan", "Alexander"] }, "CA": { first: ["Smith", "Brown", "Tremblay", "Martin", "Roy", "Wilson", "MacDonald", "Taylor", "Campbell", "Anderson"], last: ["Liam", "Noah", "Oliver", "William", "James", "Benjamin", "Lucas", "Henry", "Theodore", "Jack"] }, "MX": { first: ["Garcia", "Rodriguez", "Martinez", "Lopez", "Gonzalez", "Perez", "Sanchez", "Ramirez", "Torres", "Flores"], last: ["Santiago", "Mateo", "Sebastian", "Leonardo", "Diego", "Daniel", "Gabriel", "Adrian", "David", "Alexander"] }, "TR": { first: ["Yilmaz", "Kaya", "Demir", "Sahin", "Celik", "Yildiz", "Erdogan", "Ozturk", "Aydin", "Ozdemir"], last: ["Yusuf", "Eymen", "Ömer", "Mustafa", "Ali", "Mehmet", "Ahmet", "Emir", "Hamza", "Ibrahim"] }, "SA": { first: ["Al-Saud", "Al-Sheikh", "Al-Rashid", "Al-Qahtani", "Al-Ghamdi", "Al-Zahrani", "Al-Dossari", "Al-Shammari", "Al-Otaibi", "Al-Harbi"], last: ["Mohammed", "Abdullah", "Ahmed", "Ali", "Omar", "Ibrahim", "Khalid", "Hassan", "Fahad", "Abdul"] }, "AR": { first: ["Gonzalez", "Rodriguez", "Garcia", "Fernandez", "Lopez", "Martinez", "Perez", "Romero", "Sanchez", "Diaz"], last: ["Mateo", "Thiago", "Benjamin", "Valentino", "Santiago", "Juan", "Lucas", "Martin", "Nicolas", "Joaquin"] }, "EG": { first: ["Mohamed", "Ahmed", "Mahmoud", "Ibrahim", "Ali", "Hassan", "Hussein", "Mostafa", "Kamal", "Samir"], last: ["Omar", "Youssef", "Adam", "Malik", "Zain", "Hamza", "Kareem", "Hassan", "Ali", "Ibrahim"] }, "NG": { first: ["Okafor", "Adebayo", "Okonkwo", "Eze", "Oluwaseun", "Adegoke", "Afolabi", "Ogunleye", "Adeniyi", "Adesina"], last: ["Oluwadamilare", "Oluwatobiloba", "Ayomide", "Temitope", "Oluwaseun", "Adebayo", "Chibuike", "Chisom", "Chidi", "Obinna"] }, "ID": { first: ["Wijaya", "Kusuma", "Suryanto", "Halim", "Santoso", "Tanaka", "Wibowo", "Susanto", "Hidayat", "Putra"], last: ["Muhammad", "Ahmad", "Abdul", "Aditya", "Budi", "Dimas", "Eko", "Fajar", "Gading", "Hadi"] }, "ZA": { first: ["Nkosi", "Van der Merwe", "Botha", "Mkhize", "Khumalo", "Pretorius", "Venter", "Ndlovu", "Fourie", "Nel"], last: ["Bandile", "Themba", "Sipho", "Thabo", "Jabu", "Mandla", "Blessing", "Gift", "Lucky", "Precious"] }, "VN": { first: ["Nguyen", "Tran", "Le", "Pham", "Hoang", "Vu", "Do", "Dao", "Bui", "Dang"], last: ["Van", "Minh", "Thanh", "Ngoc", "Huu", "Quoc", "Xuan", "Duc", "Tuan", "Khanh"] } }; // 电话号码格式配置 const phoneFormats = { "US": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[201, 989]] }, "CN": { format: "+86 1XX-XXXX-XXXX", mobilePrefix: ["30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "50", "51", "52", "53", "55", "56", "57", "58", "59", "66", "70", "71", "72", "73", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89"] }, "JP": { format: "+81 XX-XXXX-XXXX", mobilePrefix: ["70", "80", "90"] }, "KR": { format: "+82 10-XXXX-XXXX" }, "UK": { format: "+44 7XXX XXXXXX", mobilePrefix: ["7"] }, "FR": { format: "+33 6 XX XX XX XX", mobilePrefix: ["6", "7"] }, "DE": { format: "+49 15X XXXXXXXX", mobilePrefix: ["15", "16", "17"] }, "TW": { format: "+886 9XX-XXX-XXX" }, "HK": { format: "+852 XXXX XXXX", mobilePrefix: ["5", "6", "9"] }, "AU": { format: "+61 4XX XXX XXX", mobilePrefix: ["4"] }, "CA": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[204, 989]] }, "MX": { format: "+52 1XX XXX XXXX" }, "TR": { format: "+90 5XX XXX XXXX", mobilePrefix: ["5"] }, "SA": { format: "+966 5XX XXX XXXX", mobilePrefix: ["5"] }, "AR": { format: "+54 9XX XXXX-XXXX" }, "EG": { format: "+20 1XX XXX XXXX", mobilePrefix: ["1"] }, "NG": { format: "+234 8XX XXX XXXX", mobilePrefix: ["7", "8", "9"] }, "ID": { format: "+62 8XX-XXXX-XXXX", mobilePrefix: ["8"] }, "ZA": { format: "+27 8X XXX XXXX", mobilePrefix: ["6", "7", "8"] }, "VN": { format: "+84 9X XXX XXXX", mobilePrefix: ["9", "8"] } }; // 新增加的所有美国地区别名都指向同一个 US 数据 namesByCountry["NY US"] = namesByCountry["US"]; namesByCountry["California US"] = namesByCountry["US"]; namesByCountry["Texas US"] = namesByCountry["US"]; namesByCountry["CA US"] = namesByCountry["US"]; namesByCountry["Illinois US"] = namesByCountry["US"]; namesByCountry["TX US"] = namesByCountry["US"]; namesByCountry["Arizona US"] = namesByCountry["US"]; namesByCountry["Pennsylvania US"] = namesByCountry["US"]; namesByCountry["Tx US"] = namesByCountry["US"]; namesByCountry["Ca US"] = namesByCountry["US"]; namesByCountry["Washington US"] = namesByCountry["US"]; namesByCountry["Berlin DE"] = namesByCountry["DE"]; namesByCountry["Bavaria DE"] = namesByCountry["DE"]; namesByCountry["Île-de-France FR"] = namesByCountry["FR"]; namesByCountry["Provence-Alpes-Côte d'Azur FR"] = namesByCountry["FR"]; namesByCountry["London UK"] = namesByCountry["UK"]; namesByCountry["Seoul KR"] = namesByCountry["KR"]; namesByCountry["Ontario CA"] = namesByCountry["CA"]; namesByCountry["Comunidad de Madrid ES"] = namesByCountry["ES"]; namesByCountry["Cataluña ES"] = namesByCountry["ES"]; namesByCountry["Tokyo JP"] = namesByCountry["JP"]; namesByCountry["Osaka JP"] = namesByCountry["JP"]; // 新增加的美国所有电话别名都指向同一个 US 数据 phoneFormats["NY US"] = phoneFormats["US"]; phoneFormats["California US"] = phoneFormats["US"]; phoneFormats["Texas US"] = phoneFormats["US"]; phoneFormats["CA US"] = phoneFormats["US"]; phoneFormats["Illinois US"] = phoneFormats["US"]; phoneFormats["TX US"] = phoneFormats["US"]; phoneFormats["Arizona US"] = phoneFormats["US"]; phoneFormats["Pennsylvania US"] = phoneFormats["US"]; phoneFormats["Tx US"] = phoneFormats["US"]; phoneFormats["Ca US"] = phoneFormats["US"]; phoneFormats["Washington US"] = phoneFormats["US"]; phoneFormats["Berlin DE"] = phoneFormats["DE"]; phoneFormats["Bavaria DE"] = phoneFormats["DE"]; phoneFormats["Île-de-France FR"] = phoneFormats["FR"]; phoneFormats["Provence-Alpes-Côte d'Azur FR"] = phoneFormats["FR"]; phoneFormats["London UK"] = phoneFormats["UK"]; phoneFormats["Seoul KR"] = phoneFormats["KR"]; phoneFormats["Ontario CA"] = phoneFormats["CA"]; phoneFormats["Comunidad de Madrid ES"] = phoneFormats["ES"]; phoneFormats["Cataluña ES"] = phoneFormats["ES"]; phoneFormats["Tokyo JP"] = phoneFormats["JP"]; phoneFormats["Osaka JP"] = phoneFormats["JP"]; // 工具函数 function getRandomLocation(country) { const coordsArray = countryCoordinates[country]; const randomCity = coordsArray[Math.floor(Math.random() * coordsArray.length)]; const lat = randomCity.lat + (Math.random() - 0.5) * 0.1; const lng = randomCity.lng + (Math.random() - 0.5) * 0.1; return { lat, lng }; } function getRandomName(country) { if (!namesByCountry[country]) { return null; } const names = namesByCountry[country]; const firstName = names.first[Math.floor(Math.random() * names.first.length)]; const lastName = names.last[Math.floor(Math.random() * names.last.length)]; return `${firstName} ${lastName}`; } function generateAreaCode(ranges) { const range = ranges[Math.floor(Math.random() * ranges.length)]; const [min, max] = range; return Math.floor(min + Math.random() * (max - min + 1)); } function getRandomPhoneNumber(country) { const format = phoneFormats[country] || phoneFormats["US"]; let phone = format.format; if (format.areaCodeRanges) { const areaCode = generateAreaCode(format.areaCodeRanges); phone = phone.replace("XXX", areaCode); phone = phone.replace(/X/g, () => Math.floor(Math.random() * 10)); } else if (format.mobilePrefix) { const prefix = format.mobilePrefix[Math.floor(Math.random() * format.mobilePrefix.length)]; // 先替换前缀 if (prefix.length === 2) { phone = phone.replace(/XX/, prefix); } else { phone = phone.replace(/X/, prefix); } // 然后替换剩余的X phone = phone.replace(/X/g, () => Math.floor(Math.random() * 10)); } else { phone = phone.replace(/X/g, () => Math.floor(Math.random() * 10)); } return phone; } function isValidAddress(data) { return data && data.address && data.address.house_number && data.address.road && (data.address.city || data.address.town); } // 处理CORS请求的headers const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; // HTML 模板 const htmlContent = `<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>随机地址生成器</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet"> <script> tailwind.config = { theme: { extend: { animation: { 'gradient': 'gradient 8s linear infinite', }, keyframes: { gradient: { '0%, 100%': { 'background-size': '200% 200%', 'background-position': 'left center' }, '50%': { 'background-size': '200% 200%', 'background-position': 'right center' } } } } } } </script> </head> <link rel="shortcut icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>🌐</text></svg>"> <body class="bg-gradient-to-br from-blue-50 via-white to-blue-50 text-gray-800 min-h-screen font-['Noto_Sans_SC']"> <!-- 头部 --> <header class="bg-gradient-to-r from-blue-500 via-sky-500 to-blue-500 animate-gradient w-full p-6 shadow-lg"> <div class="max-w-4xl mx-auto flex items-center justify-between"> <div class="flex items-center gap-3"> <h1 class="text-2xl font-bold text-white">随机地址生成器</h1> </div> <div class="flex items-center gap-3"> <a href="/" target="_blank" class="flex items-center gap-2 text-white hover:text-gray-200 transition-colors"> <svg viewBox="0 0 16 16" class="w-6 h-6 fill-current" aria-hidden="true"> <img src="https://img.freepik.com/premium-vector/minimal-location-map-icon-logo-symbol-vector-design-transparent_965979-613.jpg?w=2000" alt="Logo" class="w-8 h-8 transform hover:scale-105 transition-transform"> </svg> </a> <a href="/about" class="flex items-center gap-2 text-white hover:text-gray-200 transition-colors"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> <path d="m8.93 6.588-2.29.287-.082.38.45.083c.296.07.366.177.246.475l-.136.283-.14.064c-.296.136-.494.397-.56.745-.107 1.044.44 1.43 1.234 1.43.46.002.832-.166 1.06-.432.246-.28.334-.677.256-1.176-.09-.564-.426-.977-.977-.977-.642 0-.962.514-.962 1.176 0 .242.047.466.126.652.126.309.34.564.626.652.296.09.662.034.962-.126.296-.16.494-.432.56-.744.107-.564-.246-1.098-.832-1.098-.426 0-.764.29-.962.652-.107.19-.186.408-.246.652-.06.244-.082.49-.082.744 0 1.134.732 2.042 1.634 2.042.902 0 1.634-.908 1.634-2.042 0-1.134-.732-2.042-1.634-2.042zm-.634 5.5c-.344 0-.6-.256-.6-.588 0-.332.256-.588.6-.588.344 0 .6.256.6.588 0 .332-.256.588-.6.588z"/> </svg> <span class="font-medium">关于</span> </a> </div> </div> </header> <!-- 关于页面内容 --> <div id="about" class="hidden max-w-4xl mx-auto px-4 py-8"> <div class="bg-white rounded-2xl shadow-xl p-8 border border-gray-200"> <h2 class="text-2xl font-bold mb-6 text-blue-600">关于本项目</h2> <div class="prose max-w-none"> <p class="mb-4">随机地址生成器是一个实用工具,可以帮助用户快速生成大多数常用国家和地区的随机地址信息【根据需要,国家和地区不断完善中】。该工具适用于测试、开发以及其他需要模拟地址数据的场景。</p> <p class="mb-4">本项目使用 Cloudflare Workers 技术构建,结合 OpenStreetMap 的地理编码服务,能够实时生成准确的地址信息。通过简单的界面操作,您可以轻松获取不同国家的地址、电话号码和个人信息。</p> <h3 class="text-xl font-semibold mt-6 mb-3 text-blue-600">主要功能</h3> <ul class="list-disc pl-6 space-y-2 mb-4"> <li>支持多个国家和地区的地址生成</li> <li>自动生成姓名、性别、电话号码等个人信息</li> <li>技术有限,英国、日本等一些国家的邮编,『地址中倒数第2项提取』</li> <li>集成地图预览功能</li> <li>支持保存常用地址</li> <li>响应式设计适配各种设备</li> </ul> <h3 class="text-xl font-semibold mt-6 mb-3 text-blue-600">技术特点</h3> <ul class="list-disc pl-6 space-y-2 mb-4"> <li>基于 Cloudflare Workers 构建,实现无服务器架构</li> <li>使用 Tailwind CSS 实现现代化 UI 设计</li> <li>集成 OpenStreetMap 地理编码 API</li> <li>前端完全静态化,无需后端数据库支持</li> <li>本地存储保存历史记录</li> </ul> <h3 class="text-xl font-semibold mt-6 mb-3 text-blue-600">使用说明</h3> <p class="mb-4">选择国家或地区后点击“获取新地址”按钮即可生成随机地址信息。您可以通过点击各项信息将其复制到剪贴板,也可以点击“保存地址”按钮将当前地址保存到本地浏览器。</p> <p class="mb-4">在保存的地址列表中,您可以查看所有已保存的地址,并可随时删除不需要的记录。</p> <h3 class="text-xl font-semibold mt-6 mb-3 text-blue-600">联系方式</h3> <p>如有任何问题或建议,请访问我们的<a href="https://www.199881.xyz/" target="_blank" class="text-blue-600 hover:underline">官方网站</a>或通过 GitHub 联系我们。</p> </div> </div> </div> <!-- 主要内容 --> <main id="main-content" class="container mx-auto px-4 py-8 max-w-5xl"> <!-- 加载动画 --> <div id="loading" class="hidden fixed inset-0 bg-white bg-opacity-75 backdrop-blur-sm flex items-center justify-center z-50"> <div class="bg-white rounded-2xl p-8 flex flex-col items-center shadow-2xl"> <div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mb-4"></div> <div class="text-gray-800 text-lg font-medium">正在加载...</div> </div> </div> <div id="copied" class="hidden fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-40 transform transition-transform duration-300"> 已复制到剪贴板! </div> <!-- 国家选择 --> <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200 mb-8"> <div class="flex flex-col sm:flex-row items-start sm:items-center gap-4"> <div class="w-full sm:w-auto flex flex-col sm:flex-row items-start sm:items-center gap-2 flex-grow"> <label for="country" class="text-blue-600 font-bold whitespace-nowrap">选择国家/地区:</label> <select id="country" onchange="changeCountry(this.value)" class="w-full bg-gray-50 border border-gray-200 rounded-xl p-3 text-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"> </select> </div> <button onclick="generateNewAddress(document.getElementById('country').value)" class="w-full sm:w-auto bg-gradient-to-r from-blue-500 to-sky-500 hover:from-blue-600 hover:to-sky-600 text-white font-bold py-3 px-6 rounded-xl transition-all transform hover:scale-105 hover:shadow-lg focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"> 获取新地址 </button> </div> </div> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <!-- 左侧面板 --> <div class="space-y-2"> <!-- 信息卡片 --> <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200"> <h2 class="text-xl font-bold mb-6 text-blue-600">个人信息</h2> <div class="space-y-2"> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">姓名:</strong><span id="name" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">性别:</strong><span id="gender" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">电话:</strong><span id="phone" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">邮编:</strong><span id="postcode" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">地址:</strong><span id="address" class="ml-2"></span> </div> </div> </div> <!-- 保存按钮 --> <button onclick="saveAddress()" class="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white font-bold py-3 px-6 rounded-xl transition-all transform hover:scale-105 hover:shadow-lg focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"> 保存地址 </button> </div> <!-- 右侧面板 --> <div class="space-y-2"> <!-- 地图 --> <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200"> <h2 class="text-xl font-bold mb-6 text-blue-600">地图预览</h2> <iframe id="map" class="w-full h-[372px] rounded-xl border border-gray-200"></iframe> </div> </div> </div> <!-- 已保存的地址表格 --> <div class="mt-8 bg-white rounded-2xl shadow-xl p-6 border border-gray-200"> <h2 class="text-xl font-bold mb-6 text-blue-600">已保存的地址</h2> <div class="overflow-x-auto"> <table class="w-full border-collapse" id="savedAddressesTable"> <thead> <tr class="bg-gradient-to-r from-blue-500 to-sky-500 text-white"> <th class="p-4 text-left rounded-tl-lg">姓名</th> <th class="p-4 text-left">性别</th> <th class="p-4 text-left">电话</th> <th class="p-4 text-left">地址</th> <th class="p-4 text-left">备注</th> <th class="p-4 text-left rounded-tr-lg">操作</th> </tr> </thead> <tbody></tbody> </table> </div> </div> </main> <!-- 页脚 --> <footer class="text-center py-8 text-gray-600 text-sm mt-8 bg-gray-50 border-t border-gray-200"> <p class="max-w-4xl mx-auto px-4"> 总访问量 <span id="busuanzi_site_pv"></span> 次 | <span id="timeDate">载入天数...</span> | <a href="https://boke.199881.xyz/" target="_blank"> <span style="color: blue;">博客 | <a href="https://www.199881.xyz/" target="_blank"> <span style="color: green;">导航 <a href="https://github.com/jiangnan1224/AddressGenerator/" target="_blank" class="inline-flex items-center hover:text-blue-600 transition-colors"> <img src="https://pic.imgdb.cn/item/66e7ab36d9c307b7e9cefd24.png" alt="GitHub" class="w-5 h-5 ml-1"> </a> <script language="javascript"> var now = new Date(); function createtime(){ var grt= new Date("04/23/2025 00:00:00");/*---这里是网站的启用时间--*/ now.setTime(now.getTime()+250); days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days); document.getElementById("timeDate").innerHTML = "运行"+dnum+"天"; } setInterval("createtime()",250); </script> <script defer src="https://bsz.211119.xyz/js"></script> </p> </footer> <script> // 国家数据 const countries = [ { name: "美国", code: "US" }, { name: "美国纽约", code: "NY US" }, { name: "美国洛杉矶", code: "California US" }, { name: "美国达拉斯", code: "Texas US" }, { name: "美国圣何塞", code: "CA US" }, { name: "美国芝加哥", code: "Illinois US" }, { name: "美国休斯敦", code: "TX US" }, { name: "美国菲尼克斯", code: "Arizona US" }, { name: "美国费城", code: "Pennsylvania US" }, { name: "美国圣安东尼奥", code: "Tx US" }, { name: "美国圣地亚哥", code: "Ca US" }, { name: "美国西雅图", code: "Washington US" }, { name: "德国", code: "DE" }, { name: "德国柏林", code: "Berlin DE" }, { name: "德国慕尼黑", code: "Bavaria DE" }, { name: "法国", code: "FR" }, { name: "法国巴黎", code: "Île-de-France FR" }, { name: "法国马赛", code: "Provence-Alpes-Côte d'Azur FR" }, { name: "越南", code: "VN" }, { name: "巴西", code: "BR" }, { name: "墨西哥", code: "MX" }, { name: "韩国", code: "Seoul KR" }, { name: "韩国首尔", code: "KR" }, { name: "意大利", code: "IT" }, { name: "西班牙", code: "ES" }, { name: "西班牙马德里", code: "Comunidad de Madrid ES" }, { name: "西班牙马德里巴塞罗那", code: "Cataluña ES" }, { name: "土耳其", code: "TR" }, { name: "埃及", code: "EG" }, { name: "印度尼西亚", code: "ID" }, // 以下邮编末知 { name: "英国", code: "UK" }, { name: "英国伦敦", code: "London UK" }, { name: "中国", code: "CN" }, { name: "中国台湾", code: "TW" }, { name: "中国香港", code: "HK" }, { name: "日本", code: "JP" }, { name: "日本东京", code: "Tokyo JP" }, { name: "日本大坂", code: "Osaka JP" }, { name: "印度", code: "IN" }, { name: "澳大利亚", code: "AU" }, { name: "加拿大", code: "CA" }, { name: "加拿大多伦多", code: "Ontario CA" }, { name: "俄罗斯", code: "RU" }, { name: "南非", code: "ZA" }, { name: "沙特阿拉伯", code: "SA" }, { name: "阿根廷", code: "AR" }, { name: "尼日利亚", code: "NG" } ]; // 初始化国家选择下拉框 function initCountrySelect() { const select = document.getElementById('country'); countries.forEach(country => { const option = document.createElement('option'); option.value = country.code; option.textContent = country.name; select.appendChild(option); }); // 添加返回主页按钮的点击事件 document.querySelector('a[href="/about"]').addEventListener('click', function(e) { e.preventDefault(); showAbout(); }); // 添加返回主页按钮的点击事件(在页脚也添加了一个) document.querySelector('a[href="/"]').addEventListener('click', function(e) { e.preventDefault(); showHome(); }); } // 复制到剪贴板 function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { const copied = document.getElementById('copied'); copied.classList.remove('hidden'); copied.classList.add('translate-y-0'); copied.classList.remove('translate-y-[-100%]'); setTimeout(() => { copied.classList.add('translate-y-[-100%]'); copied.classList.remove('translate-y-0'); setTimeout(() => { copied.classList.add('hidden'); }, 300); }, 2000); }); } // 显示/隐藏加载动画 function toggleLoading(show) { const loading = document.getElementById('loading'); if (show) { loading.classList.remove('hidden'); } else { loading.classList.add('hidden'); } } // 更改国家 function changeCountry(country) { toggleLoading(true); generateNewAddress(country); } // 保存地址 function saveAddress() { const note = prompt('请输入备注(可以留空)') || ''; const savedAddresses = JSON.parse(localStorage.getItem('savedAddresses') || '[]'); const newEntry = { note: note, name: document.getElementById('name').textContent, gender: document.getElementById('gender').textContent, phone: document.getElementById('phone').textContent, address: document.getElementById('address').textContent }; savedAddresses.push(newEntry); localStorage.setItem('savedAddresses', JSON.stringify(savedAddresses)); renderSavedAddresses(); } // 渲染保存的地址 function renderSavedAddresses() { const savedAddresses = JSON.parse(localStorage.getItem('savedAddresses') || '[]'); const tbody = document.querySelector('#savedAddressesTable tbody'); tbody.innerHTML = ''; savedAddresses.forEach((entry, index) => { const row = tbody.insertRow(); row.className = 'border-t border-gray-200 hover:bg-gray-50 transition-colors'; const cells = ['name', 'gender', 'phone', 'address', 'note', '']; cells.forEach((cell, i) => { const td = row.insertCell(); td.className = 'p-4'; if (i === cells.length - 1) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.className = 'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-all transform hover:scale-105 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50'; deleteBtn.onclick = () => { if (confirm('确定要删除这条记录吗?')) { savedAddresses.splice(index, 1); localStorage.setItem('savedAddresses', JSON.stringify(savedAddresses)); renderSavedAddresses(); } }; td.appendChild(deleteBtn); } else { td.textContent = entry[cell]; } }); }); } // 生成新地址 async function generateNewAddress(country) { if (!country) { country = document.getElementById('country').value; } toggleLoading(true); try { const response = await fetch(\`\${window.location.href}api?country=\${country}\`); const data = await response.json(); if (data.error) { alert(data.error); return; } // 提取邮编逻辑 const addressText = data.address || ''; const postcodeMatch = addressText.match(/\\b\\d{5}(?:\\-\\d{4})?\\b/g); // 匹配国际通用的邮编格式 const postcode = postcodeMatch ? postcodeMatch[0] : '未知『地址中倒数第2项提取』'; document.getElementById('name').textContent = data.name; document.getElementById('gender').textContent = data.gender; document.getElementById('phone').textContent = data.phone; document.getElementById('address').textContent = addressText; // 显示完整地址 document.getElementById('postcode').textContent = postcode; // 显示邮编 // 更新地图 document.getElementById('map').src = \`https://www.google.com/maps?q=\${encodeURIComponent(data.address)}&output=embed\`; } catch (error) { console.error('Error fetching address:', error); showErrorModal('获取地址时发生错误,请检查网络后点击重试', () => { generateNewAddress(document.getElementById('country').value); }); } finally { toggleLoading(false); } } // 页面加载时初始化 window.onload = function() { initCountrySelect(); generateNewAddress(); renderSavedAddresses(); }; // 显示关于页面 function showAbout() { document.getElementById('main-content').classList.add('hidden'); document.getElementById('about').classList.remove('hidden'); } // 返回主页 function showHome() { document.getElementById('about').classList.add('hidden'); document.getElementById('main-content').classList.remove('hidden'); } </script> </body> </html>`; async function handleRequest(request) { const url = new URL(request.url); const path = url.pathname; // 处理 API 请求 if (path === '/api') { // 原有的 API 处理逻辑 return handleApiRequest(request); } // 处理根路径请求,返回 HTML 页面 return new Response(htmlContent, { headers: { 'Content-Type': 'text/html;charset=UTF-8', ...corsHeaders } }); } // 将原有的处理逻辑移到单独的函数中 async function handleApiRequest(request) { if (request.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } if (request.method !== 'GET') { return new Response('Method not allowed', { status: 405 }); } const url = new URL(request.url); const country = url.searchParams.get('country') || 'US'; if (!countryCoordinates[country]) { return new Response(JSON.stringify({ error: 'Invalid country code' }), { status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } let attempts = 0; const maxAttempts = 20; while (attempts < maxAttempts) { try { const location = getRandomLocation(country); const apiUrl = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${location.lat}&lon=${location.lng}&zoom=18&addressdetails=1`; const response = await fetch(apiUrl, { headers: { 'User-Agent': 'Cloudflare Worker Random Address Generator' } }); const data = await response.json(); if (isValidAddress(data)) { let city = data.address.city || data.address.town || ''; city = city.split(';')[0].trim(); const address = `${data.address.house_number} ${data.address.road}, ${city}, ${data.address.postcode || ''}, ${country}`.replace(/\s+/g, ' ').trim(); const name = getRandomName(country); const gender = Math.random() > 0.4 ? 'Male男' : 'Female女'; const phone = getRandomPhoneNumber(country); const result = { name, gender, phone, address, coordinates: { lat: location.lat, lng: location.lng } }; return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } attempts++; if (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 100)); } } catch (error) { attempts++; if (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 100)); } } } return new Response(JSON.stringify({ error: 'Failed to generate valid address after multiple attempts' }), { status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } // 注册事件监听器 addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); }); 2025年07月15日更新,界面调整,添加多个国家和地区// 国家坐标数据 const countryCoordinates = { "US": [{ lat: 37.7749, lng: -122.4194 }, { lat: 34.0522, lng: -118.2437 }, { lat: 32.7767, lng: -96.7970 }, { lat: 41.8781, lng: -87.6298 }], "NY US": [{ lat: 40.7128, lng: -74.0060 }], "California US": [{ lat: 34.0522, lng: -118.2437 }], "Texas US": [{ lat: 32.7767, lng: -96.7970 }], "CA US": [{ lat: 37.3382, lng: -121.8863 }], "Illinois US": [{ lat: 41.8781, lng: -87.6298 }], "UK": [{ lat: 51.5074, lng: -0.1278 }, { lat: 53.4808, lng: -2.2426 }], "FR": [{ lat: 48.8566, lng: 2.3522 }, { lat: 45.7640, lng: 4.8357 }], "DE": [{ lat: 52.5200, lng: 13.4050 }, { lat: 48.1351, lng: 11.5820 }], "CN": [{ lat: 39.9042, lng: 116.4074 }, { lat: 31.2304, lng: 121.4737 }], "TW": [{ lat: 25.0330, lng: 121.5654 }, { lat: 22.6273, lng: 120.3014 }], "HK": [{ lat: 22.3193, lng: 114.1694 },{ lat: 22.3964, lng: 114.1095 }], "JP": [{ lat: 35.6895, lng: 139.6917 }, { lat: 34.6937, lng: 135.5023 }], "IN": [{ lat: 28.6139, lng: 77.2090 }, { lat: 19.0760, lng: 72.8777 }], "AU": [{ lat: -33.8688, lng: 151.2093 }, { lat: -37.8136, lng: 144.9631 }], "BR": [{ lat: -23.5505, lng: -46.6333 }, { lat: -22.9068, lng: -43.1729 }], "CA": [{ lat: 43.651070, lng: -79.347015 }, { lat: 45.501690, lng: -73.567253 }], "RU": [{ lat: 55.7558, lng: 37.6173 }, { lat: 59.9343, lng: 30.3351 }], "ZA": [{ lat: -33.9249, lng: 18.4241 }, { lat: -26.2041, lng: 28.0473 }], "MX": [{ lat: 19.4326, lng: -99.1332 }, { lat: 20.6597, lng: -103.3496 }], "KR": [{ lat: 37.5665, lng: 126.9780 }, { lat: 35.1796, lng: 129.0756 }], "IT": [{ lat: 41.9028, lng: 12.4964 }, { lat: 45.4642, lng: 9.1900 }], "ES": [{ lat: 40.4168, lng: -3.7038 }, { lat: 41.3851, lng: 2.1734 }], "TR": [{ lat: 41.0082, lng: 28.9784 }, { lat: 39.9334, lng: 32.8597 }], "SA": [{ lat: 24.7136, lng: 46.6753 }, { lat: 21.3891, lng: 39.8579 }], "AR": [{ lat: -34.6037, lng: -58.3816 }, { lat: -31.4201, lng: -64.1888 }], "EG": [{ lat: 30.0444, lng: 31.2357 }, { lat: 31.2156, lng: 29.9553 }], "NG": [{ lat: 6.5244, lng: 3.3792 }, { lat: 9.0579, lng: 7.4951 }], "ID": [{ lat: -6.2088, lng: 106.8456 }, { lat: -7.7956, lng: 110.3695 }], "VN": [{ lat: 21.0285, lng: 105.8048 }, { lat: 10.7626, lng: 106.6602 }], // 添加越南坐标数据 }; // 姓名数据 const namesByCountry = { "CN": { first: ["李", "王", "张", "刘", "陈", "杨", "黄", "赵", "吴", "周", "孙", "苏", "马", "朱", "候", "郭", "黑", "高", "林", "钱", "冯", "卫", "蒋", "许", "吕", "施", "曹", "严", "魏", "陶", "方", "雷"], last: ["力", "斌", "那", "真", "杰", "京", "丽", "晴", "来", "东", "勇", "仕", "云", "洁", "山", "伟", "明", "亮", "刚", "飞", "英", "兴", "浩", "曙光", "平", "志远", "志珍", "第一", "建国", "玉仙", "现红", "瑞山", "一山", "新丽", "思思", "青凤", "秀美"] }, "JP": { first: ["Sato", "Suzuki", "Takahashi", "Tanaka", "Watanabe", "Ito", "Yamamoto", "Nakamura", "Kobayashi", "Kato"], last: ["Shota", "Ren", "Hina", "Yui", "Hiroto", "Sota", "Yota", "Misaki", "Nanami", "Yuto"] }, "KR": { first: ["Kim", "Lee", "Park", "Choi", "Jung", "Kang", "Jo", "Yoon", "Jang", "Lim"], last: ["Minjun", "Seojun", "Doyun", "Jiho", "Jihun", "Seoyeon", "Seoyun", "Jiwoo", "Seohyun", "Minseo"] }, "TW": { first: ["Chen", "Lin", "Huang", "Chang", "Lee", "Wang", "Wu", "Liu", "Tsai", "Yang"], last: ["Zhiming", "Jianhong", "Junjie", "Yijun", "Shufen", "Meiling", "Yating", "Jiahao", "Zhihao", "Shuhui"] }, "HK": { first: ["Chan", "Lee", "Wong", "Cheung", "Lau", "Wang", "Ng", "Cheng", "Leung", "Ho"], last: ["Chiming", "Kayan", "Junjie", "Wingsze", "Kaming", "Meiling", "Kahao", "Winger", "Chihao", "Shukfan"] }, "US": { first: ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"], last: ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Christopher"] }, "NY US": { first: ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"], last: ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Christopher"] }, "California US": { first: ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"], last: ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Christopher"] }, "Texas US": { first: ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"], last: ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Christopher"] }, "CA US": { first: ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"], last: ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Christopher"] }, "Illinois US": { first: ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"], last: ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas", "Christopher"] }, "UK": { first: ["Smith", "Jones", "Williams", "Taylor", "Brown", "Davies", "Evans", "Wilson", "Thomas", "Roberts"], last: ["Oliver", "Jack", "Harry", "George", "Noah", "Charlie", "Jacob", "Oscar", "William", "Leo"] }, "FR": { first: ["Martin", "Bernard", "Dubois", "Thomas", "Robert", "Richard", "Petit", "Durand", "Leroy", "Moreau"], last: ["Lucas", "Louis", "Gabriel", "Arthur", "Jules", "Hugo", "Leo", "Adam", "Raphael", "Paul"] }, "DE": { first: ["Mueller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Schulz", "Hoffmann"], last: ["Ben", "Paul", "Leon", "Noah", "Luis", "Finn", "Felix", "Jonas", "Maximilian", "Henry"] }, "IT": { first: ["Rossi", "Ferrari", "Russo", "Bianchi", "Romano", "Gallo", "Costa", "Fontana", "Conti", "Esposito"], last: ["Leonardo", "Francesco", "Alessandro", "Lorenzo", "Matteo", "Andrea", "Gabriele", "Marco", "Antonio", "Giuseppe"] }, "ES": { first: ["Garcia", "Rodriguez", "Gonzalez", "Fernandez", "Lopez", "Martinez", "Sanchez", "Perez", "Martin", "Gomez"], last: ["Antonio", "Jose", "Manuel", "Francisco", "David", "Juan", "Miguel", "Javier", "Rafael", "Carlos"] }, "BR": { first: ["Silva", "Santos", "Oliveira", "Souza", "Rodrigues", "Ferreira", "Alves", "Pereira", "Lima", "Gomes"], last: ["Miguel", "Arthur", "Heitor", "Pedro", "Davi", "Gabriel", "Bernardo", "Lucas", "Matheus", "Rafael"] }, "RU": { first: ["Ivanov", "Smirnov", "Kuznetsov", "Popov", "Vasiliev", "Petrov", "Sokolov", "Mikhailov", "Fedorov", "Morozov"], last: ["Alexander", "Dmitry", "Maxim", "Ivan", "Andrey", "Mikhail", "Artem", "Daniel", "Roman", "Sergey"] }, "IN": { first: ["Kumar", "Singh", "Sharma", "Patel", "Gupta", "Shah", "Verma", "Rao", "Reddy", "Joshi"], last: ["Aarav", "Vihaan", "Vivaan", "Aditya", "Arjun", "Reyansh", "Ayaan", "Sai", "Krishna", "Ishaan"] }, "AU": { first: ["Smith", "Jones", "Williams", "Brown", "Wilson", "Taylor", "Johnson", "White", "Anderson", "Thompson"], last: ["Oliver", "William", "Jack", "Noah", "Thomas", "James", "Lucas", "Henry", "Ethan", "Alexander"] }, "CA": { first: ["Smith", "Brown", "Tremblay", "Martin", "Roy", "Wilson", "MacDonald", "Taylor", "Campbell", "Anderson"], last: ["Liam", "Noah", "Oliver", "William", "James", "Benjamin", "Lucas", "Henry", "Theodore", "Jack"] }, "MX": { first: ["Garcia", "Rodriguez", "Martinez", "Lopez", "Gonzalez", "Perez", "Sanchez", "Ramirez", "Torres", "Flores"], last: ["Santiago", "Mateo", "Sebastian", "Leonardo", "Diego", "Daniel", "Gabriel", "Adrian", "David", "Alexander"] }, "TR": { first: ["Yilmaz", "Kaya", "Demir", "Sahin", "Celik", "Yildiz", "Erdogan", "Ozturk", "Aydin", "Ozdemir"], last: ["Yusuf", "Eymen", "Ömer", "Mustafa", "Ali", "Mehmet", "Ahmet", "Emir", "Hamza", "Ibrahim"] }, "SA": { first: ["Al-Saud", "Al-Sheikh", "Al-Rashid", "Al-Qahtani", "Al-Ghamdi", "Al-Zahrani", "Al-Dossari", "Al-Shammari", "Al-Otaibi", "Al-Harbi"], last: ["Mohammed", "Abdullah", "Ahmed", "Ali", "Omar", "Ibrahim", "Khalid", "Hassan", "Fahad", "Abdul"] }, "AR": { first: ["Gonzalez", "Rodriguez", "Garcia", "Fernandez", "Lopez", "Martinez", "Perez", "Romero", "Sanchez", "Diaz"], last: ["Mateo", "Thiago", "Benjamin", "Valentino", "Santiago", "Juan", "Lucas", "Martin", "Nicolas", "Joaquin"] }, "EG": { first: ["Mohamed", "Ahmed", "Mahmoud", "Ibrahim", "Ali", "Hassan", "Hussein", "Mostafa", "Kamal", "Samir"], last: ["Omar", "Youssef", "Adam", "Malik", "Zain", "Hamza", "Kareem", "Hassan", "Ali", "Ibrahim"] }, "NG": { first: ["Okafor", "Adebayo", "Okonkwo", "Eze", "Oluwaseun", "Adegoke", "Afolabi", "Ogunleye", "Adeniyi", "Adesina"], last: ["Oluwadamilare", "Oluwatobiloba", "Ayomide", "Temitope", "Oluwaseun", "Adebayo", "Chibuike", "Chisom", "Chidi", "Obinna"] }, "ID": { first: ["Wijaya", "Kusuma", "Suryanto", "Halim", "Santoso", "Tanaka", "Wibowo", "Susanto", "Hidayat", "Putra"], last: ["Muhammad", "Ahmad", "Abdul", "Aditya", "Budi", "Dimas", "Eko", "Fajar", "Gading", "Hadi"] }, "ZA": { first: ["Nkosi", "Van der Merwe", "Botha", "Mkhize", "Khumalo", "Pretorius", "Venter", "Ndlovu", "Fourie", "Nel"], last: ["Bandile", "Themba", "Sipho", "Thabo", "Jabu", "Mandla", "Blessing", "Gift", "Lucky", "Precious"] }, "VN": { first: ["Nguyen", "Tran", "Le", "Pham", "Hoang", "Vu", "Do", "Dao", "Bui", "Dang"], last: ["Van", "Minh", "Thanh", "Ngoc", "Huu", "Quoc", "Xuan", "Duc", "Tuan", "Khanh"] } }; // 电话号码格式配置 const phoneFormats = { "US": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[201, 989]] }, "NY US": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[201, 989]] }, "California US": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[201, 989]] }, "Texas US": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[201, 989]] }, "CA US": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[201, 989]] }, "Illinois US": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[201, 989]] }, "CN": { format: "+86 1XX-XXXX-XXXX", mobilePrefix: ["30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "50", "51", "52", "53", "55", "56", "57", "58", "59", "66", "70", "71", "72", "73", "75", "76", "77", "78", "79", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89"] }, "JP": { format: "+81 XX-XXXX-XXXX", mobilePrefix: ["70", "80", "90"] }, "KR": { format: "+82 10-XXXX-XXXX" }, "UK": { format: "+44 7XXX XXXXXX", mobilePrefix: ["7"] }, "FR": { format: "+33 6 XX XX XX XX", mobilePrefix: ["6", "7"] }, "DE": { format: "+49 15X XXXXXXXX", mobilePrefix: ["15", "16", "17"] }, "TW": { format: "+886 9XX-XXX-XXX" }, "HK": { format: "+852 XXXX XXXX", mobilePrefix: ["5", "6", "9"] }, "AU": { format: "+61 4XX XXX XXX", mobilePrefix: ["4"] }, "CA": { format: "+1 (XXX) XXX-XXXX", areaCodeRanges: [[204, 989]] }, "MX": { format: "+52 1XX XXX XXXX" }, "TR": { format: "+90 5XX XXX XXXX", mobilePrefix: ["5"] }, "SA": { format: "+966 5XX XXX XXXX", mobilePrefix: ["5"] }, "AR": { format: "+54 9XX XXXX-XXXX" }, "EG": { format: "+20 1XX XXX XXXX", mobilePrefix: ["1"] }, "NG": { format: "+234 8XX XXX XXXX", mobilePrefix: ["7", "8", "9"] }, "ID": { format: "+62 8XX-XXXX-XXXX", mobilePrefix: ["8"] }, "ZA": { format: "+27 8X XXX XXXX", mobilePrefix: ["6", "7", "8"] }, "VN": { format: "+84 9X XXX XXXX", mobilePrefix: ["9", "8"] } }; // 工具函数 function getRandomLocation(country) { const coordsArray = countryCoordinates[country]; const randomCity = coordsArray[Math.floor(Math.random() * coordsArray.length)]; const lat = randomCity.lat + (Math.random() - 0.5) * 0.1; const lng = randomCity.lng + (Math.random() - 0.5) * 0.1; return { lat, lng }; } function getRandomName(country) { if (!namesByCountry[country]) { return null; } const names = namesByCountry[country]; const firstName = names.first[Math.floor(Math.random() * names.first.length)]; const lastName = names.last[Math.floor(Math.random() * names.last.length)]; return `${firstName} ${lastName}`; } function generateAreaCode(ranges) { const range = ranges[Math.floor(Math.random() * ranges.length)]; const [min, max] = range; return Math.floor(min + Math.random() * (max - min + 1)); } function getRandomPhoneNumber(country) { const format = phoneFormats[country] || phoneFormats["US"]; let phone = format.format; if (format.areaCodeRanges) { const areaCode = generateAreaCode(format.areaCodeRanges); phone = phone.replace("XXX", areaCode); phone = phone.replace(/X/g, () => Math.floor(Math.random() * 10)); } else if (format.mobilePrefix) { const prefix = format.mobilePrefix[Math.floor(Math.random() * format.mobilePrefix.length)]; // 先替换前缀 if (prefix.length === 2) { phone = phone.replace(/XX/, prefix); } else { phone = phone.replace(/X/, prefix); } // 然后替换剩余的X phone = phone.replace(/X/g, () => Math.floor(Math.random() * 10)); } else { phone = phone.replace(/X/g, () => Math.floor(Math.random() * 10)); } return phone; } function isValidAddress(data) { return data && data.address && data.address.house_number && data.address.road && (data.address.city || data.address.town); } // 处理CORS请求的headers const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, HEAD, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; // HTML 模板 const htmlContent = `<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>随机地址生成器</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet"> <script> tailwind.config = { theme: { extend: { animation: { 'gradient': 'gradient 8s linear infinite', }, keyframes: { gradient: { '0%, 100%': { 'background-size': '200% 200%', 'background-position': 'left center' }, '50%': { 'background-size': '200% 200%', 'background-position': 'right center' } } } } } } </script> </head> <link rel="shortcut icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>🌐</text></svg>"> <body class="bg-gradient-to-br from-blue-50 via-white to-blue-50 text-gray-800 min-h-screen font-['Noto_Sans_SC']"> <!-- 头部 --> <header class="bg-gradient-to-r from-blue-500 via-sky-500 to-blue-500 animate-gradient w-full p-6 shadow-lg"> <div class="max-w-4xl mx-auto flex items-center justify-between"> <div class="flex items-center gap-3"> <img src="https://img.freepik.com/premium-vector/minimal-location-map-icon-logo-symbol-vector-design-transparent_965979-613.jpg?w=2000" alt="Logo" class="w-12 h-12 transform hover:scale-105 transition-transform"> <h1 class="text-2xl font-bold text-white">随机地址生成器</h1> </div> <a href="https://www.199881.xyz" target="_blank" class="flex items-center gap-2 text-white hover:text-gray-200 transition-colors"> <svg viewBox="0 0 16 16" class="w-6 h-6 fill-current" aria-hidden="true"> <img src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/logo04.png?1741698119024" alt="GitHub" class="w-5 h-5 ml-1"> </svg> <span class="font-medium">导航</span> </a> </div> </header> <!-- 主要内容 --> <main class="container mx-auto px-4 py-8 max-w-5xl"> <!-- 加载动画 --> <div id="loading" class="hidden fixed inset-0 bg-white bg-opacity-75 backdrop-blur-sm flex items-center justify-center z-50"> <div class="bg-white rounded-2xl p-8 flex flex-col items-center shadow-2xl"> <div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-blue-500 mb-4"></div> <div class="text-gray-800 text-lg font-medium">正在加载...</div> </div> </div> <div id="copied" class="hidden fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-40 transform transition-transform duration-300"> 已复制到剪贴板! </div> <!-- 国家选择 --> <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200 mb-8"> <div class="flex flex-col sm:flex-row items-start sm:items-center gap-4"> <div class="w-full sm:w-auto flex flex-col sm:flex-row items-start sm:items-center gap-2 flex-grow"> <label for="country" class="text-blue-600 font-bold whitespace-nowrap">选择国家/地区:</label> <select id="country" onchange="changeCountry(this.value)" class="w-full bg-gray-50 border border-gray-200 rounded-xl p-3 text-gray-800 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"> </select> </div> <button onclick="generateNewAddress(document.getElementById('country').value)" class="w-full sm:w-auto bg-gradient-to-r from-blue-500 to-sky-500 hover:from-blue-600 hover:to-sky-600 text-white font-bold py-3 px-6 rounded-xl transition-all transform hover:scale-105 hover:shadow-lg focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"> 获取新地址 </button> </div> </div> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <!-- 左侧面板 --> <div class="space-y-6"> <!-- 信息卡片 --> <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200"> <h2 class="text-xl font-bold mb-6 text-blue-600">个人信息</h2> <div class="space-y-4"> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">姓名:</strong><span id="name" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">性别:</strong><span id="gender" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">电话:</strong><span id="phone" class="ml-2"></span> </div> <div class="bg-gray-50 p-4 rounded-xl cursor-pointer hover:bg-gray-100 transition-all transform hover:scale-[1.02] hover:shadow-lg" onclick="copyToClipboard(this.querySelector('span').textContent)"> <strong class="text-blue-600">地址:</strong><span id="address" class="ml-2"></span> </div> </div> </div> <!-- 保存按钮 --> <button onclick="saveAddress()" class="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600 hover:to-emerald-600 text-white font-bold py-3 px-6 rounded-xl transition-all transform hover:scale-105 hover:shadow-lg focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"> 保存地址 </button> </div> <!-- 右侧面板 --> <div class="space-y-6"> <!-- 地图 --> <div class="bg-white rounded-2xl shadow-xl p-6 border border-gray-200"> <h2 class="text-xl font-bold mb-6 text-blue-600">地图预览</h2> <iframe id="map" class="w-full h-[360px] rounded-xl border border-gray-200"></iframe> </div> </div> </div> <!-- 已保存的地址表格 --> <div class="mt-8 bg-white rounded-2xl shadow-xl p-6 border border-gray-200"> <h2 class="text-xl font-bold mb-6 text-blue-600">已保存的地址</h2> <div class="overflow-x-auto"> <table class="w-full border-collapse" id="savedAddressesTable"> <thead> <tr class="bg-gradient-to-r from-blue-500 to-sky-500 text-white"> <th class="p-4 text-left rounded-tl-lg">姓名</th> <th class="p-4 text-left">性别</th> <th class="p-4 text-left">电话</th> <th class="p-4 text-left">地址</th> <th class="p-4 text-left">备注</th> <th class="p-4 text-left rounded-tr-lg">操作</th> </tr> </thead> <tbody></tbody> </table> </div> </div> </main> <!-- 页脚 --> <footer class="text-center py-8 text-gray-600 text-sm mt-8 bg-gray-50 border-t border-gray-200"> <p class="max-w-4xl mx-auto px-4"> 总访问量 <span id="busuanzi_site_pv"></span> 次 | <span id="timeDate">载入天数...</span> | <a href="https://boke.199881.xyz/" target="_blank"> <span style="color: blue;">博客 | <a href="https://www.199881.xyz/" target="_blank"> <span style="color: green;">导航 <a href="https://github.com/jiangnan1224/AddressGenerator/" target="_blank" class="inline-flex items-center hover:text-blue-600 transition-colors"> <img src="https://pic.imgdb.cn/item/66e7ab36d9c307b7e9cefd24.png" alt="GitHub" class="w-5 h-5 ml-1"> </a> <script language="javascript"> var now = new Date(); function createtime(){ var grt= new Date("04/23/2025 00:00:00");/*---这里是网站的启用时间--*/ now.setTime(now.getTime()+250); days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days); document.getElementById("timeDate").innerHTML = "运行"+dnum+"天"; } setInterval("createtime()",250); </script> <script defer src="https://bsz.211119.xyz/js"></script> </p> </footer> <script> // 国家数据 const countries = [ { name: "美国", code: "US" }, { name: "美国纽约", code: "NY US" }, { name: "美国洛杉矶", code: "California US" }, { name: "美国达拉斯", code: "Texas US" }, { name: "美国圣何塞", code: "CA US" }, { name: "美国芝加哥", code: "Illinois US" }, { name: "英国", code: "UK" }, { name: "法国", code: "FR" }, { name: "德国", code: "DE" }, { name: "中国", code: "CN" }, { name: "中国台湾", code: "TW" }, { name: "中国香港", code: "HK" }, { name: "日本", code: "JP" }, { name: "越南", code: "VN" }, { name: "印度", code: "IN" }, { name: "澳大利亚", code: "AU" }, { name: "巴西", code: "BR" }, { name: "加拿大", code: "CA" }, { name: "俄罗斯", code: "RU" }, { name: "南非", code: "ZA" }, { name: "墨西哥", code: "MX" }, { name: "韩国", code: "KR" }, { name: "意大利", code: "IT" }, { name: "西班牙", code: "ES" }, { name: "土耳其", code: "TR" }, { name: "沙特阿拉伯", code: "SA" }, { name: "阿根廷", code: "AR" }, { name: "埃及", code: "EG" }, { name: "尼日利亚", code: "NG" }, { name: "印度尼西亚", code: "ID" } ]; // 初始化国家选择下拉框 function initCountrySelect() { const select = document.getElementById('country'); countries.forEach(country => { const option = document.createElement('option'); option.value = country.code; option.textContent = country.name; select.appendChild(option); }); } // 复制到剪贴板 function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { const copied = document.getElementById('copied'); copied.classList.remove('hidden'); copied.classList.add('translate-y-0'); copied.classList.remove('translate-y-[-100%]'); setTimeout(() => { copied.classList.add('translate-y-[-100%]'); copied.classList.remove('translate-y-0'); setTimeout(() => { copied.classList.add('hidden'); }, 300); }, 2000); }); } // 显示/隐藏加载动画 function toggleLoading(show) { const loading = document.getElementById('loading'); if (show) { loading.classList.remove('hidden'); } else { loading.classList.add('hidden'); } } // 更改国家 function changeCountry(country) { toggleLoading(true); generateNewAddress(country); } // 保存地址 function saveAddress() { const note = prompt('请输入备注(可以留空)') || ''; const savedAddresses = JSON.parse(localStorage.getItem('savedAddresses') || '[]'); const newEntry = { note: note, name: document.getElementById('name').textContent, gender: document.getElementById('gender').textContent, phone: document.getElementById('phone').textContent, address: document.getElementById('address').textContent }; savedAddresses.push(newEntry); localStorage.setItem('savedAddresses', JSON.stringify(savedAddresses)); renderSavedAddresses(); } // 渲染保存的地址 function renderSavedAddresses() { const savedAddresses = JSON.parse(localStorage.getItem('savedAddresses') || '[]'); const tbody = document.querySelector('#savedAddressesTable tbody'); tbody.innerHTML = ''; savedAddresses.forEach((entry, index) => { const row = tbody.insertRow(); row.className = 'border-t border-gray-200 hover:bg-gray-50 transition-colors'; const cells = ['name', 'gender', 'phone', 'address', 'note', '']; cells.forEach((cell, i) => { const td = row.insertCell(); td.className = 'p-4'; if (i === cells.length - 1) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.className = 'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-all transform hover:scale-105 focus:ring-2 focus:ring-red-500 focus:ring-opacity-50'; deleteBtn.onclick = () => { if (confirm('确定要删除这条记录吗?')) { savedAddresses.splice(index, 1); localStorage.setItem('savedAddresses', JSON.stringify(savedAddresses)); renderSavedAddresses(); } }; td.appendChild(deleteBtn); } else { td.textContent = entry[cell]; } }); }); } // 生成新地址 async function generateNewAddress(country) { if (!country) { country = document.getElementById('country').value; } toggleLoading(true); try { const response = await fetch(\`\${window.location.href}api?country=\${country}\`); const data = await response.json(); if (data.error) { alert(data.error); return; } document.getElementById('name').textContent = data.name; document.getElementById('gender').textContent = data.gender; document.getElementById('phone').textContent = data.phone; document.getElementById('address').textContent = data.address; // 更新地图 document.getElementById('map').src = \`https://www.google.com/maps?q=\${encodeURIComponent(data.address)}&output=embed\`; } catch (error) { console.error('Error fetching address:', error); alert('获取地址时发生错误,请重试'); } finally { toggleLoading(false); } } // 页面加载时初始化 window.onload = function() { initCountrySelect(); generateNewAddress(); renderSavedAddresses(); }; </script> </body> </html>`; async function handleRequest(request) { const url = new URL(request.url); const path = url.pathname; // 处理 API 请求 if (path === '/api') { // 原有的 API 处理逻辑 return handleApiRequest(request); } // 处理根路径请求,返回 HTML 页面 return new Response(htmlContent, { headers: { 'Content-Type': 'text/html;charset=UTF-8', ...corsHeaders } }); } // 将原有的处理逻辑移到单独的函数中 async function handleApiRequest(request) { if (request.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } if (request.method !== 'GET') { return new Response('Method not allowed', { status: 405 }); } const url = new URL(request.url); const country = url.searchParams.get('country') || 'US'; if (!countryCoordinates[country]) { return new Response(JSON.stringify({ error: 'Invalid country code' }), { status: 400, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } let attempts = 0; const maxAttempts = 20; while (attempts < maxAttempts) { try { const location = getRandomLocation(country); const apiUrl = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${location.lat}&lon=${location.lng}&zoom=18&addressdetails=1`; const response = await fetch(apiUrl, { headers: { 'User-Agent': 'Cloudflare Worker Random Address Generator' } }); const data = await response.json(); if (isValidAddress(data)) { let city = data.address.city || data.address.town || ''; city = city.split(';')[0].trim(); const address = `${data.address.house_number} ${data.address.road}, ${city}, ${data.address.postcode || ''}, ${country}`.replace(/\s+/g, ' ').trim(); const name = getRandomName(country); const gender = Math.random() > 0.4 ? 'Male男' : 'Female女'; const phone = getRandomPhoneNumber(country); const result = { name, gender, phone, address, coordinates: { lat: location.lat, lng: location.lng } }; return new Response(JSON.stringify(result), { headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } attempts++; if (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 100)); } } catch (error) { attempts++; if (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 100)); } } } return new Response(JSON.stringify({ error: 'Failed to generate valid address after multiple attempts' }), { status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } // 注册事件监听器 addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)); });
2025年07月16日
0 阅读
0 评论
0 点赞
2025-06-02
cf 部署 订阅转换工具
部署 订阅 转换工具 github项目 一、🚀 快速开始Fork本项目,点击上方Deploy to Cloudflare按钮在导入储存库栏选择你的仓库(你需要绑定Github账户)更改部署命令如下,选择保存并部署即可使用【构建命令,必须】npm run deploy二、功能特点1、支持协议:ShadowSocksVMessVLESSHysteria2TrojanTUIC2、核心功能支持导入 Base64 的 http/https 订阅链接以及多种协议的分享URL纯JavaScript + Cloudflare Worker实现,一键部署,开箱即用支持固定/随机短链接生成(基于 KV)浅色/深色主题切换灵活的 API,支持脚本化操作中文,英语,波斯语三语言支持3、客户端支持Sing-BoxClashXray/V2Ray4、Web 界面特性用户友好的操作界面提供多种预定义规则集可自建关于 geo-site、geo-ip、ip-cidr 和 domain-suffix 的自定义策略组主要端点/singbox - 生成 Sing-Box 配置/clash - 生成 Clash 配置/xray - 生成 Xray 配置/shorten - 生成短链接
2025年06月02日
0 阅读
0 评论
0 点赞
2025-05-28
Sub-Hub 基于 Cloudflare Worker+D1数据库的简洁订阅管理器,摒弃第三方订阅转换!
Sub-Hub 基于 Cloudflare Worker+D1数据库的简洁订阅管理器,摒弃第三方订阅转换! github项目地址 Sub-Hub 是一个基于 Cloudflare Workers 的代理节点订阅管理系统。它提供了一个直观的 Web 界面,让您可以轻松管理多个订阅和节点基于Cloudflare Worker 搭建,不需要借助VPS进行部署支持原始格式,BASE64格式,Surge格式、Clash格式(内置模板,需要使用自己的规则可以按需修改)支持SS、VMess、VLESS(除Surge)、Trojan、SOCKS5、Snell(仅Surge)、Hysteria2、Tuic 格式节点的托管本项目不使用任何第三方订阅转换,所以可能有部分协议转换不完整,目前支持的协议经过测试没发现太大问题基于Cursor纯AI代码 越来越屎山了,有问题可以提,但不一定能解决功能特点 🚀 支持多种代理协议SS(Shadowsocks)SS2022(Shadowsocks 2022)VMessTrojanVLESS(除 Surge 外)SOCKS5Snell(仅 Surge)Hysteria2Tuic订阅管理 创建多个独立订阅自定义订阅路径支持批量导入节点节点拖拽排序多种订阅格式原始格式(适用于大多数客户端)Base64 编码格式(/v2ray 路径)Surge 配置格式(/surge 路径)Clash 配置格式(/clash 路径)(内置Clash模板)安全特性管理面板登录认证会话管理安全的 Cookie 设置🎨 现代化界面响应式设计直观的操作界面支持移动设备{dotted startColor="#ff6c6c" endColor="#1989fa"/}创建项目创建名为“sub-hub”新的 Workers 项目创建名为“sub-hub” 的D1 数据库将D1数据库与Cloudflare Workers绑定变量名称 = "DB" 数据库名称 = "sub-hub"初始化数据库,在名为“sub-hub” 的D1 数据库“控制台中执行如下代码”-- 创建订阅表CREATE TABLE IF NOT EXISTS subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, path TEXT NOT NULL UNIQUE );-- 创建节点表CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, subscription_id INTEGER NOT NULL, name TEXT NOT NULL, original_link TEXT NOT NULL, node_order INTEGER NOT NULL DEFAULT 0, enabled INTEGER DEFAULT 1, FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE );-- 创建会话表CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, username TEXT NOT NULL, expires_at INTEGER NOT NULL );-- 创建索引CREATE INDEX IF NOT EXISTS idx_subscriptions_path ON subscriptions(path); CREATE INDEX IF NOT EXISTS idx_nodes_subscription_order ON nodes(subscription_id, node_order); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);下面是原初始化数据库方法-- 创建订阅表CREATE TABLE IF NOT EXISTS subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, path TEXT NOT NULL UNIQUE );-- 创建节点表CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, subscription_id INTEGER NOT NULL, name TEXT NOT NULL, original_link TEXT NOT NULL, node_order INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE );-- 创建会话表CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, username TEXT NOT NULL, expires_at INTEGER NOT NULL );-- 创建索引CREATE INDEX IF NOT EXISTS idx_subscriptions_path ON subscriptions(path); CREATE INDEX IF NOT EXISTS idx_nodes_subscription_order ON nodes(subscription_id, node_order); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);配置环境变量,不变在 Cloudflare Dashboard 中设置以下环境变量:ADMIN_PATH: 管理面板路径(默认:admin)ADMIN_USERNAME: 管理员用户名(默认:admin)ADMIN_PASSWORD: 管理员密码(默认:password)部署代码将 “worker.js” 文件内容复制到Cloudflare Workers保存【20250710更新】新增禁用节点功能,被禁用的节点将不显示在订阅链接中A、此次更新需要添加新字段 enabled 到 nodes 表, 或者重新部署数据库ALTER TABLE nodes ADD COLUMN enabled INTEGER DEFAULT 1;这个命令会在表中添加 enabled 字段,并设置默认值为 1。但是请注意:这个默认值只适用于将来插入的新记录。现有记录不会自动更新。B、更新现有记录的 enabled 值为 1UPDATE nodes SET enabled = 1 WHERE enabled IS NULL;这个命令会将当前表中所有 enabled 为空的记录设置为 1。新文件【20250710更新】// 通用的路径验证和节点名称提取函数 function validateSubscriptionPath(path) { return /^[a-z0-9-]{5,50}$/.test(path); } // 节点类型常量定义 const NODE_TYPES = { SS: 'ss://', VMESS: 'vmess://', TROJAN: 'trojan://', VLESS: 'vless://', SOCKS: 'socks://', HYSTERIA2: 'hysteria2://', TUIC: 'tuic://', SNELL: 'snell,' }; function extractNodeName(nodeLink) { if (!nodeLink) return '未命名节点'; // 处理snell节点 if(nodeLink.includes(NODE_TYPES.SNELL)) { const name = nodeLink.split('=')[0].trim(); return name || '未命名节点'; } // 处理 VMess 链接 if (nodeLink.toLowerCase().startsWith(NODE_TYPES.VMESS)) { try { const config = JSON.parse(safeBase64Decode(nodeLink.substring(8))); if (config.ps) { return safeUtf8Decode(config.ps); } } catch {} return '未命名节点'; } // 处理其他使用哈希标记名称的链接类型(SS、TROJAN、VLESS、SOCKS、Hysteria2、TUIC等) const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } export default { async fetch(request, env) { // 解析请求路径和参数 const url = new URL(request.url); const pathname = url.pathname; const method = request.method; // 检查是否有查询参数 if (url.search && !pathname.startsWith('/admin')) { return new Response('Not Found', { status: 404 }); } // 从环境变量获取配置路径,如果未设置则使用默认值 const adminPath = env.ADMIN_PATH || 'admin'; // 从环境变量获取登录凭据 const adminUsername = env.ADMIN_USERNAME || 'admin'; const adminPassword = env.ADMIN_PASSWORD || 'password'; // 处理登录页面请求 if (pathname === `/${adminPath}/login`) { if (method === "GET") { return serveLoginPage(adminPath); } else if (method === "POST") { return handleLogin(request, env, adminUsername, adminPassword, adminPath); } } // 处理登出请求 if (pathname === `/${adminPath}/logout`) { return handleLogout(request, env, adminPath); } // 处理管理面板请求 if (pathname === `/${adminPath}`) { const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return Response.redirect(`${url.origin}/${adminPath}/login`, 302); } return serveAdminPanel(env, adminPath); } // 处理API请求 if (pathname.startsWith(`/${adminPath}/api/`)) { // 验证会话 const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return new Response(JSON.stringify({ success: false, message: '未授权访问' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // 处理节点管理API请求 const nodeApiMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)/nodes(?:/([^/]+|reorder))?$`)); if (nodeApiMatch) { const subscriptionPath = nodeApiMatch[1]; const nodeId = nodeApiMatch[2]; try { // 更新节点顺序 if (nodeId === 'reorder' && method === 'POST') { const { orders } = await request.json(); if (!Array.isArray(orders) || orders.length === 0) { return new Response(JSON.stringify({ success: false, message: '无效的排序数据' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return new Response(JSON.stringify({ success: false, message: '订阅不存在' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } const subscriptionId = subResults[0].id; // 使用事务来确保数据一致性 const statements = []; // 准备更新语句 for (const { id, order } of orders) { statements.push(env.DB.prepare( "UPDATE nodes SET node_order = ? WHERE id = ? AND subscription_id = ?" ).bind(order, id, subscriptionId)); } // 执行批量更新 const result = await env.DB.batch(statements); return new Response(JSON.stringify({ success: true, message: '节点顺序已更新' }), { headers: { 'Content-Type': 'application/json' } }); } // 获取节点列表 if (!nodeId && method === 'GET') { return handleGetNodes(env, subscriptionPath); } // 创建新节点 if (!nodeId && method === 'POST') { return handleCreateNode(request, env, subscriptionPath); } // 更新节点 if (nodeId && nodeId !== 'reorder' && method === 'PUT') { return handleUpdateNode(request, env, subscriptionPath, nodeId); } // 删除节点 if (nodeId && nodeId !== 'reorder' && method === 'DELETE') { return handleDeleteNode(env, subscriptionPath, nodeId); } // 切换节点状态 if (nodeId && nodeId !== 'reorder' && method === 'PATCH') { return handleToggleNode(env, subscriptionPath, nodeId, request); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('API请求处理失败:', error); return new Response(JSON.stringify({ success: false, message: error.message || '服务器内部错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 处理订阅管理API请求 if (pathname.startsWith(`/${adminPath}/api/subscriptions`)) { // 获取单个订阅内容 const getOneMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (getOneMatch && method === 'GET') { return handleGetSubscription(env, getOneMatch[1]); } // 获取订阅列表 if (pathname === `/${adminPath}/api/subscriptions` && method === 'GET') { return handleGetSubscriptions(env); } // 创建新订阅 if (pathname === `/${adminPath}/api/subscriptions` && method === 'POST') { try { const { name, path } = await request.json(); if (!name || !validateSubscriptionPath(path)) { return createErrorResponse('无效的参数', 400); } // 检查路径是否已存在 const { results } = await env.DB.prepare( "SELECT COUNT(*) as count FROM subscriptions WHERE path = ?" ).bind(path).all(); if (results[0].count > 0) { return createErrorResponse('该路径已被使用', 400); } // 创建订阅 const result = await env.DB.prepare( "INSERT INTO subscriptions (name, path) VALUES (?, ?)" ).bind(name, path).run(); if (!result.success) { throw new Error('创建订阅失败'); } return createSuccessResponse(null, '订阅创建成功'); } catch (error) { console.error('创建订阅失败:', error); return createErrorResponse('创建订阅失败: ' + error.message); } } // 更新订阅信息 const updateMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (updateMatch && method === 'PUT') { const data = await request.json(); return handleUpdateSubscriptionInfo(env, updateMatch[1], data); } // 删除订阅 const deleteMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (deleteMatch && method === 'DELETE') { return handleDeleteSubscription(env, deleteMatch[1]); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ success: false, message: 'Not Found' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } // 处理订阅请求 if (pathname.startsWith('/')) { // 检查路径格式是否合法(只允许一级或两级路径,如 /path 或 /path/surge 或 /path/v2ray 或 /path/clash) const pathParts = pathname.split('/').filter(Boolean); if (pathParts.length > 2) { return new Response('Not Found', { status: 404 }); } if (pathParts.length === 2 && !['surge', 'v2ray', 'clash'].includes(pathParts[1])) { return new Response('Not Found', { status: 404 }); } try { // 获取基本路径 let basePath = pathname; if (pathname.endsWith('/surge')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/surge } else if (pathname.endsWith('/v2ray')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/v2ray } else if (pathname.endsWith('/clash')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/clash } else { basePath = pathname.slice(1); // 只移除开头的/ } // 获取订阅信息 const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(basePath).all(); const subscription = results[0]; if (subscription) { // 生成订阅内容 const content = await generateSubscriptionContent(env, basePath); // 根据请求路径返回不同格式的内容 if (pathname.endsWith('/surge')) { // 返回 Surge 格式 const surgeContent = convertToSurge(content); return new Response(surgeContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/v2ray')) { // 返回 Base64 编码格式,排除 snell 节点,包括 VLESS 节点 const filteredContent = filterSnellNodes(content); const base64Content = safeBase64Encode(filteredContent); return new Response(base64Content, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/clash')) { // 返回 Clash 格式 const clashContent = convertToClash(content); return new Response(clashContent, { headers: { 'Content-Type': 'text/yaml; charset=utf-8' }, }); } // 返回普通订阅内容,排除 snell 节点 const filteredContent = filterSnellNodes(content); return new Response(filteredContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } } catch (error) { console.error('处理订阅请求失败:', error); return new Response('Internal Server Error', { status: 500 }); } // 如果没有找到匹配的订阅,返回404 return new Response('Not Found', { status: 404 }); } // 其他所有路径返回 404 return new Response('Not Found', { status: 404 }); }, }; // 添加获取单个订阅的处理函数 async function handleGetSubscription(env, path) { try { const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(path).all(); if (!results || results.length === 0) { return createErrorResponse('订阅不存在', 404); } return createSuccessResponse(results[0]); } catch (error) { console.error('获取订阅内容失败:', error); return createErrorResponse('获取订阅内容失败: ' + error.message); } } // 提供登录页面HTML function serveLoginPage(adminPath) { const html = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sub-Hub - 登录</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/all.min.css"> <style> :root { --primary-color: #4e73df; --success-color: #1cc88a; --danger-color: #e74a3b; --transition-timing: 0.25s cubic-bezier(0.4, 0, 0.2, 1); --box-shadow-light: 0 2px 10px rgba(0,0,0,0.05); --box-shadow-medium: 0 4px 15px rgba(0,0,0,0.08); --font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; --text-color: #2d3748; --text-color-secondary: #444; --border-radius-sm: 8px; --border-radius-md: 12px; --border-radius-lg: 16px; } * { transition: all var(--transition-timing); font-family: var(--font-family); } html { scrollbar-gutter: stable; } /* 防止模态框打开时页面偏移 */ .modal-open { padding-right: 0 !important; } /* 修复模态框背景遮罩的宽度 */ .modal-backdrop { width: 100vw !important; } /* 优化模态框布局 */ .modal-dialog { margin-right: auto !important; margin-left: auto !important; padding-right: 0 !important; } /* 标题和重要文字样式 */ .navbar-brand, .subscription-name, .modal-title, .form-label { font-weight: 600; color: var(--text-color); } /* 按钮统一样式 */ .btn, .logout-btn { font-weight: 500; } /* 次要文字样式 */ .link-label, .form-text { color: var(--text-color-secondary); } /* 链接标签文字加粗 */ .link-label small > span > span { font-weight: 600; } body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; } .login-container { background-color: #fff; border-radius: 16px; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); overflow: hidden; width: 360px; max-width: 90%; } .login-header { background: linear-gradient(120deg, var(--primary-color), #224abe); padding: 2rem 1.5rem; text-align: center; color: white; } .login-icon { background-color: white; color: var(--primary-color); width: 80px; height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); } .login-icon i { font-size: 2.5rem; } .login-title { font-weight: 600; margin-bottom: 0.5rem; } .login-subtitle { opacity: 0.8; font-size: 0.9rem; } .login-form { padding: 2rem; } .form-floating { margin-bottom: 1.5rem; } .form-floating input { border-radius: 8px; height: 56px; border: 2px solid #e7eaf0; box-shadow: none; } .form-floating input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 0.25rem rgba(78, 115, 223, 0.15); } .form-floating label { color: #7e7e7e; padding-left: 1rem; } .btn-login { background: linear-gradient(120deg, var(--primary-color), #224abe); border: none; border-radius: 8px; padding: 0.75rem 1.5rem; font-weight: 600; width: 100%; margin-top: 1rem; box-shadow: 0 4px 10px rgba(78, 115, 223, 0.35); transition: all 0.2s ease; } .btn-login:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(78, 115, 223, 0.4); } .alert { border-radius: 8px; font-size: 0.9rem; margin-bottom: 1.5rem; display: none; } </style> </head> <body> <div class="login-container"> <div class="login-header"> <div class="login-icon"> <i class="fas fa-cube"></i> </div> <h3 class="login-title">Sub-Hub</h3> <p class="login-subtitle">请登录以继续使用</p> </div> <div class="login-form"> <div class="alert alert-danger" id="loginAlert" role="alert"> <i class="fas fa-exclamation-triangle me-2"></i> <span id="alertMessage">用户名或密码错误</span> </div> <form id="loginForm"> <div class="form-floating"> <input type="text" class="form-control" id="username" name="username" placeholder="用户名" required> <label for="username"><i class="fas fa-user me-2"></i>用户名</label> </div> <div class="form-floating"> <input type="password" class="form-control" id="password" name="password" placeholder="密码" required> <label for="password"><i class="fas fa-lock me-2"></i>密码</label> </div> <button type="submit" class="btn btn-primary btn-login"> <i class="fas fa-sign-in-alt me-2"></i>登录 </button> </form> </div> </div> <script> document.getElementById('loginForm').addEventListener('submit', async function(e) { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const response = await fetch('/${adminPath}/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }), }); const data = await response.json(); if (data.success) { // 登录成功,重定向到管理面板 window.location.href = data.redirect; } else { // 显示错误消息 document.getElementById('alertMessage').textContent = data.message; document.getElementById('loginAlert').style.display = 'block'; } } catch (error) { // 显示错误消息 document.getElementById('alertMessage').textContent = '登录请求失败,请重试'; document.getElementById('loginAlert').style.display = 'block'; } }); </script> </body> </html>`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 验证会话 async function verifySession(request, env) { const sessionId = getSessionFromCookie(request); if (!sessionId) return false; const now = Date.now(); const { results } = await env.DB.prepare(` UPDATE sessions SET expires_at = ? WHERE session_id = ? AND expires_at > ? RETURNING * `).bind(now + 24 * 60 * 60 * 1000, sessionId, now).all(); return results.length > 0; } // 从Cookie中获取会话ID function getSessionFromCookie(request) { const cookieHeader = request.headers.get('Cookie') || ''; const sessionCookie = cookieHeader.split(';') .find(cookie => cookie.trim().startsWith('session=')); return sessionCookie ? sessionCookie.trim().substring(8) : null; } // 生成安全的会话令牌 async function generateSecureSessionToken(username, env) { // 清理过期会话和用户旧会话 const now = Date.now(); await env.DB.batch([ env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?").bind(now), env.DB.prepare("DELETE FROM sessions WHERE username = ?").bind(username) ]); const sessionId = crypto.randomUUID(); const expiresAt = now + 24 * 60 * 60 * 1000; // 24小时后过期 await env.DB.prepare(` INSERT INTO sessions (session_id, username, expires_at) VALUES (?, ?, ?) `).bind(sessionId, username, expiresAt).run(); return sessionId; } // 处理登录请求 async function handleLogin(request, env, adminUsername, adminPassword, adminPath) { const { username, password } = await request.json(); if (!username || !password || username !== adminUsername || password !== adminPassword) { return new Response(JSON.stringify({ success: false, message: '用户名或密码错误' }), { headers: { 'Content-Type': 'application/json' }, status: 401 }); } const sessionId = await generateSecureSessionToken(username, env); const headers = new Headers({ 'Content-Type': 'application/json', 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400; Secure` }); return new Response(JSON.stringify({ success: true, message: '登录成功', redirect: `/${adminPath}` }), { headers }); } // 处理登出请求 async function handleLogout(request, env, adminPath) { const sessionId = getSessionFromCookie(request); if (sessionId) { await env.DB.prepare("DELETE FROM sessions WHERE session_id = ?").bind(sessionId).run(); } const headers = new Headers({ 'Set-Cookie': `session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0; Secure` }); return Response.redirect(`${new URL(request.url).origin}/${adminPath}/login`, 302); } // 修改管理面板HTML生成函数 function serveAdminPanel(env, adminPath) { const html = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sub-Hub</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/all.min.css"> <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script> <style> :root { --primary-color: #4e73df; --success-color: #1cc88a; --danger-color: #e74a3b; --transition-timing: 0.25s cubic-bezier(0.4, 0, 0.2, 1); --box-shadow-light: 0 2px 10px rgba(0,0,0,0.05); --box-shadow-medium: 0 4px 15px rgba(0,0,0,0.08); --font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; --text-color: #2d3748; --text-color-secondary: #444; --border-radius-sm: 8px; --border-radius-md: 12px; --border-radius-lg: 16px; } * { transition: all var(--transition-timing); font-family: var(--font-family); } html { scrollbar-gutter: stable; } body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; background-color: #f8f9fc; padding: 20px 0; overflow-y: scroll; } /* 防止模态框打开时页面偏移 */ .modal-open { padding-right: 0 !important; } /* 修复模态框背景遮罩的宽度 */ .modal-backdrop { width: 100vw !important; } /* 优化模态框布局 */ .modal-dialog { margin-right: auto !important; margin-left: auto !important; padding-right: 0 !important; } /* 标题和重要文字样式 */ .navbar-brand, .subscription-name, .modal-title, .form-label { font-weight: 600; color: var(--text-color); } /* 按钮统一样式 */ .btn, .logout-btn { font-weight: 500; } /* 次要文字样式 */ .subscription-meta, .link-label, .form-text { color: var(--text-color-secondary); } /* 链接标签文字加粗 */ .link-label small > span > span { font-weight: 600; } .navbar { background-color: white; box-shadow: var(--box-shadow-light); padding: 1rem 1.5rem; margin-bottom: 0.5rem; height: 80px; display: flex; align-items: center; transform: translateY(0); } .navbar:hover { box-shadow: var(--box-shadow-medium); } .navbar-brand { font-weight: bold; color: #000; display: flex; align-items: center; font-size: 1.8rem; } .navbar-brand i { font-size: 2.2rem; margin-right: 0.8rem; color: #000; } .navbar-user { display: flex; align-items: center; } .navbar-user .user-name { font-weight: 500; } .logout-btn { color: #4a4a4a; background: none; border: none; margin-left: 1rem; padding: 0.375rem 0.75rem; border-radius: 0.25rem; text-decoration: none; font-weight: 500; } .logout-btn:hover { background-color: #f8f9fa; color: var(--danger-color); text-decoration: none; } .logout-btn:focus { text-decoration: none; outline: none; } .container { max-width: 1100px; } textarea { border: 1px solid #e5e7eb; border-radius: 8px; resize: vertical; min-height: 300px; max-height: 800px; height: 300px; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 0.9rem; line-height: 1.5; padding: 12px; } .link-label { display: flex; flex-direction: column; margin-top: 1rem; color: #444; font-size: 0.9rem; } .link-label small { display: flex; flex-direction: column; gap: 0.75rem; } .link-label small > span { display: flex; align-items: center; gap: 0.25rem; font-size: 0.9rem; } .link-label small > span > span { display: inline-block; width: 90px; color: var(--text-color); font-weight: 600; text-align: justify; text-align-last: justify; font-size: 0.9rem; } .link-label a { margin-left: 0; color: var(--primary-color); text-decoration: none; word-break: break-all; font-weight: 700; font-size: 0.9rem; } .link-label .link:hover { text-decoration: underline; } .btn-group { display: flex; gap: 0.5rem; margin-top: 1rem; } .toast-container { position: fixed; top: 1rem; right: 1rem; z-index: 1050; } @media (max-width: 767px) { .btn-group { flex-direction: column; } } .subscription-list { margin-bottom: 2rem; } .subscription-item { background: #fff; border-radius: var(--border-radius-md); padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: var(--box-shadow-light); border: 1px solid #eaeaea; overflow: hidden; transition: margin-bottom 0.5s cubic-bezier(0.4, 0, 0.2, 1); position: relative; } .edit-button { position: absolute; top: 1rem; right: 1rem; z-index: 1; } .edit-button .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; } .subscription-item:hover { box-shadow: var(--box-shadow-medium); } .subscription-edit-area { max-height: 0; overflow: hidden; opacity: 0; transform: translateY(-10px); padding: 0; margin: 0; will-change: max-height, opacity, transform, padding, margin; } .subscription-edit-area.active { max-height: 300px; opacity: 1; transform: translateY(0); padding: 1rem; margin-top: 0.5rem; } .subscription-edit-area textarea { width: 100%; height: 200px; min-height: 200px; max-height: 200px; margin-bottom: 1rem; padding: 1rem; border: 1px solid #e5e7eb; border-radius: var(--border-radius-sm); font-size: 0.9rem; line-height: 1.5; resize: none; box-shadow: var(--box-shadow-light); opacity: 0; transform: translateY(5px); } .subscription-edit-area.active textarea { opacity: 1; transform: translateY(0); } .subscription-edit-actions { display: flex; gap: 0.5rem; justify-content: flex-end; opacity: 0; transform: translateY(5px); } .subscription-edit-area.active .subscription-edit-actions { opacity: 1; transform: translateY(0); transition-delay: 0.05s; } .toast { transform: translateY(0); } .toast.hide { transform: translateY(-100%); } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .subscription-item { animation: fadeIn 0.5s ease-out; } .path-error { color: #dc3545; font-size: 0.875em; margin-top: 0.25rem; display: none; } .form-control.is-invalid { border-color: #dc3545; padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .form-control.is-valid { border-color: #198754; padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .subscription-name { font-size: 1.5rem; font-weight: 600; color: #2d3748; margin-bottom: 0.75rem; line-height: 1.2; } .node-count { display: inline-flex; align-items: center; color: var(--text-color-secondary); font-size: 1rem; margin-left: 0.75rem; } .node-count i { margin-right: 0.35rem; font-size: 1rem; } .subscription-actions { margin-top: 1.25rem; padding-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; } .subscription-actions .btn { width: 100%; padding: 0.5rem 1.25rem; font-weight: 500; border-radius: 8px; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; display: flex; align-items: center; justify-content: center; } .subscription-actions .btn i { margin-right: 0.5rem; } /* 操作按钮样式 */ .node-actions { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; gap: 4px; flex-shrink: 0; } .node-actions .btn { padding: 4px 8px !important; min-width: 32px !important; height: 28px !important; display: inline-flex !important; align-items: center; justify-content: center; margin: 0 !important; } .node-actions .btn i { font-size: 14px; line-height: 1; margin: 0 !important; } .node-checkbox { vertical-align: top; margin: 0; margin-top: 1.5px; } /* 移动端适配样式 */ @media (max-width: 767px) { /* 基础布局调整 */ .container { padding: 0 15px; } .navbar { margin-bottom: 2rem; padding: 0.8rem 1rem; } .btn-group { flex-direction: column; } /* 订阅项样式调整 */ .subscription-item { padding: 1rem; } .subscription-item > div { flex-direction: column; align-items: flex-start !important; } .subscription-item .link-label { margin: 1rem 0 0; width: 100%; } .subscription-actions { margin-top: 1rem; width: 100%; flex-direction: row !important; gap: 0.5rem; } .subscription-actions .btn { flex: 1; font-size: 0.875rem; padding: 0.5rem; } .subscription-actions .btn i { margin-right: 0.25rem; font-size: 0.875rem; } /* 模态框调整 */ .modal-dialog { margin: 0.5rem; } .modal-content { border-radius: 1rem; } /* Toast提示调整 */ .toast-container { right: 0; left: 0; bottom: 1rem; top: auto; margin: 0 1rem; } .toast { width: 100%; } /* 节点列表移动端优化 */ .table { table-layout: fixed; width: 100%; margin-bottom: 0 !important; } .table thead th:first-child { padding-left: 1rem !important; width: 100% !important; } .table thead th:last-child { display: none !important; } .node-row td { padding: 0.5rem !important; } .node-row td:first-child { font-size: 0.85rem; padding-right: 0 !important; width: calc(100% - 90px) !important; } .node-row td:first-child .text-truncate { max-width: 100% !important; width: 100% !important; padding-right: 8px !important; } .node-row td:last-child { padding-left: 0 !important; width: 90px !important; position: relative !important; } .node-row td:last-child .text-truncate { display: none !important; } /* 节点操作按钮移动端优化 */ .node-actions { margin: 0 !important; gap: 2px !important; justify-content: flex-end !important; width: 90px !important; position: absolute !important; right: 4px !important; flex-wrap: nowrap !important; align-items: center !important; } .node-actions .btn { padding: 2px 4px !important; min-width: 28px !important; height: 28px !important; margin: 0 !important; } .node-actions .btn i { font-size: 12px; line-height: 1; display: flex !important; align-items: center !important; justify-content: center !important; } /* 批量操作按钮移动端优化 */ .batch-action-buttons { flex-wrap: wrap !important; gap: 0.25rem !important; width: 100% !important; } .batch-action-buttons .btn { font-size: 0.75rem !important; padding: 0.25rem 0.5rem !important; margin: 0 !important; flex: 1 !important; min-width: 0 !important; white-space: nowrap !important; } .batch-action-buttons .btn i { font-size: 0.75rem !important; margin-right: 0.25rem !important; } } /* 模态框按钮样式 */ .modal .btn-secondary { background-color: #e2e8f0; border-color: #e2e8f0; color: var(--text-color); } .modal .btn-secondary:hover { background-color: #cbd5e0; border-color: #cbd5e0; } /* 添加通用按钮样式 */ /* 按钮基础样式 */ .action-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.625rem 1.25rem; font-size: 1rem; font-weight: 500; border-radius: 8px; transition: all 0.2s ease; min-width: 140px; height: 42px; gap: 0.5rem; line-height: 1; } .action-btn i { font-size: 1rem; display: inline-flex; align-items: center; justify-content: center; width: 1rem; height: 1rem; } /* 按钮颜色变体 */ .action-btn.btn-primary, .btn-primary { background-color: var(--primary-color); border-color: var(--primary-color); color: white; } .action-btn.btn-success, .btn-success { background-color: var(--success-color); border-color: var(--success-color); color: white; } .btn-edit { background-color: #1cc88a; border-color: #1cc88a; color: white; } /* 按钮悬停效果 */ .action-btn:hover, .btn-edit:hover { transform: translateY(-1px); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); } .btn-edit:hover { background-color: #19b57c; border-color: #19b57c; } .btn-edit:focus { background-color: #19b57c; border-color: #19b57c; box-shadow: 0 0 0 0.25rem rgba(28, 200, 138, 0.25); } .action-btn:active { transform: translateY(0); } /* 调整容器样式 */ .container { max-width: 1100px; padding: 2rem 1rem; } .d-flex.justify-content-between.align-items-center.mb-4 { margin-bottom: 2rem !important; } /* 节点列表区域样式 */ .node-list-area { max-height: 0; overflow: hidden; transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1); background: #fff; border-radius: 0 0 var(--border-radius-md) var(--border-radius-md); will-change: max-height; font-size: 0.875rem; color: var(--text-color); } .node-list-area.expanded { max-height: none; overflow: visible; } .node-list-area .table-responsive { transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); transform-origin: top; } .node-list-area .link { font-size: 0.875rem; color: var(--text-color-secondary); text-decoration: none; } .node-list-area .link:hover { text-decoration: underline; } </style> </head> <body> <nav class="navbar navbar-expand navbar-light bg-white"> <div class="container"> <a class="navbar-brand" href="#"> <i class="fas fa-cube"></i> Sub-Hub </a> <div class="navbar-user ms-auto"> <a href="/${adminPath}/logout" class="logout-btn"> <i class="fas fa-sign-out-alt me-1"></i> 退出 </a> </div> </div> </nav> <div class="container"> <div class="subscription-item" style="padding-bottom: 0; border: none; box-shadow: none; margin-bottom: 1rem; background: transparent;"> <div class="d-flex justify-content-end"> <button class="btn btn-primary action-btn" data-bs-toggle="modal" data-bs-target="#addSubscriptionModal"> <i class="fas fa-plus"></i> <span>添加订阅</span> </button> </div> </div> <!-- 订阅列表 --> <div class="subscription-list" id="subscriptionList"> <!-- 订阅项将通过JavaScript动态添加 --> </div> </div> <!-- 添加订阅模态框 --> <div class="modal fade" id="addSubscriptionModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">添加新订阅</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="addSubscriptionForm" onsubmit="return false;"> <div class="mb-3"> <label class="form-label">订阅名称 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="name" required> <div class="invalid-feedback">请输入订阅名称</div> </div> <div class="mb-3"> <label class="form-label">订阅路径 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="path" required pattern="^[a-z0-9-]+$" minlength="5" maxlength="50"> <div class="form-text">路径只能包含小写字母、数字和连字符,长度在5-50个字符之间</div> <div class="path-error text-danger"></div> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="createSubscription()">创建</button> </div> </div> </div> </div> <!-- 修改添加节点模态框 --> <div class="modal fade" id="addNodeModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">添加节点</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="addNodeForm" onsubmit="return false;"> <input type="hidden" name="subscriptionPath" value=""> <div class="mb-3"> <label class="form-label">节点内容 <span class="text-danger">*</span></label> <textarea class="form-control" name="content" rows="6" required placeholder="请输入节点内容,支持以下格式: 1. ss://... 2. vless://...(除Surge) 3. vmess://... 4. trojan://... 5. socks://... 6. hysteria2://... 7. tuic://... 8. snell格式(仅Surge) 9. Base64编码格式"></textarea> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="createNode()"> <i class="fas fa-plus me-1"></i>添加 </button> </div> </div> </div> </div> <!-- 编辑名称和路径模态框 --> <div class="modal fade" id="editNameModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">编辑订阅信息</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="editNameForm" onsubmit="return false;"> <input type="hidden" name="originalPath"> <div class="mb-3"> <label class="form-label">订阅名称 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="name" required> <div class="invalid-feedback">请输入订阅名称</div> </div> <div class="mb-3"> <label class="form-label">订阅路径 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="path" required> <div class="form-text">路径只能包含小写字母、数字和连字符,长度在5-50个字符之间</div> <div class="path-error text-danger"></div> </div> </form> </div> <div class="modal-footer justify-content-between"> <button type="button" class="btn btn-danger" onclick="confirmDelete()"> <i class="fas fa-trash"></i> 删除订阅 </button> <div> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="updateSubscriptionInfo()">保存</button> </div> </div> </div> </div> </div> <!-- 编辑节点模态框 --> <div class="modal fade" id="editNodeModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">编辑节点</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="editNodeForm" onsubmit="return false;"> <input type="hidden" name="subscriptionPath" value=""> <input type="hidden" name="nodeId" value=""> <div class="mb-3"> <label class="form-label">节点内容 <span class="text-danger">*</span></label> <textarea class="form-control" name="content" rows="6" required placeholder="请输入节点内容,支持以下格式: 1. ss://... 2. vless://...(除Surge) 3. vmess://... 4. trojan://... 5. socks://... 6. hysteria2://... 7. tuic://... 8. snell格式(仅Surge) 9. Base64编码格式"></textarea> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="updateNode()">保存</button> </div> </div> </div> </div> <!-- Toast提示 --> <div class="toast-container"> <div id="toast" class="toast align-items-center text-white bg-success" role="alert" aria-live="assertive" aria-atomic="true"> <div class="d-flex"> <div class="toast-body" id="toastMessage"> 操作成功! </div> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script> // 定义 adminPath 变量 const adminPath = '${adminPath}'; // 优化的loadSubscriptions函数 async function loadSubscriptions() { try { const response = await fetch('/' + adminPath + '/api/subscriptions'); if (!response.ok) throw new Error('加载失败'); const result = await response.json(); if (!result.success) { throw new Error(result.message || '加载失败'); } const subscriptions = result.data || []; const listElement = document.getElementById('subscriptionList'); listElement.innerHTML = ''; const fragment = document.createDocumentFragment(); for (const sub of subscriptions) { const item = document.createElement('div'); item.className = 'subscription-item'; item.innerHTML = \` <div class="d-flex justify-content-between align-items-start"> <button class="btn btn-sm btn-link text-primary edit-button" onclick="showEditNameModal('\${sub.path}', '\${sub.name}')"> <i class="fas fa-edit"></i> </button> <div class="subscription-content"> <div class="d-flex align-items-center"> <h5 class="subscription-name mb-1">\${sub.name}</h5> <span class="node-count ms-2"><i class="fas fa-server"></i>\${sub.nodeCount}</span> </div> <div class="link-label"> <small> <span> <span class="address-label">订阅地址1:</span> <a href="/\${sub.path}" target="_blank">/\${sub.path}</a> </span> <span> <span class="address-label">订阅地址2:</span> <a href="/\${sub.path}/v2ray" target="_blank">/\${sub.path}/v2ray</a> </span> <span> <span class="address-label">订阅地址3:</span> <a href="/\${sub.path}/surge" target="_blank">/\${sub.path}/surge</a> </span> <span> <span class="address-label">订阅地址4:</span> <a href="/\${sub.path}/clash" target="_blank">/\${sub.path}/clash</a> </span> </small> </div> </div> <div class="subscription-actions"> <button class="btn btn-success action-btn" onclick="showAddNodeModal('\${sub.path}')"> <i class="fas fa-plus"></i> <span>添加节点</span> </button> <button class="btn btn-primary action-btn" onclick="showNodeList('\${sub.path}')"> <i class="fas fa-list"></i> <span>节点列表</span> </button> </div> </div> <div class="node-list-area" id="node-list-\${sub.path}"> <div class="table-responsive mt-3"> <table class="table"> <thead> <tr> <th style="min-width: 300px; width: 35%; padding-left: 0.75rem;">节点名称</th> <th style="padding-left: 4.5rem;">节点链接</th> </tr> </thead> <tbody></tbody> </table> <div class="d-flex justify-content-between align-items-center mt-3 px-2 pb-2"> <div class="batch-operation-buttons" id="batch-operation-buttons-\${sub.path}"> <button class="btn btn-primary btn-sm rounded-3" onclick="enterBatchMode('\${sub.path}')" id="batch-mode-btn-\${sub.path}" style="padding: 0.5rem 1rem;"> <i class="fas fa-tasks me-1"></i>批量操作 </button> </div> <div class="batch-action-buttons" id="batch-action-buttons-\${sub.path}" style="display: none;"> <button class="btn btn-success btn-sm rounded-3 me-2" onclick="executeBatchStatusChange('\${sub.path}', true)" style="padding: 0.5rem 1rem;"> <i class="fas fa-toggle-on me-1"></i>启用 </button> <button class="btn btn-warning btn-sm rounded-3 me-2" onclick="executeBatchStatusChange('\${sub.path}', false)" style="padding: 0.5rem 1rem;"> <i class="fas fa-toggle-off me-1"></i>禁用 </button> <button class="btn btn-danger btn-sm rounded-3 me-2" onclick="executeBatchDelete('\${sub.path}')" style="padding: 0.5rem 1rem;"> <i class="fas fa-trash-alt me-1"></i>删除 </button> <button class="btn btn-outline-secondary btn-sm rounded-3 me-2" onclick="toggleSelectAll('\${sub.path}')" id="select-all-btn-\${sub.path}" style="padding: 0.5rem 1rem;"> <i class="fas fa-check-square me-1"></i>全选 </button> <button class="btn btn-secondary btn-sm rounded-3" onclick="exitBatchMode('\${sub.path}')" style="padding: 0.5rem 1rem;"> <i class="fas fa-times me-1"></i>取消 </button> </div> </div> </div> </div> \`; fragment.appendChild(item); } listElement.appendChild(fragment); } catch (error) { showToast('加载订阅列表失败: ' + error.message, 'danger'); } } // 页面加载完成后,加载订阅列表 window.addEventListener('load', loadSubscriptions); // 进入批量操作模式 function enterBatchMode(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); const checkboxes = nodeListArea.querySelectorAll('.node-checkbox'); const operationButtons = document.getElementById('batch-operation-buttons-' + subscriptionPath); const actionButtons = document.getElementById('batch-action-buttons-' + subscriptionPath); // 显示勾选框 checkboxes.forEach(checkbox => { checkbox.style.display = 'inline-block'; }); // 切换按钮显示 operationButtons.style.display = 'none'; actionButtons.style.display = 'flex'; // 标记为批量模式 nodeListArea.classList.add('batch-mode'); // 重置所有勾选框 checkboxes.forEach(cb => { cb.checked = false; }); // 重置全选按钮状态 resetSelectAllButton(subscriptionPath); showToast('请选择要操作的节点,然后选择对应的操作', 'info'); } // 退出批量操作模式 function exitBatchMode(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); const checkboxes = nodeListArea.querySelectorAll('.node-checkbox'); const operationButtons = document.getElementById('batch-operation-buttons-' + subscriptionPath); const actionButtons = document.getElementById('batch-action-buttons-' + subscriptionPath); // 隐藏勾选框 checkboxes.forEach(checkbox => { checkbox.style.display = 'none'; }); // 切换按钮显示 operationButtons.style.display = 'flex'; actionButtons.style.display = 'none'; // 取消批量模式标记 nodeListArea.classList.remove('batch-mode'); } // 重置全选按钮状态 function resetSelectAllButton(subscriptionPath) { const selectAllBtn = document.getElementById('select-all-btn-' + subscriptionPath); if (selectAllBtn) { selectAllBtn.innerHTML = '<i class="fas fa-check-square me-1"></i>全选'; } } // 全选/取消全选功能 function toggleSelectAll(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); const checkboxes = nodeListArea.querySelectorAll('.node-checkbox'); const selectAllBtn = document.getElementById('select-all-btn-' + subscriptionPath); // 检查当前是否为全选状态 const checkedCount = nodeListArea.querySelectorAll('.node-checkbox:checked').length; const isAllSelected = checkedCount === checkboxes.length && checkboxes.length > 0; // 切换选择状态 checkboxes.forEach(checkbox => { checkbox.checked = !isAllSelected; }); // 更新按钮状态 if (isAllSelected) { selectAllBtn.innerHTML = '<i class="fas fa-check-square me-1"></i>全选'; } else { selectAllBtn.innerHTML = '<i class="fas fa-minus-square me-1"></i>取消'; } } // 执行批量删除 async function executeBatchDelete(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); const checkedNodes = nodeListArea.querySelectorAll('.node-checkbox:checked'); if (checkedNodes.length === 0) { showToast('请选择要删除的节点', 'warning'); return; } if (!confirm(\`确定要删除选中的 \${checkedNodes.length} 个节点吗?\`)) { return; } try { let successCount = 0; let failCount = 0; // 显示进度提示 showToast('正在删除节点...', 'info'); // 批量删除所选节点 for (const checkbox of checkedNodes) { const nodeId = checkbox.value; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (response.ok) { successCount++; } else { failCount++; } } catch (error) { console.error('删除节点失败:', error); failCount++; } } // 显示删除结果 if (failCount === 0) { showToast(\`成功删除 \${successCount} 个节点\`, 'success'); } else { showToast(\`删除完成:成功 \${successCount} 个,失败 \${failCount} 个\`, 'warning'); } // 退出批量操作模式 exitBatchMode(subscriptionPath); // 重新加载节点列表和订阅信息 await loadNodeList(subscriptionPath); await loadSubscriptions(); } catch (error) { console.error('批量删除失败:', error); showToast('批量删除失败: ' + error.message, 'danger'); } }; // 执行批量状态切换 async function executeBatchStatusChange(subscriptionPath, enabled) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); const checkedNodes = nodeListArea.querySelectorAll('.node-checkbox:checked'); if (checkedNodes.length === 0) { showToast('请先选择要操作的节点', 'warning'); return; } const action = enabled ? '启用' : '禁用'; if (!confirm('确定要' + action + '选中的 ' + checkedNodes.length + ' 个节点吗?')) { return; } try { let successCount = 0; let failCount = 0; // 显示进度提示 showToast('正在' + action + '节点...', 'info'); // 批量操作所选节点 for (const checkbox of checkedNodes) { const nodeId = checkbox.value; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ enabled }) }); if (response.ok) { successCount++; } else { failCount++; } } catch (error) { console.error('操作节点失败:', error); failCount++; } } // 显示操作结果 if (failCount === 0) { showToast('成功' + action + ' ' + successCount + ' 个节点', 'success'); } else { showToast(action + '完成:成功 ' + successCount + ' 个,失败 ' + failCount + ' 个', 'warning'); } // 退出批量操作模式 exitBatchMode(subscriptionPath); // 重新加载节点列表和订阅信息 await loadNodeList(subscriptionPath); await loadSubscriptions(); } catch (error) { console.error('批量操作失败:', error); showToast('批量' + action + '失败: ' + error.message, 'danger'); } } // 显示添加节点模态框 function showAddNodeModal(subscriptionPath) { const modal = document.getElementById('addNodeModal'); const form = document.getElementById('addNodeForm'); // 重置表单 form.reset(); // 设置订阅路径 const pathInput = form.querySelector('[name="subscriptionPath"]'); if (pathInput) { pathInput.value = subscriptionPath; } // 显示模态框 const modalInstance = new bootstrap.Modal(modal); modalInstance.show(); } // 修改创建节点函数 async function createNode() { try { const form = document.getElementById('addNodeForm'); if (!form) { throw new Error('找不到表单元素'); } const formData = new FormData(form); const subscriptionPath = formData.get('subscriptionPath'); const name = formData.get('name')?.trim(); let content = formData.get('content')?.trim(); if (!subscriptionPath) { throw new Error('缺少订阅路径'); } if (!content) { throw new Error('请填写节点内容'); } // 分割成行 const lines = content.split(/\\r?\\n/); const validNodes = []; // 处理每一行 for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // 检查是否是Base64编码的完整配置 try { const decodedContent = safeBase64DecodeFrontend(trimmedLine); // 如果解码成功,检查是否包含多个节点 const decodedLines = decodedContent.split(/\\r?\\n/); for (const decodedLine of decodedLines) { if (decodedLine.trim() && isValidNodeLink(decodedLine.trim())) { validNodes.push({ name: name || extractNodeNameFrontend(decodedLine.trim()), content: decodedLine.trim() }); } } } catch (e) { // 如果不是Base64编码,直接检查是否是有效的节点链接 if (isValidNodeLink(trimmedLine)) { validNodes.push({ name: name || extractNodeNameFrontend(trimmedLine), content: trimmedLine }); } } } if (validNodes.length === 0) { throw new Error('未找到有效的节点链接'); } // 批量创建节点,按顺序添加 const results = []; const timestamp = Date.now(); // 使用时间戳作为基础序号 for (let i = 0; i < validNodes.length; i++) { const node = validNodes[i]; try { // 创建节点,使用时间戳+索引作为顺序值 const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ name: node.name, content: node.content, type: getNodeType(node.content), order: timestamp + i // 使用时间戳确保顺序唯一且递增 }) }); const result = await response.json(); results.push({ success: response.ok, message: result.message, link: node.content, order: i }); } catch (error) { results.push({ success: false, message: error.message, link: node.content, order: i }); } } // 按原始顺序排序结果 results.sort((a, b) => a.order - b.order); // 统计结果 const successful = results.filter(r => r.success).length; const failed = results.filter(r => !r.success).length; // 显示结果 if (validNodes.length === 1) { showToast(successful > 0 ? '节点添加成功' : '节点添加失败', successful > 0 ? 'success' : 'danger'); } else { showToast(\`添加完成:成功 \${successful} 个,失败 \${failed} 个\`, successful > 0 ? 'success' : 'warning'); } // 关闭模态框 const modal = document.getElementById('addNodeModal'); if (modal) { const modalInstance = bootstrap.Modal.getInstance(modal); if (modalInstance) { modalInstance.hide(); } } // 重置表单 form.reset(); // 刷新订阅列表 await loadSubscriptions(); if (successful > 0) { await loadNodeList(subscriptionPath); } } catch (error) { console.error('添加节点失败:', error); showToast('添加节点失败: ' + error.message, 'danger'); } } // 提取节点名称(前端版本 - 复用后端实现) function extractNodeNameFrontend(nodeLink) { if (!nodeLink) return '未命名节点'; // 处理snell节点 if(nodeLink.includes(NODE_TYPES_FRONTEND.SNELL)) { const name = nodeLink.split('=')[0].trim(); return name || '未命名节点'; } // 处理 VMess 链接 if (nodeLink.toLowerCase().startsWith(NODE_TYPES_FRONTEND.VMESS)) { try { const config = JSON.parse(safeBase64DecodeFrontend(nodeLink.substring(8))); if (config.ps) { return safeUtf8DecodeFrontend(config.ps); } } catch {} return '未命名节点'; } // 处理其他使用哈希标记名称的链接类型(SS、TROJAN、VLESS、SOCKS、Hysteria2、TUIC等) const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } // 节点类型常量定义(复用后端定义) const NODE_TYPES_FRONTEND = { SS: 'ss://', VMESS: 'vmess://', TROJAN: 'trojan://', VLESS: 'vless://', SOCKS: 'socks://', HYSTERIA2: 'hysteria2://', TUIC: 'tuic://', SNELL: 'snell,' }; // 检查是否是有效的节点链接 function isValidNodeLink(link) { const lowerLink = link.toLowerCase(); // 检查snell格式 if(lowerLink.includes('=') && lowerLink.includes('snell,')) { const parts = link.split('=')[1]?.trim().split(','); return parts && parts.length >= 4 && parts[0].trim() === 'snell'; } return Object.values(NODE_TYPES_FRONTEND).some(prefix => lowerLink.startsWith(prefix)); } // 获取节点类型 function getNodeType(link) { const lowerLink = link.toLowerCase(); if(lowerLink.includes('=') && lowerLink.includes('snell,')) { return 'snell'; } return Object.entries(NODE_TYPES_FRONTEND).find(([key, prefix]) => lowerLink.startsWith(prefix) )?.[0].toLowerCase() || ''; } // 安全的UTF-8字符串解码函数(前端版本 - 复用后端实现) function safeUtf8DecodeFrontend(str) { if (!str) return str; try { // 方法1:使用escape + decodeURIComponent return decodeURIComponent(escape(str)); } catch (e1) { try { // 方法2:直接使用decodeURIComponent return decodeURIComponent(str); } catch (e2) { try { // 方法3:使用TextDecoder(如果支持) if (typeof TextDecoder !== 'undefined') { const encoder = new TextEncoder(); const decoder = new TextDecoder('utf-8'); return decoder.decode(encoder.encode(str)); } } catch (e3) { // 如果所有方法都失败,返回原始字符串 return str; } return str; } } } // 显示节点列表 async function showNodeList(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); if (!nodeListArea) { console.error('找不到节点列表区域'); return; } const isHidden = !nodeListArea.classList.contains('expanded'); let expandedSubs = JSON.parse(localStorage.getItem('expandedSubscriptions') || '[]'); if (isHidden) { // 立即添加展开类名触发动画 nodeListArea.classList.add('expanded'); // 更新展开状态 if (!expandedSubs.includes(subscriptionPath)) { expandedSubs.push(subscriptionPath); localStorage.setItem('expandedSubscriptions', JSON.stringify(expandedSubs)); } // 同时开始加载数据 loadNodeList(subscriptionPath).catch(error => { console.error('加载节点列表失败:', error); showToast('加载节点列表失败: ' + error.message, 'danger'); }); } else { nodeListArea.classList.remove('expanded'); expandedSubs = expandedSubs.filter(path => path !== subscriptionPath); localStorage.setItem('expandedSubscriptions', JSON.stringify(expandedSubs)); } } // 修改加载节点列表函数 async function loadNodeList(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); if (!nodeListArea) { throw new Error('找不到节点列表区域'); } const tbody = nodeListArea.querySelector('tbody'); // 先显示加载中的提示 tbody.innerHTML = '<tr><td colspan="2" class="text-center py-4"><i class="fas fa-spinner fa-spin me-2"></i>加载中...</td></tr>'; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes'); if (!response.ok) throw new Error('加载失败'); const result = await response.json(); if (!result.success) { throw new Error(result.message || '加载失败'); } const nodes = result.data || []; // 构建节点列表HTML const nodesHtml = nodes.map(node => { const nodeLink = node.original_link; const escapedNodeLink = nodeLink .replace(/&/g, '&') .replace(/'/g, '\\\'') .replace(/"/g, '\\"'); const isEnabled = node.enabled === 1; return \` <tr class="node-row" data-id="\${node.id}" data-order="\${node.node_order}" data-enabled="\${isEnabled ? '1' : '0'}"> <td class="align-middle"> <div class="d-flex align-items-center"> <input class="node-checkbox me-2" type="checkbox" value="\${node.id}" data-subscription="\${subscriptionPath}" style="display: none;"> <div class="text-nowrap text-truncate \${!isEnabled ? 'text-danger' : ''}" style="max-width: 320px; \${!isEnabled ? 'text-decoration: line-through;' : ''}" title="\${node.name}"> \${node.name} </div> </div> </td> <td class="align-middle"> <div class="d-flex justify-content-between align-items-center" style="gap: 8px;"> <div class="text-nowrap text-truncate \${!isEnabled ? 'text-danger' : ''}" style="max-width: 400px; margin-left: 4rem; \${!isEnabled ? 'text-decoration: line-through;' : ''}" title="\${nodeLink}"> \${nodeLink} </div> <div class="node-actions d-flex" style="flex-shrink: 0; gap: 4px;"> <button class="btn btn-sm btn-edit" onclick="showEditNodeModal('\${subscriptionPath}', '\${node.id}', '\${escapedNodeLink}')" title="编辑节点"> <i class="fas fa-edit"></i> </button> <button class="btn btn-sm btn-primary" onclick="copyToClipboard('\${escapedNodeLink}')" title="复制链接"> <i class="fas fa-copy"></i> </button> <button class="btn btn-sm btn-danger" onclick="deleteNode('\${subscriptionPath}', \${node.id})" title="删除节点"> <i class="fas fa-trash"></i> </button> </div> </div> </td> </tr> \`; }).join(''); // 更新节点列表内容 tbody.innerHTML = nodesHtml || '<tr><td colspan="2" class="text-center py-4">暂无节点</td></tr>'; // 初始化拖拽排序 if (nodes.length > 0) { // 检查是否为移动设备 const isMobile = window.innerWidth <= 767; // 只在非移动设备上初始化排序 if (!isMobile) { initializeSortable(tbody, subscriptionPath); } } } catch (error) { tbody.innerHTML = \`<tr><td colspan="2" class="text-center py-4 text-danger"> <i class="fas fa-exclamation-circle me-2"></i>\${error.message} </td></tr>\`; throw error; } } // 初始化拖拽排序功能 function initializeSortable(tbody, subscriptionPath) { new Sortable(tbody, { animation: 150, handle: '.node-row', ghostClass: 'sortable-ghost', dragClass: 'sortable-drag', onEnd: async function(evt) { try { const rows = Array.from(tbody.querySelectorAll('.node-row')); const newOrders = rows.map((row, index) => ({ id: parseInt(row.dataset.id), order: index })); await updateNodeOrder(subscriptionPath, newOrders); showToast('节点排序已更新'); } catch (error) { console.error('更新节点排序失败:', error); showToast('更新节点排序失败: ' + error.message, 'danger'); // 重新加载列表以恢复原始顺序 await loadNodeList(subscriptionPath); } } }); } // 删除节点 async function deleteNode(subscriptionPath, nodeId) { if (!confirm('确定要删除这个节点吗?')) return; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '删除失败'); } showToast('节点已删除'); await loadNodeList(subscriptionPath); await loadSubscriptions(); } catch (error) { showToast('删除节点失败: ' + error.message, 'danger'); } } // 添加创建订阅函数 async function createSubscription() { const form = document.getElementById('addSubscriptionForm'); const formData = new FormData(form); const name = formData.get('name').trim(); const path = formData.get('path').trim(); if (!name) { showToast('请输入订阅名称', 'danger'); form.querySelector('[name="name"]').focus(); return; } if (!path || !validateSubscriptionPathFrontend(path)) { const pathInput = form.querySelector('[name="path"]'); pathInput.classList.add('is-invalid'); pathInput.classList.remove('is-valid'); form.querySelector('.path-error').textContent = '路径格式不正确'; form.querySelector('.path-error').style.display = 'block'; pathInput.focus(); return; } try { const response = await fetch('/' + adminPath + '/api/subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ name, path }) }); const result = await response.json(); if (!response.ok) throw new Error(result.message || '创建失败'); showToast('订阅创建成功'); bootstrap.Modal.getInstance(document.getElementById('addSubscriptionModal')).hide(); form.reset(); await loadSubscriptions(); } catch (error) { showToast('创建失败: ' + error.message, 'danger'); } } // 添加显示Toast提示函数 function showToast(message, type = 'success') { const toast = document.getElementById('toast'); const toastMessage = document.getElementById('toastMessage'); // 设置消息 toastMessage.textContent = message; // 设置类型 toast.className = toast.className.replace(/bg-\w+/, ''); toast.classList.add('bg-' + type); // 显示Toast const bsToast = new bootstrap.Toast(toast); bsToast.show(); } // 显示编辑名称模态框 function showEditNameModal(path, name) { const form = document.getElementById('editNameForm'); form.querySelector('input[name="originalPath"]').value = path; form.querySelector('input[name="name"]').value = name; form.querySelector('input[name="path"]').value = path; const modal = new bootstrap.Modal(document.getElementById('editNameModal')); modal.show(); } // 更新订阅信息 async function updateSubscriptionInfo() { const form = document.getElementById('editNameForm'); const originalPath = form.querySelector('input[name="originalPath"]').value; const nameInput = form.querySelector('input[name="name"]'); const pathInput = form.querySelector('input[name="path"]'); const pathError = form.querySelector('.path-error'); try { // 验证输入 if (!nameInput.value.trim()) { showToast('请输入订阅名称', 'danger'); nameInput.focus(); return; } // 验证路径格式 const path = pathInput.value.trim(); if (!validateSubscriptionPathFrontend(path)) { pathInput.classList.add('is-invalid'); pathInput.classList.remove('is-valid'); pathError.textContent = '路径格式不正确'; pathError.style.display = 'block'; pathInput.focus(); return; } // 如果路径被修改,检查新路径是否已存在 if (path !== originalPath) { const checkResponse = await fetch('/' + adminPath + '/api/subscriptions/' + path); if (checkResponse.ok) { pathInput.classList.add('is-invalid'); pathInput.classList.remove('is-valid'); pathError.textContent = '该路径已被使用'; pathError.style.display = 'block'; pathInput.focus(); return; } } const response = await fetch('/' + adminPath + '/api/subscriptions/' + originalPath, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ name: nameInput.value.trim(), path: path, action: 'update_info' }) }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '更新失败'); } showToast('订阅信息已更新'); bootstrap.Modal.getInstance(document.getElementById('editNameModal')).hide(); await loadSubscriptions(); } catch (error) { console.error('更新订阅信息失败:', error); showToast('更新失败: ' + error.message, 'danger'); } } // 添加确认删除函数 async function confirmDelete() { try { const form = document.getElementById('editNameForm'); const path = form.querySelector('input[name="originalPath"]').value; if (!path) { showToast('无效的订阅路径', 'danger'); return; } if (!confirm('确定要删除这个订阅吗?')) { return; } const response = await fetch('/' + adminPath + '/api/subscriptions/' + path, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { const result = await response.json(); throw new Error(result.message || '删除失败'); } // 关闭编辑模态框 const editModal = bootstrap.Modal.getInstance(document.getElementById('editNameModal')); if (editModal) { editModal.hide(); } showToast('订阅已删除'); await loadSubscriptions(); } catch (error) { console.error('删除失败:', error); showToast('删除失败: ' + error.message, 'danger'); } } // 表单验证工具函数(复用后端实现) function validateSubscriptionPathFrontend(path) { return /^[a-z0-9-]{5,50}$/.test(path); } // 添加复制到剪贴板函数 function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { showToast('已复制到剪贴板'); }).catch(() => { showToast('复制失败', 'danger'); }); } // 显示编辑节点模态框 function showEditNodeModal(subscriptionPath, nodeId, nodeContent) { const modal = document.getElementById('editNodeModal'); const form = document.getElementById('editNodeForm'); if (!modal || !form) { showToast('显示编辑模态框失败:找不到必要的页面元素', 'danger'); return; } // 设置表单值 form.querySelector('[name="subscriptionPath"]').value = subscriptionPath; form.querySelector('[name="nodeId"]').value = nodeId; form.setAttribute('data-original-content', nodeContent); form.querySelector('[name="content"]').value = nodeContent; // 显示模态框 new bootstrap.Modal(modal).show(); } // 更新节点 async function updateNode() { const form = document.getElementById('editNodeForm'); const formData = new FormData(form); const subscriptionPath = formData.get('subscriptionPath'); const nodeId = formData.get('nodeId'); const content = formData.get('content')?.trim(); const originalContent = form.getAttribute('data-original-content'); if (!subscriptionPath || !nodeId || !content) { showToast('请填写完整的节点信息', 'danger'); return; } // 检查内容是否被修改 if (content === originalContent) { showToast('节点内容未修改'); bootstrap.Modal.getInstance(document.getElementById('editNodeModal'))?.hide(); return; } try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ content }) }); const result = await response.json(); if (!response.ok) throw new Error(result.message || '更新失败'); showToast('节点更新成功'); bootstrap.Modal.getInstance(document.getElementById('editNodeModal'))?.hide(); form.reset(); // 刷新数据 await Promise.all([ loadSubscriptions(), loadNodeList(subscriptionPath) ]); } catch (error) { showToast('更新节点失败: ' + error.message, 'danger'); } } // 修改前端的排序处理代码 async function updateNodeOrder(subscriptionPath, orders) { try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ orders }) }); const result = await response.json(); if (!result.success) { throw new Error(result.message || '保存排序失败'); } return result; } catch (error) { console.error('更新节点排序失败:', error); } finally { // 无论成功还是失败都重新加载节点列表 await loadNodeList(subscriptionPath); } } // 安全的Base64解码函数(前端版本 - 复用后端实现) function safeBase64DecodeFrontend(str) { try { // 方法1:使用atob解码,然后正确处理UTF-8编码 const decoded = atob(str); // 将解码结果转换为UTF-8字符串 const bytes = []; for (let i = 0; i < decoded.length; i++) { bytes.push(decoded.charCodeAt(i)); } // 使用TextDecoder正确解码UTF-8字符 if (typeof TextDecoder !== 'undefined') { const decoder = new TextDecoder('utf-8'); return decoder.decode(new Uint8Array(bytes)); } else { // 如果没有TextDecoder,使用escape + decodeURIComponent let utf8String = ''; for (let i = 0; i < bytes.length; i++) { utf8String += String.fromCharCode(bytes[i]); } return decodeURIComponent(escape(utf8String)); } } catch (e) { try { // 方法2:直接使用atob,可能适用于简单ASCII字符 return atob(str); } catch (e2) { // 如果Base64解码失败,返回原字符串 return str; } } } </script> </body> </html>`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 通用响应头 const JSON_HEADERS = { 'Content-Type': 'application/json' }; // 通用响应创建函数 function createResponse(success, message, data = null, status = success ? 200 : 500) { return new Response( JSON.stringify({ success, message, ...(data && { data }) }), { headers: JSON_HEADERS, status } ); } // 错误响应函数 const createErrorResponse = (message, status = 500) => createResponse(false, message, null, status); // 成功响应函数 const createSuccessResponse = (data = null, message = '操作成功') => createResponse(true, message, data); // 修改获取订阅列表函数 async function handleGetSubscriptions(env) { const { results } = await env.DB.prepare(` SELECT s.path, s.name, COUNT(n.id) as nodeCount FROM subscriptions s LEFT JOIN nodes n ON s.id = n.subscription_id GROUP BY s.id ORDER BY s.id ASC `).all(); const subscriptions = results.map(item => ({ name: item.name, path: item.path, nodeCount: item.nodeCount || 0 })); return createSuccessResponse(subscriptions); } // 添加获取节点列表的函数 async function handleGetNodes(env, subscriptionPath) { const { results } = await env.DB.prepare(` SELECT n.id, n.name, n.original_link, n.node_order, COALESCE(n.enabled, 1) as enabled FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? ORDER BY n.node_order ASC `).bind(subscriptionPath).all(); return createSuccessResponse(results || []); } // 添加创建节点的函数 async function handleCreateNode(request, env, subscriptionPath) { const nodeData = await request.json(); if (!nodeData.content) { return createErrorResponse('缺少节点内容', 400); } const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.trim(); // 尝试Base64解码 try { const decodedContent = safeBase64Decode(originalLink); // 检查解码后的内容是否是有效的节点链接 if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.trim(); } } catch (e) { // 不是Base64格式,继续使用原始内容 } // 验证节点类型 const lowerContent = originalLink.toLowerCase(); const isSnell = lowerContent.includes('=') && lowerContent.includes('snell,'); if (!['ss://', 'vmess://', 'trojan://', 'vless://', 'socks://', 'hysteria2://', 'tuic://'].some(prefix => lowerContent.startsWith(prefix)) && !isSnell) { return createErrorResponse('不支持的节点格式', 400); } // 从节点链接中提取名称 let nodeName = extractNodeName(originalLink); // 直接使用提供的order值 const nodeOrder = nodeData.order; // 直接插入新节点,不更新其他节点的顺序 await env.DB.prepare(` INSERT INTO nodes (subscription_id, name, original_link, node_order, enabled) VALUES (?, ?, ?, ?, 1) `).bind(subscriptionId, nodeName, originalLink, nodeOrder).run(); return createSuccessResponse(null, '节点创建成功'); } // 添加删除节点的函数 async function handleDeleteNode(env, subscriptionPath, nodeId) { try { const result = await env.DB.prepare(` DELETE FROM nodes WHERE id = ? AND subscription_id = ( SELECT id FROM subscriptions WHERE path = ? LIMIT 1 ) `).bind(nodeId, subscriptionPath).run(); return createSuccessResponse(null, '节点已删除'); } catch (error) { return createErrorResponse('删除节点失败: ' + error.message); } } // 添加切换节点状态的函数 async function handleToggleNode(env, subscriptionPath, nodeId, request) { try { const { enabled } = await request.json(); // 验证enabled值 if (typeof enabled !== 'boolean') { return createErrorResponse('无效的状态值', 400); } // 更新节点状态 const result = await env.DB.prepare(` UPDATE nodes SET enabled = ? WHERE id = ? AND subscription_id = ( SELECT id FROM subscriptions WHERE path = ? LIMIT 1 ) `).bind(enabled ? 1 : 0, nodeId, subscriptionPath).run(); if (result.changes === 0) { return createErrorResponse('节点不存在或更新失败', 404); } return createSuccessResponse(null, '节点已' + (enabled ? '启用' : '禁用')); } catch (error) { return createErrorResponse('切换节点状态失败: ' + error.message); } } // 生成订阅内容 async function generateSubscriptionContent(env, path) { if (!path?.trim()) return ''; const { results } = await env.DB.prepare(` SELECT GROUP_CONCAT(n.original_link, CHAR(10)) as content FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? AND n.original_link IS NOT NULL AND (n.enabled IS NULL OR n.enabled = 1) GROUP BY s.id ORDER BY n.node_order ASC `).bind(path).all(); return results?.[0]?.content || ''; } // 解析SIP002格式 function parseSIP002Format(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; const nodeName = name ? decodeURIComponent(name) : '未命名节点'; return `${nodeName} = ss, ${server}, ${port}, encrypt-method=${method}, password=${password}`; } catch { return null; } } // 解析Vmess链接为Surge格式 function parseVmessLink(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; // 正确处理UTF-8编码的中文字符 const name = config.ps ? safeUtf8Decode(config.ps) : '未命名节点'; const configParts = [ `${name} = vmess`, config.add, config.port, `username=${config.id}`, 'vmess-aead=true', `tls=${config.tls === 'tls'}`, `sni=${config.add}`, 'skip-cert-verify=true', 'tfo=false' ]; if (config.tls === 'tls' && config.alpn) { configParts.push(`alpn=${config.alpn.replace(/,/g, ':')}`); } if (config.net === 'ws') { configParts.push('ws=true'); if (config.path) configParts.push(`ws-path=${config.path}`); configParts.push(`ws-headers=Host:${config.host || config.add}`); } return configParts.join(', '); } catch { return null; } } // 解析Trojan链接为Surge格式 function parseTrojanLink(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const configParts = [ `${nodeName} = trojan`, url.hostname, url.port, `password=${url.username}`, 'tls=true', `sni=${url.hostname}`, 'skip-cert-verify=true', 'tfo=false' ]; const alpn = params.get('alpn'); if (alpn) { configParts.push(`alpn=${safeDecodeURIComponent(alpn).replace(/,/g, ':')}`); } if (params.get('type') === 'ws') { configParts.push('ws=true'); const path = params.get('path'); if (path) { configParts.push(`ws-path=${safeDecodeURIComponent(path)}`); } const host = params.get('host'); configParts.push(`ws-headers=Host:${host ? safeDecodeURIComponent(host) : url.hostname}`); } return configParts.join(', '); } catch { return null; } } // 解析SOCKS链接为Surge格式 function parseSocksLink(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { // 处理标准格式:socks://username:password@server:port#name // 或者 socks://base64encoded@server:port#name const url = new URL(socksLink); if (!url.hostname || !url.port) return null; // 提取节点名称 const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; // 处理认证信息 let username = '', password = ''; // 专门处理 socks://base64auth@server:port 这样的格式 if (url.username) { // 首先对username进行URL解码 let decodedUsername = safeDecodeURIComponent(url.username); // 特殊处理 dXNlcm5hbWUxMjM6cGFzc3dvcmQxMjM= 这样的Base64编码认证信息 try { // 尝试Base64解码 const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { // 成功解码为 username:password 格式 const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } else { username = decodedUsername; } } else { // 不是预期的格式,使用原始值 username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } // 构建Surge格式 const configParts = [ nodeName + " = socks5", url.hostname, url.port ]; // 如果有用户名密码,添加到配置中 if (username) configParts.push(username); if (password) configParts.push(password); return configParts.join(', '); } catch (error) { return null; } } // 添加更新订阅信息的函数 async function handleUpdateSubscriptionInfo(env, path, data) { const name = data.name?.trim(); const newPath = data.path?.trim(); // 基本验证 if (!name) { return createErrorResponse('订阅名称不能为空', 400); } if (!validateSubscriptionPath(newPath)) { return createErrorResponse('无效的订阅路径格式', 400); } try { // 如果路径被修改,检查新路径是否已存在 if (newPath !== path) { const { results } = await env.DB.prepare(` SELECT 1 FROM subscriptions WHERE path = ? LIMIT 1 `).bind(newPath).all(); if (results.length > 0) { return createErrorResponse('该路径已被使用', 400); } } // 使用事务确保数据一致性 const statements = [ env.DB.prepare( "UPDATE subscriptions SET name = ?, path = ? WHERE path = ?" ).bind(name, newPath, path), env.DB.prepare( "SELECT id, name, path FROM subscriptions WHERE path = ?" ).bind(newPath) ]; const [updateResult, { results }] = await env.DB.batch(statements); if (!results?.[0]) { return createErrorResponse('更新失败:找不到订阅', 404); } return createSuccessResponse(results[0], '订阅信息已更新'); } catch (error) { return createErrorResponse('更新订阅信息失败: ' + error.message); } } // 添加删除订阅的处理函数 async function handleDeleteSubscription(env, path) { // 使用事务确保数据一致性 const statements = [ // 首先删除该订阅下的所有节点 env.DB.prepare( "DELETE FROM nodes WHERE subscription_id IN (SELECT id FROM subscriptions WHERE path = ?)" ).bind(path), // 然后删除订阅 env.DB.prepare( "DELETE FROM subscriptions WHERE path = ?" ).bind(path) ]; // 执行事务 await env.DB.batch(statements); return createSuccessResponse(null, '订阅已删除'); } // 添加更新节点的处理函数 async function handleUpdateNode(request, env, subscriptionPath, nodeId) { const nodeData = await request.json(); // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.replace(/[\r\n\s]+$/, ''); // 尝试base64解码 try { const decodedContent = safeBase64Decode(originalLink); if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.replace(/[\r\n\s]+$/, ''); } } catch (e) {} // 不是base64格式,继续使用原始内容 // 使用通用的节点名称提取函数 const nodeName = extractNodeName(originalLink); // 更新节点内容和名称 await env.DB.prepare(` UPDATE nodes SET original_link = ?, name = ? WHERE id = ? AND subscription_id = ? `).bind(originalLink, nodeName || '未命名节点', nodeId, subscriptionId).run(); return createSuccessResponse(null, '节点更新成功'); } // 将订阅内容转换为surge格式 function convertToSurge(content) { if (!content?.trim()) return ''; // 使用Map来映射节点类型和处理函数,提高性能 const nodeParserMap = new Map([ [NODE_TYPES.SS, parseSIP002Format], [NODE_TYPES.VMESS, parseVmessLink], [NODE_TYPES.TROJAN, parseTrojanLink], [NODE_TYPES.SOCKS, parseSocksLink], [NODE_TYPES.HYSTERIA2, parseHysteria2ToSurge], [NODE_TYPES.TUIC, parseTuicToSurge] ]); return content .split(/\r?\n/) .map(line => { const trimmedLine = line.trim(); if (!trimmedLine) return null; // 如果已经是snell格式,格式化并返回 if (trimmedLine.includes(NODE_TYPES.SNELL)) { return formatSnellConfig(trimmedLine); } // 跳过 VLESS 节点 if (trimmedLine.toLowerCase().startsWith(NODE_TYPES.VLESS)) { return null; } // 检查是否有匹配的解析器 for (const [prefix, parser] of nodeParserMap.entries()) { if (trimmedLine.startsWith(prefix)) { return parser(trimmedLine); } } return null; }) .filter(Boolean) .join('\n'); } // 格式化snell配置 function formatSnellConfig(snellConfig) { if (!snellConfig) return null; // 分割配置字符串,保持格式一致 const parts = snellConfig.split(',').map(part => part.trim()); return parts.join(', '); } // 安全的URL解码辅助函数 function safeDecodeURIComponent(str) { try { return decodeURIComponent(str); } catch { return str; } } // 安全的Base64编码辅助函数,支持Unicode字符 function safeBase64Encode(str) { try { return btoa(unescape(encodeURIComponent(str))); } catch (e) { return str; } } // 安全的Base64解码辅助函数 function safeBase64Decode(str) { try { // 方法1:使用atob解码,然后正确处理UTF-8编码 const decoded = atob(str); // 将解码结果转换为UTF-8字符串 const bytes = []; for (let i = 0; i < decoded.length; i++) { bytes.push(decoded.charCodeAt(i)); } // 使用TextDecoder正确解码UTF-8字符 if (typeof TextDecoder !== 'undefined') { const decoder = new TextDecoder('utf-8'); return decoder.decode(new Uint8Array(bytes)); } else { // 如果没有TextDecoder,使用escape + decodeURIComponent let utf8String = ''; for (let i = 0; i < bytes.length; i++) { utf8String += String.fromCharCode(bytes[i]); } return decodeURIComponent(escape(utf8String)); } } catch (e) { try { // 方法2:直接使用atob,可能适用于简单ASCII字符 return atob(str); } catch (e2) { // 如果Base64解码失败,返回原字符串 return str; } } } // 安全的UTF-8字符串解码函数 function safeUtf8Decode(str) { if (!str) return str; try { // 方法1:使用escape + decodeURIComponent return decodeURIComponent(escape(str)); } catch (e1) { try { // 方法2:直接使用decodeURIComponent return decodeURIComponent(str); } catch (e2) { try { // 方法3:使用TextDecoder(如果支持) if (typeof TextDecoder !== 'undefined') { const encoder = new TextEncoder(); const decoder = new TextDecoder('utf-8'); return decoder.decode(encoder.encode(str)); } } catch (e3) { // 如果所有方法都失败,返回原始字符串 return str; } return str; } } } // 过滤掉snell节点的函数 function filterSnellNodes(content) { if (!content?.trim()) return ''; return content .split(/\r?\n/) .filter(line => { const trimmedLine = line.trim(); if (!trimmedLine) return false; // 过滤掉snell节点 return !trimmedLine.includes(NODE_TYPES.SNELL); }) .join('\n'); } // 将订阅内容转换为 Clash 格式 function convertToClash(content) { if (!content?.trim()) { return generateEmptyClashConfig(); } const nodes = content .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean) .map(parseNodeToClash) .filter(Boolean); return generateClashConfig(nodes); } // 解析单个节点为 Clash 格式 function parseNodeToClash(nodeLink) { if (!nodeLink) return null; const lowerLink = nodeLink.toLowerCase(); // 跳过 snell 节点,Clash 不支持 if (nodeLink.includes(NODE_TYPES.SNELL)) { return null; } // 解析 SS 节点 if (lowerLink.startsWith(NODE_TYPES.SS)) { return parseSSToClash(nodeLink); } // 解析 VMess 节点 if (lowerLink.startsWith(NODE_TYPES.VMESS)) { return parseVmessToClash(nodeLink); } // 解析 Trojan 节点 if (lowerLink.startsWith(NODE_TYPES.TROJAN)) { return parseTrojanToClash(nodeLink); } // 解析 VLESS 节点 if (lowerLink.startsWith(NODE_TYPES.VLESS)) { return parseVlessToClash(nodeLink); } // 解析 SOCKS 节点 if (lowerLink.startsWith(NODE_TYPES.SOCKS)) { return parseSocksToClash(nodeLink); } // 解析 Hysteria2 节点 if (lowerLink.startsWith(NODE_TYPES.HYSTERIA2)) { return parseHysteria2ToClash(nodeLink); } // 解析 TUIC 节点 if (lowerLink.startsWith(NODE_TYPES.TUIC)) { return parseTuicToClash(nodeLink); } return null; } // 解析 SS 节点为 Clash 格式 function parseSSToClash(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; return { name: name ? decodeURIComponent(name) : '未命名节点', type: 'ss', server: server, port: parseInt(port), cipher: method, password: password }; } catch { return null; } } // 解析 VMess 节点为 Clash 格式 function parseVmessToClash(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; const node = { name: config.ps ? safeUtf8Decode(config.ps) : '未命名节点', type: 'vmess', server: config.add, port: parseInt(config.port), uuid: config.id, alterId: parseInt(config.aid) || 0, cipher: 'auto', tls: config.tls === 'tls' }; // 添加网络类型配置 if (config.net === 'ws') { node.network = 'ws'; if (config.path) { node['ws-opts'] = { path: config.path, headers: { Host: config.host || config.add } }; } } else if (config.net === 'grpc') { node.network = 'grpc'; if (config.path) { node['grpc-opts'] = { 'grpc-service-name': config.path }; } } // TLS 配置 if (config.tls === 'tls') { node['skip-cert-verify'] = true; if (config.sni) { node.servername = config.sni; } } return node; } catch { return null; } } // 解析 Trojan 节点为 Clash 格式 function parseTrojanToClash(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'trojan', server: url.hostname, port: parseInt(url.port), password: url.username, 'skip-cert-verify': true }; // 添加网络类型配置 if (params.get('type') === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (params.get('type') === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 VLESS 节点为 Clash 格式 function parseVlessToClash(vlessLink) { if (!vlessLink.startsWith(NODE_TYPES.VLESS)) return null; try { const url = new URL(vlessLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'vless', server: url.hostname, port: parseInt(url.port), uuid: url.username, tls: params.get('security') === 'tls' || params.get('security') === 'reality', 'client-fingerprint': 'chrome', tfo: false, 'skip-cert-verify': false }; // 添加 flow 参数 const flow = params.get('flow'); if (flow) { node.flow = flow; } // Reality 配置 if (params.get('security') === 'reality') { const publicKey = params.get('pbk'); const shortId = params.get('sid'); if (publicKey || shortId) { node['reality-opts'] = {}; if (publicKey) node['reality-opts']['public-key'] = publicKey; if (shortId) node['reality-opts']['short-id'] = shortId; } } // 添加网络类型配置 const type = params.get('type'); if (type === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (type === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } else if (type === 'tcp') { node.network = 'tcp'; } else { // 默认设置为 tcp node.network = 'tcp'; } // SNI 配置 const sni = params.get('sni'); if (sni) { node.servername = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 SOCKS 节点为 Clash 格式 function parseSocksToClash(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { const url = new URL(socksLink); if (!url.hostname || !url.port) return null; const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'socks5', server: url.hostname, port: parseInt(url.port) }; // 处理认证信息 if (url.username) { let username = '', password = ''; let decodedUsername = safeDecodeURIComponent(url.username); try { const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } } else { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } if (username) node.username = username; if (password) node.password = password; } return node; } catch { return null; } } // 生成 Clash 配置文件 function generateClashConfig(proxies) { const proxyNames = proxies.map(proxy => proxy.name); const config = { // 用于下载订阅时指定UA 'global-ua': 'clash', // 全局配置 mode: 'rule', 'mixed-port': 7890, 'allow-lan': true, // 控制面板 'external-controller': '0.0.0.0:9090', // 如果有代理节点,则包含代理节点配置 proxies: proxies.length > 0 ? proxies : [], // 策略组 'proxy-groups': [ { name: '节点选择', type: 'select', proxies: ['DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Proxy.png' }, { name: '媒体服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Netflix.png' }, { name: '微软服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Microsoft.png' }, { name: '苹果服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Apple.png' }, { name: 'CDN服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/OneDrive.png' }, { name: 'AI服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/ChatGPT.png' }, { name: 'Telegram', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Telegram.png' }, { name: 'Speedtest', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Speedtest.png' }, ], // 分流规则 rules: [ 'RULE-SET,reject_non_ip,REJECT', 'RULE-SET,reject_domainset,REJECT', 'RULE-SET,reject_extra_domainset,REJECT', 'RULE-SET,reject_non_ip_drop,REJECT-DROP', 'RULE-SET,reject_non_ip_no_drop,REJECT', // 域名类规则 'RULE-SET,speedtest,Speedtest', 'RULE-SET,telegram_non_ip,Telegram', 'RULE-SET,apple_cdn,DIRECT', 'RULE-SET,apple_cn_non_ip,DIRECT', 'RULE-SET,microsoft_cdn_non_ip,DIRECT', 'RULE-SET,apple_services,苹果服务', 'RULE-SET,microsoft_non_ip,微软服务', 'RULE-SET,download_domainset,CDN服务', 'RULE-SET,download_non_ip,CDN服务', 'RULE-SET,cdn_domainset,CDN服务', 'RULE-SET,cdn_non_ip,CDN服务', 'RULE-SET,stream_non_ip,媒体服务', 'RULE-SET,ai_non_ip,AI服务', 'RULE-SET,global_non_ip,节点选择', 'RULE-SET,domestic_non_ip,DIRECT', 'RULE-SET,direct_non_ip,DIRECT', 'RULE-SET,lan_non_ip,DIRECT', 'GEOSITE,CN,DIRECT', // IP 类规则 'RULE-SET,reject_ip,REJECT', 'RULE-SET,telegram_ip,Telegram', 'RULE-SET,stream_ip,媒体服务', 'RULE-SET,lan_ip,DIRECT', 'RULE-SET,domestic_ip,DIRECT', 'RULE-SET,china_ip,DIRECT', 'GEOIP,LAN,DIRECT', 'GEOIP,CN,DIRECT', // 兜底规则 'MATCH,节点选择' ], // 规则提供者 'rule-providers': { reject_non_ip_no_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-no-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_no_drop.txt' }, reject_non_ip_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_drop.txt' }, reject_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip.txt' }, reject_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject.txt', path: './rule_set/sukkaw_ruleset/reject_domainset.txt' }, reject_extra_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject_extra.txt', path: './sukkaw_ruleset/reject_domainset_extra.txt' }, reject_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_ip.txt' }, speedtest: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: 'Speedtest', url: 'https://ruleset.skk.moe/Clash/domainset/speedtest.txt', path: './rule_set/sukkaw_ruleset/speedtest.txt' }, cdn_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_domainset.txt' }, cdn_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_non_ip.txt' }, stream_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_non_ip.txt' }, stream_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_ip.txt' }, ai_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/ai.txt', path: './rule_set/sukkaw_ruleset/ai_non_ip.txt' }, telegram_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_non_ip.txt' }, telegram_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_ip.txt' }, apple_cdn: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/apple_cdn.txt', path: './rule_set/sukkaw_ruleset/apple_cdn.txt' }, apple_services: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_services.txt', path: './rule_set/sukkaw_ruleset/apple_services.txt' }, apple_cn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_cn.txt', path: './rule_set/sukkaw_ruleset/apple_cn_non_ip.txt' }, microsoft_cdn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft_cdn.txt', path: './rule_set/sukkaw_ruleset/microsoft_cdn_non_ip.txt' }, microsoft_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft.txt', path: './rule_set/sukkaw_ruleset/microsoft_non_ip.txt' }, download_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/download.txt', path: './rule_set/sukkaw_ruleset/download_domainset.txt' }, download_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/download.txt', path: './rule_set/sukkaw_ruleset/download_non_ip.txt' }, lan_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_non_ip.txt' }, lan_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_ip.txt' }, domestic_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_non_ip.txt' }, direct_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/direct.txt', path: './rule_set/sukkaw_ruleset/direct_non_ip.txt' }, global_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/global.txt', path: './rule_set/sukkaw_ruleset/global_non_ip.txt' }, domestic_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_ip.txt' }, china_ip: { type: 'http', behavior: 'ipcidr', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/china_ip.txt', path: './rule_set/sukkaw_ruleset/china_ip.txt' } } }; return `# Clash 配置文件 - Sub-Hub 自动生成 # 生成时间: ${new Date().toISOString()} ${convertToYaml(config)}`; } // 生成空的 Clash 配置 function generateEmptyClashConfig() { return generateClashConfig([]); } // 简化的对象转 YAML 函数,针对 Clash 配置优化 function convertToYaml(obj, indent = 0) { const spaces = ' '.repeat(indent); let yaml = ''; for (const [key, value] of Object.entries(obj)) { // 处理键名,只对真正需要的情况加引号 let yamlKey = key; if (key.includes(' ') || key.includes('@') || key.includes('&') || key.includes('*') || key.includes('?') || key.includes('>') || key.includes('<') || key.includes('!') || key.includes('%') || key.includes('^') || key.includes('`') || /^\d/.test(key) || key === '' || /^(true|false|null|yes|no|on|off)$/i.test(key)) { yamlKey = `"${key.replace(/"/g, '\\"')}"`; } if (value === null || value === undefined) { yaml += `${spaces}${yamlKey}: null\n`; } else if (typeof value === 'boolean') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'number') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'string') { // 对字符串值更宽松的引号判断,主要针对真正会导致 YAML 解析问题的字符 const needsQuotes = value.includes(':') || value.includes('#') || value.includes('"') || value.includes('\n') || value.includes('&') || value.includes('*') || value.includes('[') || value.includes(']') || value.includes('{') || value.includes('}') || value.includes('@') || value.includes('`') || /^\s/.test(value) || /\s$/.test(value) || value === '' || /^(true|false|null|yes|no|on|off)$/i.test(value) || (/^\d+$/.test(value) && value.length > 1) || (/^\d+\.\d+$/.test(value) && value.length > 1); if (needsQuotes) { const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces}${yamlKey}: "${escapedValue}"\n`; } else { yaml += `${spaces}${yamlKey}: ${value}\n`; } } else if (Array.isArray(value)) { if (value.length === 0) { yaml += `${spaces}${yamlKey}: []\n`; } else { yaml += `${spaces}${yamlKey}:\n`; for (const item of value) { if (typeof item === 'object' && item !== null) { yaml += `${spaces} -\n`; const itemYaml = convertToYaml(item, 0); yaml += itemYaml.split('\n').map(line => line.trim() ? `${spaces} ${line}` : '' ).filter(line => line).join('\n') + '\n'; } else if (typeof item === 'string') { // 对数组中的字符串项(如节点名称)更宽松的引号判断 const needsQuotes = item.includes(':') || item.includes('#') || item.includes('"') || item.includes('\n') || item.includes('&') || item.includes('*') || item.includes('[') || item.includes(']') || item.includes('{') || item.includes('}') || item.includes('@') || item.includes('`') || /^\s/.test(item) || /\s$/.test(item) || item === '' || /^(true|false|null|yes|no|on|off)$/i.test(item) || (/^\d+$/.test(item) && item.length > 1) || (/^\d+\.\d+$/.test(item) && item.length > 1); if (needsQuotes) { const escapedItem = item.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces} - "${escapedItem}"\n`; } else { yaml += `${spaces} - ${item}\n`; } } else { yaml += `${spaces} - ${item}\n`; } } } } else if (typeof value === 'object' && value !== null) { yaml += `${spaces}${yamlKey}:\n`; yaml += convertToYaml(value, indent + 1); } } return yaml; } // 解析 Hysteria2 节点为 Clash 格式 function parseHysteria2ToClash(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'hysteria2', server: url.hostname, port: parseInt(url.port), password: url.username || params.get('password') || '', 'skip-cert-verify': true }; // 上传和下载速度配置 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) node.up = upMbps; if (downMbps) node.down = downMbps; // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // 混淆配置 const obfs = params.get('obfs'); if (obfs) { node.obfs = safeDecodeURIComponent(obfs); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { node['obfs-password'] = safeDecodeURIComponent(obfsPassword); } } // 拥塞控制算法 const cc = params.get('cc'); if (cc) { node.cc = cc; } return node; } catch { return null; } } // 解析 TUIC 节点为 Clash 格式 function parseTuicToClash(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'tuic', server: url.hostname, port: parseInt(url.port), uuid: url.username || params.get('uuid') || '', password: url.password || params.get('password') || '', 'skip-cert-verify': true }; // TUIC 版本 const version = params.get('version') || params.get('v'); if (version) { node.version = parseInt(version); } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // UDP Relay 模式 const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) { node['udp-relay-mode'] = udpRelayMode; } // 拥塞控制算法 const cc = params.get('congestion_control') || params.get('cc'); if (cc) { node['congestion-control'] = cc; } // 禁用 SNI const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { node['disable-sni'] = true; } // 减少 RTT const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { node['reduce-rtt'] = true; } return node; } catch { return null; } } // 解析 Hysteria2 链接为 Surge 格式 function parseHysteria2ToSurge(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const password = url.username || params.get('password') || ''; // 构建 Surge 格式的 Hysteria2 配置 const configParts = [ `${nodeName} = hysteria2`, url.hostname, url.port, `password=${password}` ]; // 添加可选参数 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) configParts.push(`up=${upMbps}`); if (downMbps) configParts.push(`down=${downMbps}`); const sni = params.get('sni'); if (sni) configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); const obfs = params.get('obfs'); if (obfs) { configParts.push(`obfs=${safeDecodeURIComponent(obfs)}`); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { configParts.push(`obfs-password=${safeDecodeURIComponent(obfsPassword)}`); } } const cc = params.get('cc'); if (cc) configParts.push(`cc=${cc}`); // 默认跳过证书验证 configParts.push('skip-cert-verify=true'); return configParts.join(', '); } catch (error) { return null; } } // 解析 TUIC 链接为 Surge 格式 function parseTuicToSurge(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const uuid = url.username || params.get('uuid') || ''; const password = url.password || params.get('password') || ''; // 构建 Surge 格式的 TUIC 配置 const configParts = [ `${nodeName} = tuic`, url.hostname, url.port, `uuid=${uuid}`, `password=${password}` ]; // TUIC 版本 - 如果没有指定版本,默认使用版本 5 const version = params.get('version') || params.get('v') || '5'; configParts.push(`version=${version}`); // SNI 配置 - 如果没有指定 SNI,使用服务器地址作为 SNI const sni = params.get('sni') || url.hostname; configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); // 处理 allow_insecure 参数 const allowInsecure = params.get('allow_insecure') || params.get('allowInsecure'); if (allowInsecure === 'true' || allowInsecure === '1') { configParts.push('skip-cert-verify=true'); } else { // 如果没有明确设置 allow_insecure,默认为 false configParts.push('skip-cert-verify=false'); } const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) configParts.push(`udp-relay-mode=${udpRelayMode}`); const cc = params.get('congestion_control') || params.get('congestion-control') || params.get('cc'); if (cc) configParts.push(`congestion-control=${cc}`); const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { configParts.push('disable-sni=true'); } const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { configParts.push('reduce-rtt=true'); } return configParts.join(', '); } catch (error) { return null; } }以下是原worker文件【20250529更新】// 通用的路径验证和节点名称提取函数 function validateSubscriptionPath(path) { return /^[a-z0-9-]{5,50}$/.test(path); } // 节点类型常量定义 const NODE_TYPES = { SS: 'ss://', VMESS: 'vmess://', TROJAN: 'trojan://', VLESS: 'vless://', SOCKS: 'socks://', HYSTERIA2: 'hysteria2://', TUIC: 'tuic://', SNELL: 'snell,' }; function extractNodeName(nodeLink) { if (!nodeLink) return '未命名节点'; // 处理snell节点 if(nodeLink.includes(NODE_TYPES.SNELL)) { const name = nodeLink.split('=')[0].trim(); return name || '未命名节点'; } // 处理 VMess 链接 if (nodeLink.toLowerCase().startsWith(NODE_TYPES.VMESS)) { try { const config = JSON.parse(safeBase64Decode(nodeLink.substring(8))); if (config.ps) { return safeUtf8Decode(config.ps); } } catch {} return '未命名节点'; } // 处理其他使用哈希标记名称的链接类型(SS、TROJAN、VLESS、SOCKS、Hysteria2、TUIC等) const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } export default { async fetch(request, env) { // 解析请求路径和参数 const url = new URL(request.url); const pathname = url.pathname; const method = request.method; // 检查是否有查询参数 if (url.search && !pathname.startsWith('/admin')) { return new Response('Not Found', { status: 404 }); } // 从环境变量获取配置路径,如果未设置则使用默认值 const adminPath = env.ADMIN_PATH || 'admin'; // 从环境变量获取登录凭据 const adminUsername = env.ADMIN_USERNAME || 'admin'; const adminPassword = env.ADMIN_PASSWORD || 'password'; // 处理登录页面请求 if (pathname === `/${adminPath}/login`) { if (method === "GET") { return serveLoginPage(adminPath); } else if (method === "POST") { return handleLogin(request, env, adminUsername, adminPassword, adminPath); } } // 处理登出请求 if (pathname === `/${adminPath}/logout`) { return handleLogout(request, env, adminPath); } // 处理管理面板请求 if (pathname === `/${adminPath}`) { const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return Response.redirect(`${url.origin}/${adminPath}/login`, 302); } return serveAdminPanel(env, adminPath); } // 处理API请求 if (pathname.startsWith(`/${adminPath}/api/`)) { // 验证会话 const isAuthenticated = await verifySession(request, env); if (!isAuthenticated) { return new Response(JSON.stringify({ success: false, message: '未授权访问' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); } // 处理节点管理API请求 const nodeApiMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)/nodes(?:/([^/]+|reorder))?$`)); if (nodeApiMatch) { const subscriptionPath = nodeApiMatch[1]; const nodeId = nodeApiMatch[2]; try { // 更新节点顺序 if (nodeId === 'reorder' && method === 'POST') { const { orders } = await request.json(); if (!Array.isArray(orders) || orders.length === 0) { return new Response(JSON.stringify({ success: false, message: '无效的排序数据' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return new Response(JSON.stringify({ success: false, message: '订阅不存在' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } const subscriptionId = subResults[0].id; // 使用事务来确保数据一致性 const statements = []; // 准备更新语句 for (const { id, order } of orders) { statements.push(env.DB.prepare( "UPDATE nodes SET node_order = ? WHERE id = ? AND subscription_id = ?" ).bind(order, id, subscriptionId)); } // 执行批量更新 const result = await env.DB.batch(statements); return new Response(JSON.stringify({ success: true, message: '节点顺序已更新' }), { headers: { 'Content-Type': 'application/json' } }); } // 获取节点列表 if (!nodeId && method === 'GET') { return handleGetNodes(env, subscriptionPath); } // 创建新节点 if (!nodeId && method === 'POST') { return handleCreateNode(request, env, subscriptionPath); } // 更新节点 if (nodeId && nodeId !== 'reorder' && method === 'PUT') { return handleUpdateNode(request, env, subscriptionPath, nodeId); } // 删除节点 if (nodeId && nodeId !== 'reorder' && method === 'DELETE') { return handleDeleteNode(env, subscriptionPath, nodeId); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('API请求处理失败:', error); return new Response(JSON.stringify({ success: false, message: error.message || '服务器内部错误' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } // 处理订阅管理API请求 if (pathname.startsWith(`/${adminPath}/api/subscriptions`)) { // 获取单个订阅内容 const getOneMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (getOneMatch && method === 'GET') { return handleGetSubscription(env, getOneMatch[1]); } // 获取订阅列表 if (pathname === `/${adminPath}/api/subscriptions` && method === 'GET') { return handleGetSubscriptions(env); } // 创建新订阅 if (pathname === `/${adminPath}/api/subscriptions` && method === 'POST') { try { const { name, path } = await request.json(); if (!name || !validateSubscriptionPath(path)) { return createErrorResponse('无效的参数', 400); } // 检查路径是否已存在 const { results } = await env.DB.prepare( "SELECT COUNT(*) as count FROM subscriptions WHERE path = ?" ).bind(path).all(); if (results[0].count > 0) { return createErrorResponse('该路径已被使用', 400); } // 创建订阅 const result = await env.DB.prepare( "INSERT INTO subscriptions (name, path) VALUES (?, ?)" ).bind(name, path).run(); if (!result.success) { throw new Error('创建订阅失败'); } return createSuccessResponse(null, '订阅创建成功'); } catch (error) { console.error('创建订阅失败:', error); return createErrorResponse('创建订阅失败: ' + error.message); } } // 更新订阅信息 const updateMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (updateMatch && method === 'PUT') { const data = await request.json(); return handleUpdateSubscriptionInfo(env, updateMatch[1], data); } // 删除订阅 const deleteMatch = pathname.match(new RegExp(`^/${adminPath}/api/subscriptions/([^/]+)$`)); if (deleteMatch && method === 'DELETE') { return handleDeleteSubscription(env, deleteMatch[1]); } return new Response(JSON.stringify({ success: false, message: 'Method Not Allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ success: false, message: 'Not Found' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } // 处理订阅请求 if (pathname.startsWith('/')) { // 检查路径格式是否合法(只允许一级或两级路径,如 /path 或 /path/surge 或 /path/v2ray 或 /path/clash) const pathParts = pathname.split('/').filter(Boolean); if (pathParts.length > 2) { return new Response('Not Found', { status: 404 }); } if (pathParts.length === 2 && !['surge', 'v2ray', 'clash'].includes(pathParts[1])) { return new Response('Not Found', { status: 404 }); } try { // 获取基本路径 let basePath = pathname; if (pathname.endsWith('/surge')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/surge } else if (pathname.endsWith('/v2ray')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/v2ray } else if (pathname.endsWith('/clash')) { basePath = pathname.slice(1, -6); // 移除开头的/和结尾的/clash } else { basePath = pathname.slice(1); // 只移除开头的/ } // 获取订阅信息 const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(basePath).all(); const subscription = results[0]; if (subscription) { // 生成订阅内容 const content = await generateSubscriptionContent(env, basePath); // 根据请求路径返回不同格式的内容 if (pathname.endsWith('/surge')) { // 返回 Surge 格式 const surgeContent = convertToSurge(content); return new Response(surgeContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/v2ray')) { // 返回 Base64 编码格式,排除 snell 节点,包括 VLESS 节点 const filteredContent = filterSnellNodes(content); const base64Content = safeBase64Encode(filteredContent); return new Response(base64Content, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } else if (pathname.endsWith('/clash')) { // 返回 Clash 格式 const clashContent = convertToClash(content); return new Response(clashContent, { headers: { 'Content-Type': 'text/yaml; charset=utf-8' }, }); } // 返回普通订阅内容,排除 snell 节点 const filteredContent = filterSnellNodes(content); return new Response(filteredContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8' }, }); } } catch (error) { console.error('处理订阅请求失败:', error); return new Response('Internal Server Error', { status: 500 }); } // 如果没有找到匹配的订阅,返回404 return new Response('Not Found', { status: 404 }); } // 其他所有路径返回 404 return new Response('Not Found', { status: 404 }); }, }; // 添加获取单个订阅的处理函数 async function handleGetSubscription(env, path) { try { const { results } = await env.DB.prepare( "SELECT * FROM subscriptions WHERE path = ?" ).bind(path).all(); if (!results || results.length === 0) { return createErrorResponse('订阅不存在', 404); } return createSuccessResponse(results[0]); } catch (error) { console.error('获取订阅内容失败:', error); return createErrorResponse('获取订阅内容失败: ' + error.message); } } // 提供登录页面HTML function serveLoginPage(adminPath) { const html = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sub-Hub - 登录</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/all.min.css"> <style> :root { --primary-color: #4e73df; --success-color: #1cc88a; --danger-color: #e74a3b; --transition-timing: 0.25s cubic-bezier(0.4, 0, 0.2, 1); --box-shadow-light: 0 2px 10px rgba(0,0,0,0.05); --box-shadow-medium: 0 4px 15px rgba(0,0,0,0.08); --font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; --text-color: #2d3748; --text-color-secondary: #444; --border-radius-sm: 8px; --border-radius-md: 12px; --border-radius-lg: 16px; } * { transition: all var(--transition-timing); font-family: var(--font-family); } html { scrollbar-gutter: stable; } /* 防止模态框打开时页面偏移 */ .modal-open { padding-right: 0 !important; } /* 修复模态框背景遮罩的宽度 */ .modal-backdrop { width: 100vw !important; } /* 优化模态框布局 */ .modal-dialog { margin-right: auto !important; margin-left: auto !important; padding-right: 0 !important; } /* 标题和重要文字样式 */ .navbar-brand, .subscription-name, .modal-title, .form-label { font-weight: 600; color: var(--text-color); } /* 按钮统一样式 */ .btn, .logout-btn { font-weight: 500; } /* 次要文字样式 */ .link-label, .form-text { color: var(--text-color-secondary); } /* 链接标签文字加粗 */ .link-label small > span > span { font-weight: 600; } body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; } .login-container { background-color: #fff; border-radius: 16px; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); overflow: hidden; width: 360px; max-width: 90%; } .login-header { background: linear-gradient(120deg, var(--primary-color), #224abe); padding: 2rem 1.5rem; text-align: center; color: white; } .login-icon { background-color: white; color: var(--primary-color); width: 80px; height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); } .login-icon i { font-size: 2.5rem; } .login-title { font-weight: 600; margin-bottom: 0.5rem; } .login-subtitle { opacity: 0.8; font-size: 0.9rem; } .login-form { padding: 2rem; } .form-floating { margin-bottom: 1.5rem; } .form-floating input { border-radius: 8px; height: 56px; border: 2px solid #e7eaf0; box-shadow: none; } .form-floating input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 0.25rem rgba(78, 115, 223, 0.15); } .form-floating label { color: #7e7e7e; padding-left: 1rem; } .btn-login { background: linear-gradient(120deg, var(--primary-color), #224abe); border: none; border-radius: 8px; padding: 0.75rem 1.5rem; font-weight: 600; width: 100%; margin-top: 1rem; box-shadow: 0 4px 10px rgba(78, 115, 223, 0.35); transition: all 0.2s ease; } .btn-login:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(78, 115, 223, 0.4); } .alert { border-radius: 8px; font-size: 0.9rem; margin-bottom: 1.5rem; display: none; } </style> </head> <body> <div class="login-container"> <div class="login-header"> <div class="login-icon"> <i class="fas fa-cube"></i> </div> <h3 class="login-title">Sub-Hub</h3> <p class="login-subtitle">请登录以继续使用</p> </div> <div class="login-form"> <div class="alert alert-danger" id="loginAlert" role="alert"> <i class="fas fa-exclamation-triangle me-2"></i> <span id="alertMessage">用户名或密码错误</span> </div> <form id="loginForm"> <div class="form-floating"> <input type="text" class="form-control" id="username" name="username" placeholder="用户名" required> <label for="username"><i class="fas fa-user me-2"></i>用户名</label> </div> <div class="form-floating"> <input type="password" class="form-control" id="password" name="password" placeholder="密码" required> <label for="password"><i class="fas fa-lock me-2"></i>密码</label> </div> <button type="submit" class="btn btn-primary btn-login"> <i class="fas fa-sign-in-alt me-2"></i>登录 </button> </form> </div> </div> <script> document.getElementById('loginForm').addEventListener('submit', async function(e) { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const response = await fetch('/${adminPath}/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username, password }), }); const data = await response.json(); if (data.success) { // 登录成功,重定向到管理面板 window.location.href = data.redirect; } else { // 显示错误消息 document.getElementById('alertMessage').textContent = data.message; document.getElementById('loginAlert').style.display = 'block'; } } catch (error) { // 显示错误消息 document.getElementById('alertMessage').textContent = '登录请求失败,请重试'; document.getElementById('loginAlert').style.display = 'block'; } }); </script> </body> </html>`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 验证会话 async function verifySession(request, env) { const sessionId = getSessionFromCookie(request); if (!sessionId) return false; const now = Date.now(); const { results } = await env.DB.prepare(` UPDATE sessions SET expires_at = ? WHERE session_id = ? AND expires_at > ? RETURNING * `).bind(now + 24 * 60 * 60 * 1000, sessionId, now).all(); return results.length > 0; } // 从Cookie中获取会话ID function getSessionFromCookie(request) { const cookieHeader = request.headers.get('Cookie') || ''; const sessionCookie = cookieHeader.split(';') .find(cookie => cookie.trim().startsWith('session=')); return sessionCookie ? sessionCookie.trim().substring(8) : null; } // 生成安全的会话令牌 async function generateSecureSessionToken(username, env) { // 清理过期会话和用户旧会话 const now = Date.now(); await env.DB.batch([ env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?").bind(now), env.DB.prepare("DELETE FROM sessions WHERE username = ?").bind(username) ]); const sessionId = crypto.randomUUID(); const expiresAt = now + 24 * 60 * 60 * 1000; // 24小时后过期 await env.DB.prepare(` INSERT INTO sessions (session_id, username, expires_at) VALUES (?, ?, ?) `).bind(sessionId, username, expiresAt).run(); return sessionId; } // 处理登录请求 async function handleLogin(request, env, adminUsername, adminPassword, adminPath) { const { username, password } = await request.json(); if (!username || !password || username !== adminUsername || password !== adminPassword) { return new Response(JSON.stringify({ success: false, message: '用户名或密码错误' }), { headers: { 'Content-Type': 'application/json' }, status: 401 }); } const sessionId = await generateSecureSessionToken(username, env); const headers = new Headers({ 'Content-Type': 'application/json', 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400; Secure` }); return new Response(JSON.stringify({ success: true, message: '登录成功', redirect: `/${adminPath}` }), { headers }); } // 处理登出请求 async function handleLogout(request, env, adminPath) { const sessionId = getSessionFromCookie(request); if (sessionId) { await env.DB.prepare("DELETE FROM sessions WHERE session_id = ?").bind(sessionId).run(); } const headers = new Headers({ 'Set-Cookie': `session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0; Secure` }); return Response.redirect(`${new URL(request.url).origin}/${adminPath}/login`, 302); } // 修改管理面板HTML生成函数 function serveAdminPanel(env, adminPath) { const html = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sub-Hub</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/all.min.css"> <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script> <style> :root { --primary-color: #4e73df; --success-color: #1cc88a; --danger-color: #e74a3b; --transition-timing: 0.25s cubic-bezier(0.4, 0, 0.2, 1); --box-shadow-light: 0 2px 10px rgba(0,0,0,0.05); --box-shadow-medium: 0 4px 15px rgba(0,0,0,0.08); --font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; --text-color: #2d3748; --text-color-secondary: #444; --border-radius-sm: 8px; --border-radius-md: 12px; --border-radius-lg: 16px; } * { transition: all var(--transition-timing); font-family: var(--font-family); } html { scrollbar-gutter: stable; } body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; background-color: #f8f9fc; padding: 20px 0; overflow-y: scroll; } /* 防止模态框打开时页面偏移 */ .modal-open { padding-right: 0 !important; } /* 修复模态框背景遮罩的宽度 */ .modal-backdrop { width: 100vw !important; } /* 优化模态框布局 */ .modal-dialog { margin-right: auto !important; margin-left: auto !important; padding-right: 0 !important; } /* 标题和重要文字样式 */ .navbar-brand, .subscription-name, .modal-title, .form-label { font-weight: 600; color: var(--text-color); } /* 按钮统一样式 */ .btn, .logout-btn { font-weight: 500; } /* 次要文字样式 */ .subscription-meta, .link-label, .form-text { color: var(--text-color-secondary); } /* 链接标签文字加粗 */ .link-label small > span > span { font-weight: 600; } .navbar { background-color: white; box-shadow: var(--box-shadow-light); padding: 1rem 1.5rem; margin-bottom: 0.5rem; height: 80px; display: flex; align-items: center; transform: translateY(0); } .navbar:hover { box-shadow: var(--box-shadow-medium); } .navbar-brand { font-weight: bold; color: #000; display: flex; align-items: center; font-size: 1.8rem; } .navbar-brand i { font-size: 2.2rem; margin-right: 0.8rem; color: #000; } .navbar-user { display: flex; align-items: center; } .navbar-user .user-name { font-weight: 500; } .logout-btn { color: #4a4a4a; background: none; border: none; margin-left: 1rem; padding: 0.375rem 0.75rem; border-radius: 0.25rem; text-decoration: none; font-weight: 500; } .logout-btn:hover { background-color: #f8f9fa; color: var(--danger-color); text-decoration: none; } .logout-btn:focus { text-decoration: none; outline: none; } .container { max-width: 1100px; } textarea { border: 1px solid #e5e7eb; border-radius: 8px; resize: vertical; min-height: 300px; max-height: 800px; height: 300px; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 0.9rem; line-height: 1.5; padding: 12px; } .link-label { display: flex; flex-direction: column; margin-top: 1rem; color: #444; font-size: 0.9rem; } .link-label small { display: flex; flex-direction: column; gap: 0.75rem; } .link-label small > span { display: flex; align-items: center; gap: 0.25rem; font-size: 0.9rem; } .link-label small > span > span { display: inline-block; width: 90px; color: var(--text-color); font-weight: 600; text-align: justify; text-align-last: justify; font-size: 0.9rem; } .link-label a { margin-left: 0; color: var(--primary-color); text-decoration: none; word-break: break-all; font-weight: 700; font-size: 0.9rem; } .link-label .link:hover { text-decoration: underline; } .btn-group { display: flex; gap: 0.5rem; margin-top: 1rem; } .toast-container { position: fixed; top: 1rem; right: 1rem; z-index: 1050; } @media (max-width: 767px) { .btn-group { flex-direction: column; } } .subscription-list { margin-bottom: 2rem; } .subscription-item { background: #fff; border-radius: var(--border-radius-md); padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: var(--box-shadow-light); border: 1px solid #eaeaea; overflow: hidden; transition: margin-bottom 0.5s cubic-bezier(0.4, 0, 0.2, 1); position: relative; } .edit-button { position: absolute; top: 1rem; right: 1rem; z-index: 1; } .edit-button .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; } .subscription-item:hover { box-shadow: var(--box-shadow-medium); } .subscription-edit-area { max-height: 0; overflow: hidden; opacity: 0; transform: translateY(-10px); padding: 0; margin: 0; will-change: max-height, opacity, transform, padding, margin; } .subscription-edit-area.active { max-height: 300px; opacity: 1; transform: translateY(0); padding: 1rem; margin-top: 0.5rem; } .subscription-edit-area textarea { width: 100%; height: 200px; min-height: 200px; max-height: 200px; margin-bottom: 1rem; padding: 1rem; border: 1px solid #e5e7eb; border-radius: var(--border-radius-sm); font-size: 0.9rem; line-height: 1.5; resize: none; box-shadow: var(--box-shadow-light); opacity: 0; transform: translateY(5px); } .subscription-edit-area.active textarea { opacity: 1; transform: translateY(0); } .subscription-edit-actions { display: flex; gap: 0.5rem; justify-content: flex-end; opacity: 0; transform: translateY(5px); } .subscription-edit-area.active .subscription-edit-actions { opacity: 1; transform: translateY(0); transition-delay: 0.05s; } .toast { transform: translateY(0); } .toast.hide { transform: translateY(-100%); } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .subscription-item { animation: fadeIn 0.5s ease-out; } .path-error { color: #dc3545; font-size: 0.875em; margin-top: 0.25rem; display: none; } .form-control.is-invalid { border-color: #dc3545; padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .form-control.is-valid { border-color: #198754; padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .subscription-name { font-size: 1.5rem; font-weight: 600; color: #2d3748; margin-bottom: 0.75rem; line-height: 1.2; } .node-count { display: inline-flex; align-items: center; color: var(--text-color-secondary); font-size: 1rem; margin-left: 0.75rem; } .node-count i { margin-right: 0.35rem; font-size: 1rem; } .subscription-actions { margin-top: 1.25rem; padding-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; } .subscription-actions .btn { width: 100%; padding: 0.5rem 1.25rem; font-weight: 500; border-radius: 8px; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; display: flex; align-items: center; justify-content: center; } .subscription-actions .btn i { margin-right: 0.5rem; } /* 操作按钮样式 */ .node-actions { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; gap: 4px; flex-shrink: 0; } .node-actions .btn { padding: 4px 8px !important; min-width: 32px !important; height: 28px !important; display: inline-flex !important; align-items: center; justify-content: center; margin: 0 !important; } .node-actions .btn i { font-size: 14px; line-height: 1; margin: 0 !important; } /* 移动端适配样式 */ @media (max-width: 767px) { /* 基础布局调整 */ .container { padding: 0 15px; } .navbar { margin-bottom: 2rem; padding: 0.8rem 1rem; } .btn-group { flex-direction: column; } /* 订阅项样式调整 */ .subscription-item { padding: 1rem; } .subscription-item > div { flex-direction: column; align-items: flex-start !important; } .subscription-item .link-label { margin: 1rem 0 0; width: 100%; } .subscription-actions { margin-top: 1rem; width: 100%; flex-direction: row !important; gap: 0.5rem; } .subscription-actions .btn { flex: 1; font-size: 0.875rem; padding: 0.5rem; } .subscription-actions .btn i { margin-right: 0.25rem; font-size: 0.875rem; } /* 模态框调整 */ .modal-dialog { margin: 0.5rem; } .modal-content { border-radius: 1rem; } /* Toast提示调整 */ .toast-container { right: 0; left: 0; bottom: 1rem; top: auto; margin: 0 1rem; } .toast { width: 100%; } /* 节点列表移动端优化 */ .table { table-layout: fixed; width: 100%; margin-bottom: 0 !important; } .table thead th:first-child { padding-left: 1rem !important; width: 100% !important; } .table thead th:last-child { display: none !important; } .node-row td { padding: 0.5rem !important; } .node-row td:first-child { font-size: 0.85rem; padding-right: 0 !important; width: calc(100% - 90px) !important; } .node-row td:first-child .text-truncate { max-width: 100% !important; width: 100% !important; padding-right: 8px !important; } .node-row td:last-child { padding-left: 0 !important; width: 90px !important; position: relative !important; } .node-row td:last-child .text-truncate { display: none !important; } /* 节点操作按钮移动端优化 */ .node-actions { margin: 0 !important; gap: 2px !important; justify-content: flex-end !important; width: 90px !important; position: absolute !important; right: 4px !important; flex-wrap: nowrap !important; align-items: center !important; } .node-actions .btn { padding: 2px 4px !important; min-width: 28px !important; height: 28px !important; margin: 0 !important; } .node-actions .btn i { font-size: 12px; line-height: 1; display: flex !important; align-items: center !important; justify-content: center !important; } } /* 模态框按钮样式 */ .modal .btn-secondary { background-color: #e2e8f0; border-color: #e2e8f0; color: var(--text-color); } .modal .btn-secondary:hover { background-color: #cbd5e0; border-color: #cbd5e0; } /* 添加通用按钮样式 */ /* 按钮基础样式 */ .action-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.625rem 1.25rem; font-size: 1rem; font-weight: 500; border-radius: 8px; transition: all 0.2s ease; min-width: 140px; height: 42px; gap: 0.5rem; line-height: 1; } .action-btn i { font-size: 1rem; display: inline-flex; align-items: center; justify-content: center; width: 1rem; height: 1rem; } /* 按钮颜色变体 */ .action-btn.btn-primary, .btn-primary { background-color: var(--primary-color); border-color: var(--primary-color); color: white; } .action-btn.btn-success, .btn-success { background-color: var(--success-color); border-color: var(--success-color); color: white; } .btn-edit { background-color: #1cc88a; border-color: #1cc88a; color: white; } /* 按钮悬停效果 */ .action-btn:hover, .btn-edit:hover { transform: translateY(-1px); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); } .btn-edit:hover { background-color: #19b57c; border-color: #19b57c; } .btn-edit:focus { background-color: #19b57c; border-color: #19b57c; box-shadow: 0 0 0 0.25rem rgba(28, 200, 138, 0.25); } .action-btn:active { transform: translateY(0); } /* 调整容器样式 */ .container { max-width: 1100px; padding: 2rem 1rem; } .d-flex.justify-content-between.align-items-center.mb-4 { margin-bottom: 2rem !important; } /* 节点列表区域样式 */ .node-list-area { max-height: 0; overflow: hidden; transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1); background: #fff; border-radius: 0 0 var(--border-radius-md) var(--border-radius-md); will-change: max-height; font-size: 0.875rem; color: var(--text-color); } .node-list-area.expanded { max-height: 2000px; } .node-list-area .table-responsive { transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); transform-origin: top; } .node-list-area .link { font-size: 0.875rem; color: var(--text-color-secondary); text-decoration: none; } .node-list-area .link:hover { text-decoration: underline; } /* 订阅项样式 */ .subscription-item { background: #fff; border-radius: var(--border-radius-md); padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: var(--box-shadow-light); border: 1px solid #eaeaea; overflow: hidden; transition: margin-bottom 0.5s cubic-bezier(0.4, 0, 0.2, 1); position: relative; } .subscription-item:has(.node-list-area.expanded) { margin-bottom: 2rem; } .subscription-content { margin-left: 1rem; } .subscription-content .d-flex.align-items-center { margin-left: -0.1rem; } /* 编辑按钮样式 */ .edit-button { position: absolute; top: 1rem; right: 1rem; z-index: 1; } .edit-button .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; } /* 节点行样式 */ .node-row { cursor: move; } .node-row td { vertical-align: middle; font-family: var(--font-family); } .node-row .text-truncate, .node-row .text-nowrap { font-family: var(--font-family); font-weight: 500; } .node-row .form-check { display: flex; align-items: center; margin: 0; padding: 0; min-height: auto; } .node-row .form-check-input { margin: 0; position: static; } .node-row.sortable-ghost { opacity: 0.5; background-color: #f8f9fa; } .node-row.sortable-drag { background-color: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } </style> </head> <body> <nav class="navbar navbar-expand navbar-light bg-white"> <div class="container"> <a class="navbar-brand" href="#"> <i class="fas fa-cube"></i> Sub-Hub </a> <div class="navbar-user ms-auto"> <a href="/${adminPath}/logout" class="logout-btn"> <i class="fas fa-sign-out-alt me-1"></i> 退出 </a> </div> </div> </nav> <div class="container"> <div class="subscription-item" style="padding-bottom: 0; border: none; box-shadow: none; margin-bottom: 1rem; background: transparent;"> <div class="d-flex justify-content-end"> <button class="btn btn-primary action-btn" data-bs-toggle="modal" data-bs-target="#addSubscriptionModal"> <i class="fas fa-plus"></i> <span>添加订阅</span> </button> </div> </div> <!-- 订阅列表 --> <div class="subscription-list" id="subscriptionList"> <!-- 订阅项将通过JavaScript动态添加 --> </div> </div> <!-- 添加订阅模态框 --> <div class="modal fade" id="addSubscriptionModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">添加新订阅</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="addSubscriptionForm" onsubmit="return false;"> <div class="mb-3"> <label class="form-label">订阅名称 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="name" required> <div class="invalid-feedback">请输入订阅名称</div> </div> <div class="mb-3"> <label class="form-label">订阅路径 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="path" required pattern="^[a-z0-9-]+$" minlength="5" maxlength="50"> <div class="form-text">路径只能包含小写字母、数字和连字符,长度在5-50个字符之间</div> <div class="path-error text-danger"></div> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="createSubscription()">创建</button> </div> </div> </div> </div> <!-- 修改添加节点模态框 --> <div class="modal fade" id="addNodeModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">添加节点</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="addNodeForm" onsubmit="return false;"> <input type="hidden" name="subscriptionPath" value=""> <div class="mb-3"> <label class="form-label">节点内容 <span class="text-danger">*</span></label> <textarea class="form-control" name="content" rows="6" required placeholder="请输入节点内容,支持以下格式: 1. ss://... 2. vless://...(除Surge) 3. vmess://... 4. trojan://... 5. socks://... 6. hysteria2://... 7. tuic://... 8. snell格式(仅Surge) 9. Base64编码格式"></textarea> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="createNode()"> <i class="fas fa-plus me-1"></i>添加 </button> </div> </div> </div> </div> <!-- 编辑名称和路径模态框 --> <div class="modal fade" id="editNameModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">编辑订阅信息</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="editNameForm" onsubmit="return false;"> <input type="hidden" name="originalPath"> <div class="mb-3"> <label class="form-label">订阅名称 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="name" required> <div class="invalid-feedback">请输入订阅名称</div> </div> <div class="mb-3"> <label class="form-label">订阅路径 <span class="text-danger">*</span></label> <input type="text" class="form-control" name="path" required> <div class="form-text">路径只能包含小写字母、数字和连字符,长度在5-50个字符之间</div> <div class="path-error text-danger"></div> </div> </form> </div> <div class="modal-footer justify-content-between"> <button type="button" class="btn btn-danger" onclick="confirmDelete()"> <i class="fas fa-trash"></i> 删除订阅 </button> <div> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="updateSubscriptionInfo()">保存</button> </div> </div> </div> </div> </div> <!-- 编辑节点模态框 --> <div class="modal fade" id="editNodeModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">编辑节点</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <form id="editNodeForm" onsubmit="return false;"> <input type="hidden" name="subscriptionPath" value=""> <input type="hidden" name="nodeId" value=""> <div class="mb-3"> <label class="form-label">节点内容 <span class="text-danger">*</span></label> <textarea class="form-control" name="content" rows="6" required placeholder="请输入节点内容,支持以下格式: 1. ss://... 2. vless://...(除Surge) 3. vmess://... 4. trojan://... 5. socks://... 6. hysteria2://... 7. tuic://... 8. snell格式(仅Surge) 9. Base64编码格式"></textarea> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" onclick="updateNode()">保存</button> </div> </div> </div> </div> <!-- Toast提示 --> <div class="toast-container"> <div id="toast" class="toast align-items-center text-white bg-success" role="alert" aria-live="assertive" aria-atomic="true"> <div class="d-flex"> <div class="toast-body" id="toastMessage"> 操作成功! </div> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script> // 定义 adminPath 变量 const adminPath = '${adminPath}'; // 优化的loadSubscriptions函数 async function loadSubscriptions() { try { const response = await fetch('/' + adminPath + '/api/subscriptions'); if (!response.ok) throw new Error('加载失败'); const result = await response.json(); if (!result.success) { throw new Error(result.message || '加载失败'); } const subscriptions = result.data || []; const listElement = document.getElementById('subscriptionList'); listElement.innerHTML = ''; const fragment = document.createDocumentFragment(); for (const sub of subscriptions) { const item = document.createElement('div'); item.className = 'subscription-item'; item.innerHTML = \` <div class="d-flex justify-content-between align-items-start"> <button class="btn btn-sm btn-link text-primary edit-button" onclick="showEditNameModal('\${sub.path}', '\${sub.name}')"> <i class="fas fa-edit"></i> </button> <div class="subscription-content"> <div class="d-flex align-items-center"> <h5 class="subscription-name mb-1">\${sub.name}</h5> <span class="node-count ms-2"><i class="fas fa-server"></i>\${sub.nodeCount}</span> </div> <div class="link-label"> <small> <span> <span class="address-label">订阅地址1:</span> <a href="/\${sub.path}" target="_blank">/\${sub.path}</a> </span> <span> <span class="address-label">订阅地址2:</span> <a href="/\${sub.path}/v2ray" target="_blank">/\${sub.path}/v2ray</a> </span> <span> <span class="address-label">订阅地址3:</span> <a href="/\${sub.path}/surge" target="_blank">/\${sub.path}/surge</a> </span> <span> <span class="address-label">订阅地址4:</span> <a href="/\${sub.path}/clash" target="_blank">/\${sub.path}/clash</a> </span> </small> </div> </div> <div class="subscription-actions"> <button class="btn btn-success action-btn" onclick="showAddNodeModal('\${sub.path}')"> <i class="fas fa-plus"></i> <span>添加节点</span> </button> <button class="btn btn-primary action-btn" onclick="showNodeList('\${sub.path}')"> <i class="fas fa-list"></i> <span>节点列表</span> </button> </div> </div> <div class="node-list-area" id="node-list-\${sub.path}"> <div class="table-responsive mt-3"> <table class="table"> <thead> <tr> <th style="min-width: 250px; width: 30%; padding-left: 1rem;">名称</th> <th style="padding-left: 4.5rem;">节点链接</th> </tr> </thead> <tbody></tbody> </table> <div class="d-flex justify-content-between align-items-center mt-3 px-2 pb-2"> <button class="btn btn-danger btn-sm rounded-3" onclick="toggleBatchDelete('\${sub.path}')" id="batch-delete-btn-\${sub.path}" style="padding: 0.5rem 1rem;"> <i class="fas fa-trash-alt me-1"></i>批量删除 </button> <div class="batch-delete-actions" id="batch-delete-actions-\${sub.path}" style="display: none;"> <button class="btn btn-danger btn-sm me-2 rounded-3" onclick="executeBatchDelete('\${sub.path}')" style="padding: 0.5rem 1rem;"> <i class="fas fa-check me-1"></i>确认删除 </button> <button class="btn btn-secondary btn-sm rounded-3" onclick="cancelBatchDelete('\${sub.path}')" style="padding: 0.5rem 1rem;"> <i class="fas fa-times me-1"></i>取消 </button> </div> </div> </div> </div> \`; fragment.appendChild(item); } listElement.appendChild(fragment); } catch (error) { showToast('加载订阅列表失败: ' + error.message, 'danger'); } } // 页面加载完成后,加载订阅列表 window.addEventListener('load', loadSubscriptions); // 切换批量删除模式 function toggleBatchDelete(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); const checkboxes = nodeListArea.querySelectorAll('.form-check'); const deleteBtn = document.getElementById('batch-delete-btn-' + subscriptionPath); const deleteActions = document.getElementById('batch-delete-actions-' + subscriptionPath); const isEnteringBatchMode = checkboxes[0].style.display === 'none'; // 切换勾选框显示 checkboxes.forEach(checkbox => { checkbox.style.display = isEnteringBatchMode ? 'block' : 'none'; }); // 切换按钮和操作区域 deleteBtn.style.display = isEnteringBatchMode ? 'none' : 'block'; deleteActions.style.display = isEnteringBatchMode ? 'flex' : 'none'; nodeListArea.classList.toggle('batch-delete-mode', isEnteringBatchMode); // 重置所有勾选框 nodeListArea.querySelectorAll('.node-checkbox').forEach(cb => { cb.checked = false; }); } // 取消批量删除 function cancelBatchDelete(subscriptionPath) { toggleBatchDelete(subscriptionPath); } // 执行批量删除 async function executeBatchDelete(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); const checkedNodes = nodeListArea.querySelectorAll('.node-checkbox:checked'); if (checkedNodes.length === 0) { showToast('请选择要删除的节点', 'warning'); return; } if (!confirm(\`确定要删除选中的 \${checkedNodes.length} 个节点吗?\`)) { return; } try { let successCount = 0; let failCount = 0; // 显示进度提示 showToast('正在删除节点...', 'info'); // 批量删除所选节点 for (const checkbox of checkedNodes) { const nodeId = checkbox.value; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (response.ok) { successCount++; } else { failCount++; } } catch (error) { console.error('删除节点失败:', error); failCount++; } } // 显示删除结果 if (failCount === 0) { showToast(\`成功删除 \${successCount} 个节点\`, 'success'); } else { showToast(\`删除完成:成功 \${successCount} 个,失败 \${failCount} 个\`, 'warning'); } // 退出批量删除模式 toggleBatchDelete(subscriptionPath); // 重新加载节点列表和订阅信息 await loadNodeList(subscriptionPath); await loadSubscriptions(); } catch (error) { console.error('批量删除失败:', error); showToast('批量删除失败: ' + error.message, 'danger'); } }; // 显示添加节点模态框 function showAddNodeModal(subscriptionPath) { const modal = document.getElementById('addNodeModal'); const form = document.getElementById('addNodeForm'); // 重置表单 form.reset(); // 设置订阅路径 const pathInput = form.querySelector('[name="subscriptionPath"]'); if (pathInput) { pathInput.value = subscriptionPath; } // 显示模态框 const modalInstance = new bootstrap.Modal(modal); modalInstance.show(); } // 修改创建节点函数 async function createNode() { try { const form = document.getElementById('addNodeForm'); if (!form) { throw new Error('找不到表单元素'); } const formData = new FormData(form); const subscriptionPath = formData.get('subscriptionPath'); const name = formData.get('name')?.trim(); let content = formData.get('content')?.trim(); if (!subscriptionPath) { throw new Error('缺少订阅路径'); } if (!content) { throw new Error('请填写节点内容'); } // 分割成行 const lines = content.split(/\\r?\\n/); const validNodes = []; // 处理每一行 for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine) continue; // 检查是否是Base64编码的完整配置 try { const decodedContent = safeBase64DecodeFrontend(trimmedLine); // 如果解码成功,检查是否包含多个节点 const decodedLines = decodedContent.split(/\\r?\\n/); for (const decodedLine of decodedLines) { if (decodedLine.trim() && isValidNodeLink(decodedLine.trim())) { validNodes.push({ name: name || extractNodeNameFrontend(decodedLine.trim()), content: decodedLine.trim() }); } } } catch (e) { // 如果不是Base64编码,直接检查是否是有效的节点链接 if (isValidNodeLink(trimmedLine)) { validNodes.push({ name: name || extractNodeNameFrontend(trimmedLine), content: trimmedLine }); } } } if (validNodes.length === 0) { throw new Error('未找到有效的节点链接'); } // 批量创建节点,按顺序添加 const results = []; const timestamp = Date.now(); // 使用时间戳作为基础序号 for (let i = 0; i < validNodes.length; i++) { const node = validNodes[i]; try { // 创建节点,使用时间戳+索引作为顺序值 const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ name: node.name, content: node.content, type: getNodeType(node.content), order: timestamp + i // 使用时间戳确保顺序唯一且递增 }) }); const result = await response.json(); results.push({ success: response.ok, message: result.message, link: node.content, order: i }); } catch (error) { results.push({ success: false, message: error.message, link: node.content, order: i }); } } // 按原始顺序排序结果 results.sort((a, b) => a.order - b.order); // 统计结果 const successful = results.filter(r => r.success).length; const failed = results.filter(r => !r.success).length; // 显示结果 if (validNodes.length === 1) { showToast(successful > 0 ? '节点添加成功' : '节点添加失败', successful > 0 ? 'success' : 'danger'); } else { showToast(\`添加完成:成功 \${successful} 个,失败 \${failed} 个\`, successful > 0 ? 'success' : 'warning'); } // 关闭模态框 const modal = document.getElementById('addNodeModal'); if (modal) { const modalInstance = bootstrap.Modal.getInstance(modal); if (modalInstance) { modalInstance.hide(); } } // 重置表单 form.reset(); // 刷新订阅列表 await loadSubscriptions(); if (successful > 0) { await loadNodeList(subscriptionPath); } } catch (error) { console.error('添加节点失败:', error); showToast('添加节点失败: ' + error.message, 'danger'); } } // 提取节点名称(前端版本) function extractNodeNameFrontend(nodeLink) { if (!nodeLink) return '未命名节点'; // 处理snell节点 if(nodeLink.includes('snell,')) { const name = nodeLink.split('=')[0].trim(); return name || '未命名节点'; } // 处理 SOCKS 链接 if (nodeLink.toLowerCase().startsWith('socks://')) { const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } // 处理 VLESS 链接 if (nodeLink.toLowerCase().startsWith('vless://')) { const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } // 处理 VMess 链接 if (nodeLink.toLowerCase().startsWith('vmess://')) { try { const config = JSON.parse(safeBase64DecodeFrontend(nodeLink.substring(8))); if (config.ps) { return safeUtf8DecodeFrontend(config.ps); } } catch {} return '未命名节点'; } // 处理其他类型的链接 const hashIndex = nodeLink.indexOf('#'); if (hashIndex !== -1) { try { return decodeURIComponent(nodeLink.substring(hashIndex + 1)); } catch { return nodeLink.substring(hashIndex + 1) || '未命名节点'; } } return '未命名节点'; } // 节点类型配置(前端版本) const NODE_TYPES_FRONTEND = { 'ss://': 'ss', 'vmess://': 'vmess', 'trojan://': 'trojan', 'vless://': 'vless', 'socks://': 'socks', 'hysteria2://': 'hysteria2', 'tuic://': 'tuic', 'snell,': 'snell' }; // 检查是否是有效的节点链接 function isValidNodeLink(link) { const lowerLink = link.toLowerCase(); // 检查snell格式 if(lowerLink.includes('=') && lowerLink.includes('snell,')) { const parts = link.split('=')[1]?.trim().split(','); return parts && parts.length >= 4 && parts[0].trim() === 'snell'; } return Object.keys(NODE_TYPES_FRONTEND).some(prefix => lowerLink.startsWith(prefix)); } // 获取节点类型 function getNodeType(link) { const lowerLink = link.toLowerCase(); if(lowerLink.includes('=') && lowerLink.includes('snell,')) { return 'snell'; } return Object.entries(NODE_TYPES_FRONTEND).find(([prefix]) => lowerLink.startsWith(prefix) )?.[1] || ''; } // 安全的UTF-8字符串解码函数(前端版本) function safeUtf8DecodeFrontend(str) { if (!str) return str; try { // 方法1:使用escape + decodeURIComponent return decodeURIComponent(escape(str)); } catch (e1) { try { // 方法2:直接使用decodeURIComponent return decodeURIComponent(str); } catch (e2) { try { // 方法3:使用TextDecoder(如果支持) if (typeof TextDecoder !== 'undefined') { const encoder = new TextEncoder(); const decoder = new TextDecoder('utf-8'); return decoder.decode(encoder.encode(str)); } } catch (e3) { // 如果所有方法都失败,返回原始字符串 return str; } return str; } } } // 显示节点列表 async function showNodeList(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); if (!nodeListArea) { console.error('找不到节点列表区域'); return; } const isHidden = !nodeListArea.classList.contains('expanded'); let expandedSubs = JSON.parse(localStorage.getItem('expandedSubscriptions') || '[]'); if (isHidden) { // 立即添加展开类名触发动画 nodeListArea.classList.add('expanded'); // 更新展开状态 if (!expandedSubs.includes(subscriptionPath)) { expandedSubs.push(subscriptionPath); localStorage.setItem('expandedSubscriptions', JSON.stringify(expandedSubs)); } // 检查是否需要滚动 const rect = nodeListArea.getBoundingClientRect(); const isVisible = ( rect.top >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) ); if (!isVisible) { const scrollTarget = nodeListArea.offsetTop - window.innerHeight + rect.height + 100; window.scrollTo({ top: scrollTarget, behavior: 'smooth' }); } // 同时开始加载数据 loadNodeList(subscriptionPath).catch(error => { console.error('加载节点列表失败:', error); showToast('加载节点列表失败: ' + error.message, 'danger'); }); } else { nodeListArea.classList.remove('expanded'); expandedSubs = expandedSubs.filter(path => path !== subscriptionPath); localStorage.setItem('expandedSubscriptions', JSON.stringify(expandedSubs)); } } // 修改加载节点列表函数 async function loadNodeList(subscriptionPath) { const nodeListArea = document.getElementById('node-list-' + subscriptionPath); if (!nodeListArea) { throw new Error('找不到节点列表区域'); } const tbody = nodeListArea.querySelector('tbody'); // 先显示加载中的提示 tbody.innerHTML = '<tr><td colspan="2" class="text-center py-4"><i class="fas fa-spinner fa-spin me-2"></i>加载中...</td></tr>'; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes'); if (!response.ok) throw new Error('加载失败'); const result = await response.json(); if (!result.success) { throw new Error(result.message || '加载失败'); } const nodes = result.data || []; // 构建节点列表HTML const nodesHtml = nodes.map(node => { const nodeLink = node.original_link; const escapedNodeLink = nodeLink .replace(/&/g, '&') .replace(/'/g, '\\\'') .replace(/"/g, '\\"'); return \` <tr class="node-row" data-id="\${node.id}" data-order="\${node.node_order}"> <td> <div class="d-flex align-items-center"> <div class="form-check me-2" style="display: none;"> <input class="form-check-input node-checkbox" type="checkbox" value="\${node.id}" data-subscription="\${subscriptionPath}" style="margin-top: 0;"> </div> <div class="text-nowrap text-truncate" style="max-width: 300px; padding-left: 0.5rem;" title="\${node.name}"> \${node.name} </div> </div> </td> <td> <div class="d-flex justify-content-between align-items-center" style="gap: 8px;"> <div class="text-nowrap text-truncate" style="max-width: 400px; margin-left: 4rem;" title="\${nodeLink}"> \${nodeLink} </div> <div class="node-actions d-flex" style="flex-shrink: 0; gap: 4px;"> <button class="btn btn-sm btn-edit" onclick="showEditNodeModal('\${subscriptionPath}', '\${node.id}', '\${escapedNodeLink}')" title="编辑节点"> <i class="fas fa-edit"></i> </button> <button class="btn btn-sm btn-primary" onclick="copyToClipboard('\${escapedNodeLink}')" title="复制链接"> <i class="fas fa-copy"></i> </button> <button class="btn btn-sm btn-danger" onclick="deleteNode('\${subscriptionPath}', \${node.id})" title="删除节点"> <i class="fas fa-trash"></i> </button> </div> </div> </td> </tr> \`; }).join(''); // 更新节点列表内容 tbody.innerHTML = nodesHtml || '<tr><td colspan="2" class="text-center py-4">暂无节点</td></tr>'; // 初始化拖拽排序 if (nodes.length > 0) { // 检查是否为移动设备 const isMobile = window.innerWidth <= 767; // 只在非移动设备上初始化排序 if (!isMobile) { initializeSortable(tbody, subscriptionPath); } } } catch (error) { tbody.innerHTML = \`<tr><td colspan="2" class="text-center py-4 text-danger"> <i class="fas fa-exclamation-circle me-2"></i>\${error.message} </td></tr>\`; throw error; } } // 初始化拖拽排序功能 function initializeSortable(tbody, subscriptionPath) { new Sortable(tbody, { animation: 150, handle: '.node-row', ghostClass: 'sortable-ghost', dragClass: 'sortable-drag', onEnd: async function(evt) { try { const rows = Array.from(tbody.querySelectorAll('.node-row')); const newOrders = rows.map((row, index) => ({ id: parseInt(row.dataset.id), order: index })); await updateNodeOrder(subscriptionPath, newOrders); showToast('节点排序已更新'); } catch (error) { console.error('更新节点排序失败:', error); showToast('更新节点排序失败: ' + error.message, 'danger'); // 重新加载列表以恢复原始顺序 await loadNodeList(subscriptionPath); } } }); } // 删除节点 async function deleteNode(subscriptionPath, nodeId) { if (!confirm('确定要删除这个节点吗?')) return; try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '删除失败'); } showToast('节点已删除'); await loadNodeList(subscriptionPath); await loadSubscriptions(); } catch (error) { showToast('删除节点失败: ' + error.message, 'danger'); } } // 添加创建订阅函数 async function createSubscription() { const form = document.getElementById('addSubscriptionForm'); const formData = new FormData(form); const name = formData.get('name').trim(); const path = formData.get('path').trim(); if (!name) { showToast('请输入订阅名称', 'danger'); form.querySelector('[name="name"]').focus(); return; } if (!path || !validateSubscriptionPathFrontend(path)) { const pathInput = form.querySelector('[name="path"]'); pathInput.classList.add('is-invalid'); pathInput.classList.remove('is-valid'); form.querySelector('.path-error').textContent = '路径格式不正确'; form.querySelector('.path-error').style.display = 'block'; pathInput.focus(); return; } try { const response = await fetch('/' + adminPath + '/api/subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ name, path }) }); const result = await response.json(); if (!response.ok) throw new Error(result.message || '创建失败'); showToast('订阅创建成功'); bootstrap.Modal.getInstance(document.getElementById('addSubscriptionModal')).hide(); form.reset(); await loadSubscriptions(); } catch (error) { showToast('创建失败: ' + error.message, 'danger'); } } // 添加显示Toast提示函数 function showToast(message, type = 'success') { const toast = document.getElementById('toast'); const toastMessage = document.getElementById('toastMessage'); // 设置消息 toastMessage.textContent = message; // 设置类型 toast.className = toast.className.replace(/bg-\w+/, ''); toast.classList.add('bg-' + type); // 显示Toast const bsToast = new bootstrap.Toast(toast); bsToast.show(); } // 显示编辑名称模态框 function showEditNameModal(path, name) { const form = document.getElementById('editNameForm'); form.querySelector('input[name="originalPath"]').value = path; form.querySelector('input[name="name"]').value = name; form.querySelector('input[name="path"]').value = path; const modal = new bootstrap.Modal(document.getElementById('editNameModal')); modal.show(); } // 更新订阅信息 async function updateSubscriptionInfo() { const form = document.getElementById('editNameForm'); const originalPath = form.querySelector('input[name="originalPath"]').value; const nameInput = form.querySelector('input[name="name"]'); const pathInput = form.querySelector('input[name="path"]'); const pathError = form.querySelector('.path-error'); try { // 验证输入 if (!nameInput.value.trim()) { showToast('请输入订阅名称', 'danger'); nameInput.focus(); return; } // 验证路径格式 const path = pathInput.value.trim(); if (!validateSubscriptionPathFrontend(path)) { pathInput.classList.add('is-invalid'); pathInput.classList.remove('is-valid'); pathError.textContent = '路径格式不正确'; pathError.style.display = 'block'; pathInput.focus(); return; } // 如果路径被修改,检查新路径是否已存在 if (path !== originalPath) { const checkResponse = await fetch('/' + adminPath + '/api/subscriptions/' + path); if (checkResponse.ok) { pathInput.classList.add('is-invalid'); pathInput.classList.remove('is-valid'); pathError.textContent = '该路径已被使用'; pathError.style.display = 'block'; pathInput.focus(); return; } } const response = await fetch('/' + adminPath + '/api/subscriptions/' + originalPath, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ name: nameInput.value.trim(), path: path, action: 'update_info' }) }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '更新失败'); } showToast('订阅信息已更新'); bootstrap.Modal.getInstance(document.getElementById('editNameModal')).hide(); await loadSubscriptions(); } catch (error) { console.error('更新订阅信息失败:', error); showToast('更新失败: ' + error.message, 'danger'); } } // 添加确认删除函数 async function confirmDelete() { try { const form = document.getElementById('editNameForm'); const path = form.querySelector('input[name="originalPath"]').value; if (!path) { showToast('无效的订阅路径', 'danger'); return; } if (!confirm('确定要删除这个订阅吗?')) { return; } const response = await fetch('/' + adminPath + '/api/subscriptions/' + path, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { const result = await response.json(); throw new Error(result.message || '删除失败'); } // 关闭编辑模态框 const editModal = bootstrap.Modal.getInstance(document.getElementById('editNameModal')); if (editModal) { editModal.hide(); } showToast('订阅已删除'); await loadSubscriptions(); } catch (error) { console.error('删除失败:', error); showToast('删除失败: ' + error.message, 'danger'); } } // 表单验证工具函数(前端版本) function validateSubscriptionPathFrontend(path) { return /^[a-z0-9-]{5,50}$/.test(path); } // 添加复制到剪贴板函数 function copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => { showToast('已复制到剪贴板'); }).catch(() => { showToast('复制失败', 'danger'); }); } // 显示编辑节点模态框 function showEditNodeModal(subscriptionPath, nodeId, nodeContent) { const modal = document.getElementById('editNodeModal'); const form = document.getElementById('editNodeForm'); if (!modal || !form) { showToast('显示编辑模态框失败:找不到必要的页面元素', 'danger'); return; } // 设置表单值 form.querySelector('[name="subscriptionPath"]').value = subscriptionPath; form.querySelector('[name="nodeId"]').value = nodeId; form.setAttribute('data-original-content', nodeContent); form.querySelector('[name="content"]').value = nodeContent; // 显示模态框 new bootstrap.Modal(modal).show(); } // 更新节点 async function updateNode() { const form = document.getElementById('editNodeForm'); const formData = new FormData(form); const subscriptionPath = formData.get('subscriptionPath'); const nodeId = formData.get('nodeId'); const content = formData.get('content')?.trim(); const originalContent = form.getAttribute('data-original-content'); if (!subscriptionPath || !nodeId || !content) { showToast('请填写完整的节点信息', 'danger'); return; } // 检查内容是否被修改 if (content === originalContent) { showToast('节点内容未修改'); bootstrap.Modal.getInstance(document.getElementById('editNodeModal'))?.hide(); return; } try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/' + nodeId, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ content }) }); const result = await response.json(); if (!response.ok) throw new Error(result.message || '更新失败'); showToast('节点更新成功'); bootstrap.Modal.getInstance(document.getElementById('editNodeModal'))?.hide(); form.reset(); // 刷新数据 await Promise.all([ loadSubscriptions(), loadNodeList(subscriptionPath) ]); } catch (error) { showToast('更新节点失败: ' + error.message, 'danger'); } } // 修改前端的排序处理代码 async function updateNodeOrder(subscriptionPath, orders) { try { const response = await fetch('/' + adminPath + '/api/subscriptions/' + subscriptionPath + '/nodes/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ orders }) }); const result = await response.json(); if (!result.success) { throw new Error(result.message || '保存排序失败'); } return result; } catch (error) { console.error('更新节点排序失败:', error); } finally { // 无论成功还是失败都重新加载节点列表 await loadNodeList(subscriptionPath); } } // 安全的Base64解码函数(前端版本) function safeBase64DecodeFrontend(str) { try { // 方法1:使用atob解码,然后正确处理UTF-8编码 const decoded = atob(str); // 将解码结果转换为UTF-8字符串 const bytes = []; for (let i = 0; i < decoded.length; i++) { bytes.push(decoded.charCodeAt(i)); } // 使用TextDecoder正确解码UTF-8字符 if (typeof TextDecoder !== 'undefined') { const decoder = new TextDecoder('utf-8'); return decoder.decode(new Uint8Array(bytes)); } else { // 如果没有TextDecoder,使用escape + decodeURIComponent let utf8String = ''; for (let i = 0; i < bytes.length; i++) { utf8String += String.fromCharCode(bytes[i]); } return decodeURIComponent(escape(utf8String)); } } catch (e) { try { // 方法2:直接使用atob,可能适用于简单ASCII字符 return atob(str); } catch (e2) { // 如果Base64解码失败,返回原字符串 return str; } } } </script> </body> </html>`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); } // 通用响应头 const JSON_HEADERS = { 'Content-Type': 'application/json' }; // 通用响应创建函数 function createResponse(success, message, data = null, status = success ? 200 : 500) { return new Response( JSON.stringify({ success, message, ...(data && { data }) }), { headers: JSON_HEADERS, status } ); } // 错误响应函数 const createErrorResponse = (message, status = 500) => createResponse(false, message, null, status); // 成功响应函数 const createSuccessResponse = (data = null, message = '操作成功') => createResponse(true, message, data); // 修改获取订阅列表函数 async function handleGetSubscriptions(env) { const { results } = await env.DB.prepare(` SELECT s.path, s.name, COUNT(n.id) as nodeCount FROM subscriptions s LEFT JOIN nodes n ON s.id = n.subscription_id GROUP BY s.id ORDER BY s.id ASC `).all(); const subscriptions = results.map(item => ({ name: item.name, path: item.path, nodeCount: item.nodeCount || 0 })); return createSuccessResponse(subscriptions); } // 添加获取节点列表的函数 async function handleGetNodes(env, subscriptionPath) { const { results } = await env.DB.prepare(` SELECT n.id, n.name, n.original_link, n.node_order FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? ORDER BY n.node_order ASC `).bind(subscriptionPath).all(); return createSuccessResponse(results || []); } // 添加创建节点的函数 async function handleCreateNode(request, env, subscriptionPath) { const nodeData = await request.json(); if (!nodeData.content) { return createErrorResponse('缺少节点内容', 400); } const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.trim(); // 尝试Base64解码 try { const decodedContent = safeBase64Decode(originalLink); // 检查解码后的内容是否是有效的节点链接 if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.trim(); } } catch (e) { // 不是Base64格式,继续使用原始内容 } // 验证节点类型 const lowerContent = originalLink.toLowerCase(); const isSnell = lowerContent.includes('=') && lowerContent.includes('snell,'); if (!['ss://', 'vmess://', 'trojan://', 'vless://', 'socks://', 'hysteria2://', 'tuic://'].some(prefix => lowerContent.startsWith(prefix)) && !isSnell) { return createErrorResponse('不支持的节点格式', 400); } // 从节点链接中提取名称 let nodeName = extractNodeName(originalLink); // 直接使用提供的order值 const nodeOrder = nodeData.order; // 直接插入新节点,不更新其他节点的顺序 await env.DB.prepare(` INSERT INTO nodes (subscription_id, name, original_link, node_order) VALUES (?, ?, ?, ?) `).bind(subscriptionId, nodeName, originalLink, nodeOrder).run(); return createSuccessResponse(null, '节点创建成功'); } // 添加删除节点的函数 async function handleDeleteNode(env, subscriptionPath, nodeId) { try { const result = await env.DB.prepare(` DELETE FROM nodes WHERE id = ? AND subscription_id = ( SELECT id FROM subscriptions WHERE path = ? LIMIT 1 ) `).bind(nodeId, subscriptionPath).run(); return createSuccessResponse(null, '节点已删除'); } catch (error) { return createErrorResponse('删除节点失败: ' + error.message); } } // 生成订阅内容 async function generateSubscriptionContent(env, path) { if (!path?.trim()) return ''; const { results } = await env.DB.prepare(` SELECT GROUP_CONCAT(n.original_link, CHAR(10)) as content FROM nodes n JOIN subscriptions s ON n.subscription_id = s.id WHERE s.path = ? AND n.original_link IS NOT NULL GROUP BY s.id ORDER BY n.node_order ASC `).bind(path).all(); return results?.[0]?.content || ''; } // 解析SIP002格式 function parseSIP002Format(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; const nodeName = name ? decodeURIComponent(name) : '未命名节点'; return `${nodeName} = ss, ${server}, ${port}, encrypt-method=${method}, password=${password}`; } catch { return null; } } // 解析Vmess链接为Surge格式 function parseVmessLink(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; // 正确处理UTF-8编码的中文字符 const name = config.ps ? safeUtf8Decode(config.ps) : '未命名节点'; const configParts = [ `${name} = vmess`, config.add, config.port, `username=${config.id}`, 'vmess-aead=true', `tls=${config.tls === 'tls'}`, `sni=${config.add}`, 'skip-cert-verify=true', 'tfo=false' ]; if (config.tls === 'tls' && config.alpn) { configParts.push(`alpn=${config.alpn.replace(/,/g, ':')}`); } if (config.net === 'ws') { configParts.push('ws=true'); if (config.path) configParts.push(`ws-path=${config.path}`); configParts.push(`ws-headers=Host:${config.host || config.add}`); } return configParts.join(', '); } catch { return null; } } // 解析Trojan链接为Surge格式 function parseTrojanLink(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const configParts = [ `${nodeName} = trojan`, url.hostname, url.port, `password=${url.username}`, 'tls=true', `sni=${url.hostname}`, 'skip-cert-verify=true', 'tfo=false' ]; const alpn = params.get('alpn'); if (alpn) { configParts.push(`alpn=${safeDecodeURIComponent(alpn).replace(/,/g, ':')}`); } if (params.get('type') === 'ws') { configParts.push('ws=true'); const path = params.get('path'); if (path) { configParts.push(`ws-path=${safeDecodeURIComponent(path)}`); } const host = params.get('host'); configParts.push(`ws-headers=Host:${host ? safeDecodeURIComponent(host) : url.hostname}`); } return configParts.join(', '); } catch { return null; } } // 解析SOCKS链接为Surge格式 function parseSocksLink(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { // 处理标准格式:socks://username:password@server:port#name // 或者 socks://base64encoded@server:port#name const url = new URL(socksLink); if (!url.hostname || !url.port) return null; // 提取节点名称 const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; // 处理认证信息 let username = '', password = ''; // 专门处理 socks://base64auth@server:port 这样的格式 if (url.username) { // 首先对username进行URL解码 let decodedUsername = safeDecodeURIComponent(url.username); // 特殊处理 dXNlcm5hbWUxMjM6cGFzc3dvcmQxMjM= 这样的Base64编码认证信息 try { // 尝试Base64解码 const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { // 成功解码为 username:password 格式 const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } else { username = decodedUsername; } } else { // 不是预期的格式,使用原始值 username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } // 构建Surge格式 const configParts = [ nodeName + " = socks5", url.hostname, url.port ]; // 如果有用户名密码,添加到配置中 if (username) configParts.push(username); if (password) configParts.push(password); return configParts.join(', '); } catch (error) { return null; } } // 添加更新订阅信息的函数 async function handleUpdateSubscriptionInfo(env, path, data) { const name = data.name?.trim(); const newPath = data.path?.trim(); // 基本验证 if (!name) { return createErrorResponse('订阅名称不能为空', 400); } if (!validateSubscriptionPath(newPath)) { return createErrorResponse('无效的订阅路径格式', 400); } try { // 如果路径被修改,检查新路径是否已存在 if (newPath !== path) { const { results } = await env.DB.prepare(` SELECT 1 FROM subscriptions WHERE path = ? LIMIT 1 `).bind(newPath).all(); if (results.length > 0) { return createErrorResponse('该路径已被使用', 400); } } // 使用事务确保数据一致性 const statements = [ env.DB.prepare( "UPDATE subscriptions SET name = ?, path = ? WHERE path = ?" ).bind(name, newPath, path), env.DB.prepare( "SELECT id, name, path FROM subscriptions WHERE path = ?" ).bind(newPath) ]; const [updateResult, { results }] = await env.DB.batch(statements); if (!results?.[0]) { return createErrorResponse('更新失败:找不到订阅', 404); } return createSuccessResponse(results[0], '订阅信息已更新'); } catch (error) { return createErrorResponse('更新订阅信息失败: ' + error.message); } } // 添加删除订阅的处理函数 async function handleDeleteSubscription(env, path) { // 使用事务确保数据一致性 const statements = [ // 首先删除该订阅下的所有节点 env.DB.prepare( "DELETE FROM nodes WHERE subscription_id IN (SELECT id FROM subscriptions WHERE path = ?)" ).bind(path), // 然后删除订阅 env.DB.prepare( "DELETE FROM subscriptions WHERE path = ?" ).bind(path) ]; // 执行事务 await env.DB.batch(statements); return createSuccessResponse(null, '订阅已删除'); } // 添加更新节点的处理函数 async function handleUpdateNode(request, env, subscriptionPath, nodeId) { const nodeData = await request.json(); // 获取订阅ID const { results: subResults } = await env.DB.prepare( "SELECT id FROM subscriptions WHERE path = ?" ).bind(subscriptionPath).all(); if (!subResults?.length) { return createErrorResponse('订阅不存在', 404); } const subscriptionId = subResults[0].id; let originalLink = nodeData.content.replace(/[\r\n\s]+$/, ''); // 尝试base64解码 try { const decodedContent = safeBase64Decode(originalLink); if (Object.values(NODE_TYPES).some(prefix => decodedContent.startsWith(prefix) && prefix !== NODE_TYPES.SNELL)) { originalLink = decodedContent.replace(/[\r\n\s]+$/, ''); } } catch (e) {} // 不是base64格式,继续使用原始内容 // 使用通用的节点名称提取函数 const nodeName = extractNodeName(originalLink); // 更新节点内容和名称 await env.DB.prepare(` UPDATE nodes SET original_link = ?, name = ? WHERE id = ? AND subscription_id = ? `).bind(originalLink, nodeName || '未命名节点', nodeId, subscriptionId).run(); return createSuccessResponse(null, '节点更新成功'); } // 将订阅内容转换为surge格式 function convertToSurge(content) { if (!content?.trim()) return ''; // 使用Map来映射节点类型和处理函数,提高性能 const nodeParserMap = new Map([ [NODE_TYPES.SS, parseSIP002Format], [NODE_TYPES.VMESS, parseVmessLink], [NODE_TYPES.TROJAN, parseTrojanLink], [NODE_TYPES.SOCKS, parseSocksLink], [NODE_TYPES.HYSTERIA2, parseHysteria2ToSurge], [NODE_TYPES.TUIC, parseTuicToSurge] ]); return content .split(/\r?\n/) .map(line => { const trimmedLine = line.trim(); if (!trimmedLine) return null; // 如果已经是snell格式,格式化并返回 if (trimmedLine.includes(NODE_TYPES.SNELL)) { return formatSnellConfig(trimmedLine); } // 跳过 VLESS 节点 if (trimmedLine.toLowerCase().startsWith(NODE_TYPES.VLESS)) { return null; } // 检查是否有匹配的解析器 for (const [prefix, parser] of nodeParserMap.entries()) { if (trimmedLine.startsWith(prefix)) { return parser(trimmedLine); } } return null; }) .filter(Boolean) .join('\n'); } // 格式化snell配置 function formatSnellConfig(snellConfig) { if (!snellConfig) return null; // 分割配置字符串,保持格式一致 const parts = snellConfig.split(',').map(part => part.trim()); return parts.join(', '); } // 安全的URL解码辅助函数 function safeDecodeURIComponent(str) { try { return decodeURIComponent(str); } catch { return str; } } // 安全的Base64编码辅助函数,支持Unicode字符 function safeBase64Encode(str) { try { return btoa(unescape(encodeURIComponent(str))); } catch (e) { return str; } } // 安全的Base64解码辅助函数 function safeBase64Decode(str) { try { // 方法1:使用atob解码,然后正确处理UTF-8编码 const decoded = atob(str); // 将解码结果转换为UTF-8字符串 const bytes = []; for (let i = 0; i < decoded.length; i++) { bytes.push(decoded.charCodeAt(i)); } // 使用TextDecoder正确解码UTF-8字符 if (typeof TextDecoder !== 'undefined') { const decoder = new TextDecoder('utf-8'); return decoder.decode(new Uint8Array(bytes)); } else { // 如果没有TextDecoder,使用escape + decodeURIComponent let utf8String = ''; for (let i = 0; i < bytes.length; i++) { utf8String += String.fromCharCode(bytes[i]); } return decodeURIComponent(escape(utf8String)); } } catch (e) { try { // 方法2:直接使用atob,可能适用于简单ASCII字符 return atob(str); } catch (e2) { // 如果Base64解码失败,返回原字符串 return str; } } } // 安全的UTF-8字符串解码函数 function safeUtf8Decode(str) { if (!str) return str; try { // 方法1:使用escape + decodeURIComponent return decodeURIComponent(escape(str)); } catch (e1) { try { // 方法2:直接使用decodeURIComponent return decodeURIComponent(str); } catch (e2) { try { // 方法3:使用TextDecoder(如果支持) if (typeof TextDecoder !== 'undefined') { const encoder = new TextEncoder(); const decoder = new TextDecoder('utf-8'); return decoder.decode(encoder.encode(str)); } } catch (e3) { // 如果所有方法都失败,返回原始字符串 return str; } return str; } } } // 过滤掉snell节点的函数 function filterSnellNodes(content) { if (!content?.trim()) return ''; return content .split(/\r?\n/) .filter(line => { const trimmedLine = line.trim(); if (!trimmedLine) return false; // 过滤掉snell节点 return !trimmedLine.includes(NODE_TYPES.SNELL); }) .join('\n'); } // 将订阅内容转换为 Clash 格式 function convertToClash(content) { if (!content?.trim()) { return generateEmptyClashConfig(); } const nodes = content .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean) .map(parseNodeToClash) .filter(Boolean); return generateClashConfig(nodes); } // 解析单个节点为 Clash 格式 function parseNodeToClash(nodeLink) { if (!nodeLink) return null; const lowerLink = nodeLink.toLowerCase(); // 跳过 snell 节点,Clash 不支持 if (nodeLink.includes(NODE_TYPES.SNELL)) { return null; } // 解析 SS 节点 if (lowerLink.startsWith(NODE_TYPES.SS)) { return parseSSToClash(nodeLink); } // 解析 VMess 节点 if (lowerLink.startsWith(NODE_TYPES.VMESS)) { return parseVmessToClash(nodeLink); } // 解析 Trojan 节点 if (lowerLink.startsWith(NODE_TYPES.TROJAN)) { return parseTrojanToClash(nodeLink); } // 解析 VLESS 节点 if (lowerLink.startsWith(NODE_TYPES.VLESS)) { return parseVlessToClash(nodeLink); } // 解析 SOCKS 节点 if (lowerLink.startsWith(NODE_TYPES.SOCKS)) { return parseSocksToClash(nodeLink); } // 解析 Hysteria2 节点 if (lowerLink.startsWith(NODE_TYPES.HYSTERIA2)) { return parseHysteria2ToClash(nodeLink); } // 解析 TUIC 节点 if (lowerLink.startsWith(NODE_TYPES.TUIC)) { return parseTuicToClash(nodeLink); } return null; } // 解析 SS 节点为 Clash 格式 function parseSSToClash(ssLink) { try { const [base, name = ''] = ssLink.split('#'); if (!base.startsWith(NODE_TYPES.SS)) return null; const prefixRemoved = base.substring(5); const atIndex = prefixRemoved.indexOf('@'); if (atIndex === -1) return null; const [server, port] = prefixRemoved.substring(atIndex + 1).split(':'); if (!server || !port) return null; let method, password; const methodPassBase64 = prefixRemoved.substring(0, atIndex); try { [method, password] = safeBase64Decode(methodPassBase64).split(':'); } catch { [method, password] = safeDecodeURIComponent(methodPassBase64).split(':'); } if (!method || !password) return null; return { name: name ? decodeURIComponent(name) : '未命名节点', type: 'ss', server: server, port: parseInt(port), cipher: method, password: password }; } catch { return null; } } // 解析 VMess 节点为 Clash 格式 function parseVmessToClash(vmessLink) { if (!vmessLink.startsWith(NODE_TYPES.VMESS)) return null; try { const config = JSON.parse(safeBase64Decode(vmessLink.substring(8))); if (!config.add || !config.port || !config.id) return null; const node = { name: config.ps ? safeUtf8Decode(config.ps) : '未命名节点', type: 'vmess', server: config.add, port: parseInt(config.port), uuid: config.id, alterId: parseInt(config.aid) || 0, cipher: 'auto', tls: config.tls === 'tls' }; // 添加网络类型配置 if (config.net === 'ws') { node.network = 'ws'; if (config.path) { node['ws-opts'] = { path: config.path, headers: { Host: config.host || config.add } }; } } else if (config.net === 'grpc') { node.network = 'grpc'; if (config.path) { node['grpc-opts'] = { 'grpc-service-name': config.path }; } } // TLS 配置 if (config.tls === 'tls') { node['skip-cert-verify'] = true; if (config.sni) { node.servername = config.sni; } } return node; } catch { return null; } } // 解析 Trojan 节点为 Clash 格式 function parseTrojanToClash(trojanLink) { if (!trojanLink.startsWith(NODE_TYPES.TROJAN)) return null; try { const url = new URL(trojanLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'trojan', server: url.hostname, port: parseInt(url.port), password: url.username, 'skip-cert-verify': true }; // 添加网络类型配置 if (params.get('type') === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (params.get('type') === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 VLESS 节点为 Clash 格式 function parseVlessToClash(vlessLink) { if (!vlessLink.startsWith(NODE_TYPES.VLESS)) return null; try { const url = new URL(vlessLink); if (!url.hostname || !url.port || !url.username) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'vless', server: url.hostname, port: parseInt(url.port), uuid: url.username, tls: params.get('security') === 'tls' || params.get('security') === 'reality', 'skip-cert-verify': true, tfo: false, udp: true, 'ip-version': 'dual' }; // 添加网络类型配置 const type = params.get('type'); if (type === 'ws') { node.network = 'ws'; const path = params.get('path'); const host = params.get('host'); if (path || host) { node['ws-opts'] = {}; if (path) node['ws-opts'].path = safeDecodeURIComponent(path); if (host) { node['ws-opts'].headers = { Host: safeDecodeURIComponent(host) }; } } } else if (type === 'grpc') { node.network = 'grpc'; const serviceName = params.get('serviceName') || params.get('path'); if (serviceName) { node['grpc-opts'] = { 'grpc-service-name': safeDecodeURIComponent(serviceName) }; } } else { node.network = 'tcp'; } // Reality 配置 if (params.get('security') === 'reality') { node.reality = true; const publicKey = params.get('pbk'); if (publicKey) { node['reality-opts'] = { 'public-key': publicKey }; } } // 添加 flow 参数 const flow = params.get('flow'); if (flow) { node.flow = flow; } // 添加 client-fingerprint const fp = params.get('fp'); if (fp) { node['client-fingerprint'] = fp; } // SNI 配置 const sni = params.get('sni'); if (sni) { node.servername = safeDecodeURIComponent(sni); } return node; } catch { return null; } } // 解析 SOCKS 节点为 Clash 格式 function parseSocksToClash(socksLink) { if (!socksLink.startsWith(NODE_TYPES.SOCKS)) return null; try { const url = new URL(socksLink); if (!url.hostname || !url.port) return null; const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'socks5', server: url.hostname, port: parseInt(url.port) }; // 处理认证信息 if (url.username) { let username = '', password = ''; let decodedUsername = safeDecodeURIComponent(url.username); try { const decoded = safeBase64Decode(decodedUsername); if (decoded.includes(':')) { const parts = decoded.split(':'); if (parts.length >= 2) { username = parts[0]; password = parts[1]; } } else { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } } catch (e) { username = decodedUsername; if (url.password) { password = safeDecodeURIComponent(url.password); } } if (username) node.username = username; if (password) node.password = password; } return node; } catch { return null; } } // 生成 Clash 配置文件 function generateClashConfig(proxies) { const proxyNames = proxies.map(proxy => proxy.name); const config = { // 用于下载订阅时指定UA 'global-ua': 'clash', // 全局配置 mode: 'rule', 'mixed-port': 7890, 'allow-lan': true, // 控制面板 'external-controller': '0.0.0.0:9090', // 如果有代理节点,则包含代理节点配置 proxies: proxies.length > 0 ? proxies : [], // 策略组 'proxy-groups': [ { name: '节点选择', type: 'select', proxies: ['DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Proxy.png' }, { name: '媒体服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Netflix.png' }, { name: '微软服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Microsoft.png' }, { name: '苹果服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Apple.png' }, { name: 'CDN服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/OneDrive.png' }, { name: 'AI服务', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/ChatGPT.png' }, { name: 'Telegram', type: 'select', proxies: ['节点选择', 'DIRECT'].concat(proxyNames), icon: 'https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/Telegram.png' }, ], // 分流规则 rules: [ 'RULE-SET,reject_non_ip,REJECT', 'RULE-SET,reject_domainset,REJECT', 'RULE-SET,reject_extra_domainset,REJECT', 'RULE-SET,reject_non_ip_drop,REJECT-DROP', 'RULE-SET,reject_non_ip_no_drop,REJECT', // 域名类规则 'RULE-SET,telegram_non_ip,Telegram', 'RULE-SET,apple_cdn,苹果服务', 'RULE-SET,apple_cn_non_ip,苹果服务', 'RULE-SET,microsoft_cdn_non_ip,微软服务', 'RULE-SET,apple_services,苹果服务', 'RULE-SET,microsoft_non_ip,微软服务', 'RULE-SET,download_domainset,CDN服务', 'RULE-SET,download_non_ip,CDN服务', 'RULE-SET,cdn_domainset,CDN服务', 'RULE-SET,cdn_non_ip,CDN服务', 'RULE-SET,stream_non_ip,媒体服务', 'RULE-SET,ai_non_ip,AI服务', 'RULE-SET,global_non_ip,节点选择', 'RULE-SET,domestic_non_ip,DIRECT', 'RULE-SET,direct_non_ip,DIRECT', 'RULE-SET,lan_non_ip,DIRECT', 'GEOSITE,CN,DIRECT', // IP 类规则 'RULE-SET,reject_ip,REJECT', 'RULE-SET,telegram_ip,Telegram', 'RULE-SET,stream_ip,媒体服务', 'RULE-SET,lan_ip,DIRECT', 'RULE-SET,domestic_ip,DIRECT', 'RULE-SET,china_ip,DIRECT', 'GEOIP,LAN,DIRECT', 'GEOIP,CN,DIRECT', // 兜底规则 'MATCH,节点选择' ], // 规则提供者 'rule-providers': { reject_non_ip_no_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-no-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_no_drop.txt' }, reject_non_ip_drop: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject-drop.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip_drop.txt' }, reject_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_non_ip.txt' }, reject_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject.txt', path: './rule_set/sukkaw_ruleset/reject_domainset.txt' }, reject_extra_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/reject_extra.txt', path: './sukkaw_ruleset/reject_domainset_extra.txt' }, reject_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/reject.txt', path: './rule_set/sukkaw_ruleset/reject_ip.txt' }, cdn_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_domainset.txt' }, cdn_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/cdn.txt', path: './rule_set/sukkaw_ruleset/cdn_non_ip.txt' }, stream_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_non_ip.txt' }, stream_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/stream.txt', path: './rule_set/sukkaw_ruleset/stream_ip.txt' }, ai_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/ai.txt', path: './rule_set/sukkaw_ruleset/ai_non_ip.txt' }, telegram_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_non_ip.txt' }, telegram_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/telegram.txt', path: './rule_set/sukkaw_ruleset/telegram_ip.txt' }, apple_cdn: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/apple_cdn.txt', path: './rule_set/sukkaw_ruleset/apple_cdn.txt' }, apple_services: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_services.txt', path: './rule_set/sukkaw_ruleset/apple_services.txt' }, apple_cn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/apple_cn.txt', path: './rule_set/sukkaw_ruleset/apple_cn_non_ip.txt' }, microsoft_cdn_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft_cdn.txt', path: './rule_set/sukkaw_ruleset/microsoft_cdn_non_ip.txt' }, microsoft_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/microsoft.txt', path: './rule_set/sukkaw_ruleset/microsoft_non_ip.txt' }, download_domainset: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/domainset/download.txt', path: './rule_set/sukkaw_ruleset/download_domainset.txt' }, download_non_ip: { type: 'http', behavior: 'domain', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/download.txt', path: './rule_set/sukkaw_ruleset/download_non_ip.txt' }, lan_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_non_ip.txt' }, lan_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/lan.txt', path: './rule_set/sukkaw_ruleset/lan_ip.txt' }, domestic_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_non_ip.txt' }, direct_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/direct.txt', path: './rule_set/sukkaw_ruleset/direct_non_ip.txt' }, global_non_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/non_ip/global.txt', path: './rule_set/sukkaw_ruleset/global_non_ip.txt' }, domestic_ip: { type: 'http', behavior: 'classical', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/domestic.txt', path: './rule_set/sukkaw_ruleset/domestic_ip.txt' }, china_ip: { type: 'http', behavior: 'ipcidr', interval: 43200, format: 'text', proxy: '节点选择', url: 'https://ruleset.skk.moe/Clash/ip/china_ip.txt', path: './rule_set/sukkaw_ruleset/china_ip.txt' } } }; return `# Clash 配置文件 - Sub-Hub 自动生成 # 生成时间: ${new Date().toISOString()} ${convertToYaml(config)}`; } // 生成空的 Clash 配置 function generateEmptyClashConfig() { return generateClashConfig([]); } // 简化的对象转 YAML 函数,针对 Clash 配置优化 function convertToYaml(obj, indent = 0) { const spaces = ' '.repeat(indent); let yaml = ''; for (const [key, value] of Object.entries(obj)) { // 处理键名,只对真正需要的情况加引号 let yamlKey = key; if (key.includes(' ') || key.includes('@') || key.includes('&') || key.includes('*') || key.includes('?') || key.includes('>') || key.includes('<') || key.includes('!') || key.includes('%') || key.includes('^') || key.includes('`') || /^\d/.test(key) || key === '' || /^(true|false|null|yes|no|on|off)$/i.test(key)) { yamlKey = `"${key.replace(/"/g, '\\"')}"`; } if (value === null || value === undefined) { yaml += `${spaces}${yamlKey}: null\n`; } else if (typeof value === 'boolean') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'number') { yaml += `${spaces}${yamlKey}: ${value}\n`; } else if (typeof value === 'string') { // 对字符串值更宽松的引号判断,主要针对真正会导致 YAML 解析问题的字符 const needsQuotes = value.includes(':') || value.includes('#') || value.includes('"') || value.includes('\n') || value.includes('&') || value.includes('*') || value.includes('[') || value.includes(']') || value.includes('{') || value.includes('}') || value.includes('@') || value.includes('`') || /^\s/.test(value) || /\s$/.test(value) || value === '' || /^(true|false|null|yes|no|on|off)$/i.test(value) || (/^\d+$/.test(value) && value.length > 1) || (/^\d+\.\d+$/.test(value) && value.length > 1); if (needsQuotes) { const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces}${yamlKey}: "${escapedValue}"\n`; } else { yaml += `${spaces}${yamlKey}: ${value}\n`; } } else if (Array.isArray(value)) { if (value.length === 0) { yaml += `${spaces}${yamlKey}: []\n`; } else { yaml += `${spaces}${yamlKey}:\n`; for (const item of value) { if (typeof item === 'object' && item !== null) { yaml += `${spaces} -\n`; const itemYaml = convertToYaml(item, 0); yaml += itemYaml.split('\n').map(line => line.trim() ? `${spaces} ${line}` : '' ).filter(line => line).join('\n') + '\n'; } else if (typeof item === 'string') { // 对数组中的字符串项(如节点名称)更宽松的引号判断 const needsQuotes = item.includes(':') || item.includes('#') || item.includes('"') || item.includes('\n') || item.includes('&') || item.includes('*') || item.includes('[') || item.includes(']') || item.includes('{') || item.includes('}') || item.includes('@') || item.includes('`') || /^\s/.test(item) || /\s$/.test(item) || item === '' || /^(true|false|null|yes|no|on|off)$/i.test(item) || (/^\d+$/.test(item) && item.length > 1) || (/^\d+\.\d+$/.test(item) && item.length > 1); if (needsQuotes) { const escapedItem = item.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); yaml += `${spaces} - "${escapedItem}"\n`; } else { yaml += `${spaces} - ${item}\n`; } } else { yaml += `${spaces} - ${item}\n`; } } } } else if (typeof value === 'object' && value !== null) { yaml += `${spaces}${yamlKey}:\n`; yaml += convertToYaml(value, indent + 1); } } return yaml; } // 解析 Hysteria2 节点为 Clash 格式 function parseHysteria2ToClash(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'hysteria2', server: url.hostname, port: parseInt(url.port), password: url.username || params.get('password') || '', 'skip-cert-verify': true }; // 上传和下载速度配置 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) node.up = upMbps; if (downMbps) node.down = downMbps; // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // 混淆配置 const obfs = params.get('obfs'); if (obfs) { node.obfs = safeDecodeURIComponent(obfs); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { node['obfs-password'] = safeDecodeURIComponent(obfsPassword); } } // 拥塞控制算法 const cc = params.get('cc'); if (cc) { node.cc = cc; } return node; } catch { return null; } } // 解析 TUIC 节点为 Clash 格式 function parseTuicToClash(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const node = { name: url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点', type: 'tuic', server: url.hostname, port: parseInt(url.port), uuid: url.username || params.get('uuid') || '', password: url.password || params.get('password') || '', 'skip-cert-verify': true }; // TUIC 版本 const version = params.get('version') || params.get('v'); if (version) { node.version = parseInt(version); } // SNI 配置 const sni = params.get('sni'); if (sni) { node.sni = safeDecodeURIComponent(sni); } // ALPN 配置 const alpn = params.get('alpn'); if (alpn) { node.alpn = alpn.split(',').map(s => s.trim()); } // UDP Relay 模式 const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) { node['udp-relay-mode'] = udpRelayMode; } // 拥塞控制算法 const cc = params.get('congestion_control') || params.get('cc'); if (cc) { node['congestion-control'] = cc; } // 禁用 SNI const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { node['disable-sni'] = true; } // 减少 RTT const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { node['reduce-rtt'] = true; } return node; } catch { return null; } } // 解析 Hysteria2 链接为 Surge 格式 function parseHysteria2ToSurge(hysteria2Link) { if (!hysteria2Link.startsWith(NODE_TYPES.HYSTERIA2)) return null; try { const url = new URL(hysteria2Link); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const password = url.username || params.get('password') || ''; // 构建 Surge 格式的 Hysteria2 配置 const configParts = [ `${nodeName} = hysteria2`, url.hostname, url.port, `password=${password}` ]; // 添加可选参数 const upMbps = params.get('upmbps') || params.get('up'); const downMbps = params.get('downmbps') || params.get('down'); if (upMbps) configParts.push(`up=${upMbps}`); if (downMbps) configParts.push(`down=${downMbps}`); const sni = params.get('sni'); if (sni) configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); const obfs = params.get('obfs'); if (obfs) { configParts.push(`obfs=${safeDecodeURIComponent(obfs)}`); const obfsPassword = params.get('obfs-password'); if (obfsPassword) { configParts.push(`obfs-password=${safeDecodeURIComponent(obfsPassword)}`); } } const cc = params.get('cc'); if (cc) configParts.push(`cc=${cc}`); // 默认跳过证书验证 configParts.push('skip-cert-verify=true'); return configParts.join(', '); } catch (error) { return null; } } // 解析 TUIC 链接为 Surge 格式 function parseTuicToSurge(tuicLink) { if (!tuicLink.startsWith(NODE_TYPES.TUIC)) return null; try { const url = new URL(tuicLink); if (!url.hostname || !url.port) return null; const params = new URLSearchParams(url.search); const nodeName = url.hash ? decodeURIComponent(url.hash.substring(1)) : '未命名节点'; const uuid = url.username || params.get('uuid') || ''; const password = url.password || params.get('password') || ''; // 构建 Surge 格式的 TUIC 配置 const configParts = [ `${nodeName} = tuic`, url.hostname, url.port, `uuid=${uuid}`, `password=${password}` ]; // TUIC 版本 - 如果没有指定版本,默认使用版本 5 const version = params.get('version') || params.get('v') || '5'; configParts.push(`version=${version}`); // SNI 配置 - 如果没有指定 SNI,使用服务器地址作为 SNI const sni = params.get('sni') || url.hostname; configParts.push(`sni=${safeDecodeURIComponent(sni)}`); const alpn = params.get('alpn'); if (alpn) configParts.push(`alpn=${alpn}`); // 处理 allow_insecure 参数 const allowInsecure = params.get('allow_insecure') || params.get('allowInsecure'); if (allowInsecure === 'true' || allowInsecure === '1') { configParts.push('skip-cert-verify=true'); } else { // 如果没有明确设置 allow_insecure,默认为 false configParts.push('skip-cert-verify=false'); } const udpRelayMode = params.get('udp_relay_mode') || params.get('udp-relay-mode'); if (udpRelayMode) configParts.push(`udp-relay-mode=${udpRelayMode}`); const cc = params.get('congestion_control') || params.get('congestion-control') || params.get('cc'); if (cc) configParts.push(`congestion-control=${cc}`); const disableSni = params.get('disable_sni'); if (disableSni === 'true' || disableSni === '1') { configParts.push('disable-sni=true'); } const reduceRtt = params.get('reduce_rtt'); if (reduceRtt === 'true' || reduceRtt === '1') { configParts.push('reduce-rtt=true'); } return configParts.join(', '); } catch (error) { return null; } } {dotted startColor="#ff6c6c" endColor="#1989fa"/}访问系统访问管理面板:https://你的域名/ADMIN_PATH订阅地址格式:原始格式:https://你的域名/订阅路径Base64 格式:https://你的域名/订阅路径/v2raySurge 格式:https://你的域名/订阅路径/surgeClash 格式:https://你的域名/订阅路径/clash{dotted startColor="#ff6c6c" endColor="#1989fa"/}使用说明创建订阅登录管理面板点击"添加订阅"按钮输入订阅名称和路径(路径只能包含小写字母、数字和连字符)点击"创建"按钮管理节点在订阅列表中找到目标订阅点击"添加节点"按钮添加新节点支持以下格式:单个节点链接多个节点链接(每行一个)Base64 编码的节点列表节点排序点击"节点列表"按钮查看节点拖拽节点行可以调整顺序顺序会自动保存批量操作点击"批量删除"按钮进入批量模式勾选要删除的节点点击"确认删除"执行删除操作注意事项首次部署后请立即修改默认的管理员密码定期备份数据库内容妥善保管管理面板地址和登录信息建议使用强密码提高安全性
2025年05月28日
0 阅读
0 评论
0 点赞
2024-09-08
部署在CF的轻量化导航页面,可移动卡片式书签,方便管理
部署在CF的轻量化导航页面,可移动卡片式书签,方便管理 github项目 Card-Tab 书签卡片式管理,进入管理模式可以自由移动书签位置,添加和删除书签,支持自定义网站分类,支持切换黑暗色主题一、cloudflare workes部署 现用代码 ,nodeseek风格const HTML_CONTENT = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ws01主页</title> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>💠</text></svg>"> <style> /* 全局样式 */ body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #d4d4d4; transition: background-color 0.3s ease; } ul { padding: 0; margin-block-start: 1em; margin-block-end: 1em; margin-inline-start: 0px; margin-inline-end: 0px; padding-inline-start: 40px; unicode-bidi: isolate; } li { display: list-item; text-align: -webkit-match-parent; unicode-bidi: isolate; margin: 0 8px; } .background { background-image: linear-gradient(#d4d4d4 1px, transparent 0), linear-gradient(90deg, #d4d4d4 1px, transparent 0); background-size: 32px 32px; background-color: #fffcf8; } .header { background-color: #fff; box-shadow: 0 0 5px rgba(0, 0, 0, .1); transition: background-color .5s; } .navbar { display: flex; width: 1280px; margin: auto; height: 40px; } .navbar .brand { display: flex; align-items: center; color: #555; } .brand .logo { max-width: 36px; } .brand .title { margin-left: 5px; font-family: helvetica neue, helvetica, arial, sans-serif; font-size: 32px; font-weight: 700; } .beta { color: #ccc; font-size: 11px; font-weight: 400; position: relative; top: -14px; } .category-list { list-style: none; display: flex; align-items: center; } .sites { width:1286px; background:; border:2px solid auto; margin:15px auto; padding:0px; text-align:center; } .sites1 { width:1286px; background:; border:2px solid auto; margin:15px auto; padding:0px; } .sites dl { height:36px; line-height:36px; display:block; margin:0; } .sites dl.alt { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dl.alt2 { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dt,.sites dd { text-align:center; display:block; float:left; } .sites dt { width:60px; } .sites dd { width:90px; margin:0; } .footer { width:580px; text-align:center; margin:5px auto; padding:2px; } /* 固定元素样式 */ .fixed-elements { position: fixed; top: 0; left: 0; right: 0; background-color: #d4d4d4; z-index: 1000; padding: 10px; transition: background-color 0.3s ease; height: 130px; } .fixed-elements h3 { position: absolute; top: 10px; left: 20px; margin: 0; } /* 中心内容样式 */ .center-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; max-width: 600px; text-align: center; } /* 管理员控制面板样式 */ .admin-controls { position: fixed; top: 10px; right: 10px; font-size: 60%; } /* 添加/删除控制按钮样式 */ .add-remove-controls { display: none; flex-direction: column; position: fixed; right: 20px; top: 50%; transform: translateY(-50%); align-items: center; gap: 10px; } .round-btn { background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; margin: 5px 0; } .add-btn { order: 1; } .remove-btn { order: 2; } .category-btn { order: 3; } .remove-category-btn { order: 4; } /* 主要内容区域样式 */ .content { margin-top: 140px; padding: 20px; } /* 主题切换按钮样式 */ #theme-toggle { position: fixed; bottom: 50px; right: 20px; background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; } /* 显示日志按钮样式 */ #view-logs-btn { position: fixed; top: 100px; right: 10px; z-index: 1000; } /* 对话框样式 */ #dialog-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } #dialog-box { background-color: white; padding: 20px; border-radius: 5px; width: 300px; } #dialog-box input, #dialog-box select { width: 100%; margin-bottom: 10px; padding: 5px; } /* 分类和卡片样式 */ .section { margin-bottom: 20px; } .section-title-container { display: flex; align-items: center; margin-bottom: 10px; } .section-title { font-size: 32px; font-weight: bold; margin-left: 20px; /* 分类标签右移添加这一行 */ color: green; /* 分类标签字体颜色添加这一行 */ } .delete-category-btn { background-color: pink; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer; } .card-container { display: flex; flex-wrap: wrap; gap: 10px; } .card { background-color: rgba(160, 201, 229, 0.6); border-radius: 5px; padding: 10px; width: 182px; border: 2px solid gray; /* 添加2px灰色边框 */ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); cursor: pointer; transition: transform 0.2s; position: relative; } /* 中等屏幕 (平板等) */ @media (max-width: 1024px) { .card { width: 225px; } } /* 小屏幕 (手机) */ @media (max-width: 768px) { .card { width: 224px; } } .card:hover { transform: translateY(-5px); } .card-top { display: flex; align-items: center; margin-bottom: 5px; } .card-icon { width: 20px; height: 20px; margin-right: 5px; } .card-title { font-size: 18px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-url { font-size: 14px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: left; /* 添加左对齐 */ } /* 超小屏幕 (更小手机) */ @media (max-width: 480px) { .card { padding: 24px; /* 进一步减小卡片内边距6 */ width: 368px; /* 手机上显示4列,235时显示6列 */ } .card-icon { width: 46px; height: 46px; } .card-title { font-size: 40px; } .card-url { font-size: 22px; } } .private-tag { background-color: #ff9800; color: white; font-size: 10px; padding: 2px 5px; border-radius: 3px; position: absolute; top: 5px; right: 5px; } .delete-btn { position: absolute; top: -10px; right: -10px; background-color: red; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; text-align: center; font-size: 14px; line-height: 20px; cursor: pointer; display: none; } /* 版权信息样式 */ #copyright { position: fixed; bottom: 0; left: 0; width: 100%; height: 40px; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; font-size: 14px; z-index: 1000; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); } #copyright p { margin: 0; } #copyright a { color: #007bff; text-decoration: none; } #copyright a:hover { text-decoration: underline; } </style> </head> <body class="background"> </div> <div class="sites"> <!-- 00 --> <header class="header"> <nav class="navbar"> <a href="https://www.211119.xyz/" class="brand"> <img class="debug logo" src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299"> <span class="debug title">WS01の主页</span> <span class="debug beta">beta</span> <!-- 一言模块 --> <p id="hitokoto"> <a href="#" id="hitokoto_text"></a> </p> <script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script> </a> </header> </div> </div> </div> <div class="head"> <!-- 添加/删除控制按钮 --> <div class="add-remove-controls"> <button class="round-btn add-btn" onclick="showAddDialog()">+</button> <button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button> <button class="round-btn category-btn" onclick="addCategory()">C+</button> <button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button> </div> <div class="sites1"> <!-- 00 --> <!-- 分类和卡片容器 --> <div id="sections-container"></div> <!-- 主题切换按钮 --> <button id="theme-toggle" onclick="toggleTheme()">◑</button> <!-- 显示日志按钮 用于调试--> <!--<button id="view-logs-btn" onclick="viewLogs()">显示日志</button>--> <!-- 添加链接对话框 --> <div id="dialog-overlay"> <div id="dialog-box"> <label for="name-input">名称</label> <input type="text" id="name-input"> <label for="url-input">地址</label> <input type="text" id="url-input"> <label for="category-select">选择分类</label> <select id="category-select"></select> <div class="private-link-container"> <label for="private-checkbox">私密链接</label> <input type="checkbox" id="private-checkbox"> </div> <button onclick="addLink()">确定</button> <button onclick="hideAddDialog()">取消</button> </div> </div> <br /> <!-- body 页脚 --> <div class="footer"> <!-- 管理员控制面板 --> <input type="password" id="admin-password" placeholder="输入密码"> <button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button> <button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button> <br /> <br /> <br /> <!-- 版权信息 --> <div id="copyright" class="copyright"> <span id="timeDate">载入天数...</span> <script language="javascript"> var now = new Date(); function createtime(){ var grt= new Date("01/05/2025 00:00:00");/*---这里是网站的启用时间--*/ now.setTime(now.getTime()+250); days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days); document.getElementById("timeDate").innerHTML = "稳定运行"+dnum+"天"; } setInterval("createtime()",250); </script> <span <p> | 本页总访问量 <span id="busuanzi_site_pv"></span> 次 | 开源于 <a href="https://github.com/hmhm2022/Card-Tab" target="_blank">@GitHub</a></p></span> <script defer src="https://bsz.211119.xyz/js"></script> </p> </div> </div> <script> // 日志记录函数 function logAction(action, details) { const timestamp = new Date().toISOString(); const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details); let logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); logs.push(logEntry); // 保留最新的1000条日志 if (logs.length > 1000) { logs = logs.slice(-1000); } localStorage.setItem('cardTabLogs', JSON.stringify(logs)); console.log(logEntry); // 同时在控制台输出日志 } // 查看日志的函数 function viewLogs() { const logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); console.log('Card Tab Logs:'); logs.forEach(log => console.log(log)); alert('日志已在控制台输出,请按F12打开开发者工具查看。'); } // 全局变量 let publicLinks = []; let privateLinks = []; let isAdmin = false; let isLoggedIn = false; let removeMode = false; let isRemoveCategoryMode = false; let isDarkTheme = false; let links = []; const categories = {}; // 添加新分类 async function addCategory() { if (!await validateToken()) { return; } const categoryName = prompt('请输入新分类名称:'); if (categoryName && !categories[categoryName]) { categories[categoryName] = []; updateCategorySelect(); renderCategories(); saveLinks(); logAction('添加分类', { categoryName, currentLinkCount: links.length }); } else if (categories[categoryName]) { alert('该分类已存在'); logAction('添加分类失败', { categoryName, reason: '分类已存在' }); } } // 删除分类 async function deleteCategory(category) { if (!await validateToken()) { return; } if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) { delete categories[category]; links = links.filter(link => link.category !== category); publicLinks = publicLinks.filter(link => link.category !== category); privateLinks = privateLinks.filter(link => link.category !== category); updateCategorySelect(); saveLinks(); renderCategories(); logAction('删除分类', { category }); } } // 渲染分类(不重新加载链接) function renderCategories() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); container.appendChild(section); const categoryLinks = links.filter(link => link.category === category); categoryLinks.forEach(link => { createCard(link, cardContainer); }); }); logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length }); } // 读取链接数据 async function loadLinks() { const headers = { 'Content-Type': 'application/json' }; // 如果已登录,从 localStorage 获取 token 并添加到请求头 if (isLoggedIn) { const token = localStorage.getItem('authToken'); if (token) { headers['Authorization'] = token; } } try { const response = await fetch('/api/getLinks?userId=testUser', { headers: headers }); if (!response.ok) { throw new Error("HTTP error! status: " + response.status); } const data = await response.json(); console.log('Received data:', data); if (data.categories) { Object.assign(categories, data.categories); } publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : []; privateLinks = data.links ? data.links.filter(link => link.isPrivate) : []; links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; loadSections(); updateCategorySelect(); updateUIState(); logAction('读取链接', { publicCount: publicLinks.length, privateCount: privateLinks.length, isLoggedIn: isLoggedIn, hasToken: !!localStorage.getItem('authToken') }); } catch (error) { console.error('Error loading links:', error); alert('加载链接时出错,请刷新页面重试'); } } // 更新UI状态 function updateUIState() { const passwordInput = document.getElementById('admin-password'); const adminBtn = document.getElementById('admin-mode-btn'); const secretGardenBtn = document.getElementById('secret-garden-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block'; secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录"; secretGardenBtn.style.display = 'inline-block'; if (isAdmin) { adminBtn.textContent = "离开设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'flex'; } else if (isLoggedIn) { adminBtn.textContent = "设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'none'; } else { adminBtn.style.display = 'none'; addRemoveControls.style.display = 'none'; } logAction('更新UI状态', { isAdmin, isLoggedIn }); } // 登录状态显示(加载所有链接) function showSecretGarden() { if (isLoggedIn) { links = [...publicLinks, ...privateLinks]; loadSections(); // 显示所有私密标签 document.querySelectorAll('.private-tag').forEach(tag => { tag.style.display = 'block'; }); logAction('显示私密花园'); } } // 加载分类和链接 function loadSections() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); let privateCount = 0; let linkCount = 0; links.forEach(link => { if (link.category === category) { if (link.isPrivate) privateCount++; linkCount++; createCard(link, cardContainer); } }); if (privateCount < linkCount || isLoggedIn) { container.appendChild(section); } }); logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length }); } // 创建卡片 function createCard(link, container) { const card = document.createElement('div'); card.className = 'card'; card.setAttribute('draggable', isAdmin); card.dataset.isPrivate = link.isPrivate; const cardTop = document.createElement('div'); cardTop.className = 'card-top'; // 定义默认的 SVG 图标 const defaultIconSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>' + '<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>' + '</svg>'; // 创建图标元素 const icon = document.createElement('img'); icon.className = 'card-icon'; // icon.src = 'https://api.iowen.cn/favicon/' + extractDomain(link.url) + '.png'; icon.src = 'https://www.faviconextractor.com/favicon/' + extractDomain(link.url); icon.alt = 'Website Icon'; // 如果图片加载失败,使用默认的 SVG 图标 icon.onerror = function() { const svgBlob = new Blob([defaultIconSVG], {type: 'image/svg+xml'}); const svgUrl = URL.createObjectURL(svgBlob); this.src = svgUrl; this.onload = () => URL.revokeObjectURL(svgUrl); }; function extractDomain(url) { let domain; try { domain = new URL(url).hostname; } catch (e) { domain = url; } return domain; } const title = document.createElement('div'); title.className = 'card-title'; title.textContent = link.name; cardTop.appendChild(icon); cardTop.appendChild(title); const url = document.createElement('div'); url.className = 'card-url'; url.textContent = link.url; card.appendChild(cardTop); card.appendChild(url); if (link.isPrivate) { const privateTag = document.createElement('div'); privateTag.className = 'private-tag'; privateTag.textContent = '私密'; card.appendChild(privateTag); } const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url; if (!isAdmin) { card.addEventListener('click', () => { window.open(correctedUrl, '_blank'); logAction('打开链接', { name: link.name, url: correctedUrl }); }); } const deleteBtn = document.createElement('button'); deleteBtn.textContent = '–'; deleteBtn.className = 'delete-btn'; deleteBtn.onclick = function (event) { event.stopPropagation(); removeCard(card); }; card.appendChild(deleteBtn); updateCardStyle(card); card.addEventListener('dragstart', dragStart); card.addEventListener('dragover', dragOver); card.addEventListener('dragend', dragEnd); card.addEventListener('drop', drop); card.addEventListener('touchstart', touchStart, { passive: false }); if (isAdmin && removeMode) { deleteBtn.style.display = 'block'; } if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) { container.appendChild(card); } // logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate }); } // 更新卡片样式 function updateCardStyle(card) { if (isDarkTheme) { card.style.backgroundColor = 'rgba(30, 30, 30, 0.5)'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; } else { card.style.backgroundColor = 'rgba(160, 201, 229, 0.6)'; card.style.color = '#333'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; } } // 更新分类选择下拉框 function updateCategorySelect() { const categorySelect = document.getElementById('category-select'); categorySelect.innerHTML = ''; Object.keys(categories).forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; categorySelect.appendChild(option); }); logAction('更新分类选择', { categoryCount: Object.keys(categories).length }); } // 保存链接数据 async function saveLinks() { if (isAdmin && !(await validateToken())) { return; } let allLinks = [...publicLinks, ...privateLinks]; try { await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('authToken') }, body: JSON.stringify({ userId: 'testUser', links: allLinks, categories: categories }), }); logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length }); } catch (error) { logAction('保存链接失败', { error: error.message }); alert('保存链接失败,请重试'); } } // 添加卡片弹窗 async function addLink() { if (!await validateToken()) { return; } const name = document.getElementById('name-input').value; const url = document.getElementById('url-input').value; const category = document.getElementById('category-select').value; const isPrivate = document.getElementById('private-checkbox').checked; if (name && url && category) { const newLink = { name, url, category, isPrivate }; if (isPrivate) { privateLinks.push(newLink); } else { publicLinks.push(newLink); } links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) { const container = document.getElementById(category); if (container) { createCard(newLink, container); } else { categories[category] = []; renderCategories(); } } saveLinks(); document.getElementById('name-input').value = ''; document.getElementById('url-input').value = ''; document.getElementById('private-checkbox').checked = false; hideAddDialog(); logAction('添加卡片', { name, url, category, isPrivate }); } } // 删除卡片 async function removeCard(card) { if (!await validateToken()) { return; } const name = card.querySelector('.card-title').textContent; const url = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; links = links.filter(link => link.url !== url); if (isPrivate) { privateLinks = privateLinks.filter(link => link.url !== url); } else { publicLinks = publicLinks.filter(link => link.url !== url); } for (const key in categories) { categories[key] = categories[key].filter(link => link.url !== url); } card.remove(); saveLinks(); logAction('删除卡片', { name, url, isPrivate }); } // 拖拽卡片 let draggedCard = null; let touchStartX, touchStartY; // 触屏端拖拽卡片 function touchStart(event) { if (!isAdmin) { return; } draggedCard = event.target.closest('.card'); if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; touchStartX = touch.clientX; touchStartY = touch.clientY; draggedCard.classList.add('dragging'); document.addEventListener('touchmove', touchMove, { passive: false }); document.addEventListener('touchend', touchEnd); } function touchMove(event) { if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; const currentX = touch.clientX; const currentY = touch.clientY; const deltaX = currentX - touchStartX; const deltaY = currentY - touchStartY; draggedCard.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)"; const target = findCardUnderTouch(currentX, currentY); if (target && target !== draggedCard) { const container = target.parentElement; const targetRect = target.getBoundingClientRect(); if (currentX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } function touchEnd(event) { if (!draggedCard) return; const card = draggedCard; const targetCategory = card.closest('.card-container').id; validateToken().then(isValid => { if (isValid && card) { updateCardCategory(card, targetCategory); saveCardOrder().catch(error => { console.error('Save failed:', error); }); } cleanupDragState(); }); } function findCardUnderTouch(x, y) { const cards = document.querySelectorAll('.card:not(.dragging)'); return Array.from(cards).find(card => { const rect = card.getBoundingClientRect(); return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; }); } // PC端拖拽卡片 function dragStart(event) { if (!isAdmin) { event.preventDefault(); return; } draggedCard = event.target.closest('.card'); if (!draggedCard) return; draggedCard.classList.add('dragging'); event.dataTransfer.effectAllowed = "move"; logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent }); } function dragOver(event) { if (!isAdmin) { event.preventDefault(); return; } event.preventDefault(); const target = event.target.closest('.card'); if (target && target !== draggedCard) { const container = target.parentElement; const mousePositionX = event.clientX; const targetRect = target.getBoundingClientRect(); if (mousePositionX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } // 清理拖拽状态函数 function cleanupDragState() { if (draggedCard) { draggedCard.classList.remove('dragging'); draggedCard.style.transform = ''; draggedCard = null; } document.removeEventListener('touchmove', touchMove); document.removeEventListener('touchend', touchEnd); touchStartX = null; touchStartY = null; } // PC端拖拽结束 function drop(event) { if (!isAdmin) { event.preventDefault(); return; } event.preventDefault(); const card = draggedCard; const targetCategory = event.target.closest('.card-container').id; validateToken().then(isValid => { if (isValid && card) { updateCardCategory(card, targetCategory); saveCardOrder().catch(error => { console.error('Save failed:', error); }); } cleanupDragState(); }); } function dragEnd(event) { if (draggedCard) { draggedCard.classList.remove('dragging'); logAction('拖拽卡片结束'); } } // 更新卡片分类 function updateCardCategory(card, newCategory) { const cardTitle = card.querySelector('.card-title').textContent; const cardUrl = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; const linkIndex = links.findIndex(link => link.url === cardUrl); if (linkIndex !== -1) { links[linkIndex].category = newCategory; } const linkArray = isPrivate ? privateLinks : publicLinks; const arrayIndex = linkArray.findIndex(link => link.url === cardUrl); if (arrayIndex !== -1) { linkArray[arrayIndex].category = newCategory; } card.dataset.category = newCategory; } // 在页面加载完成后添加触摸事件监听器 document.addEventListener('DOMContentLoaded', function() { const cardContainers = document.querySelectorAll('.card-container'); cardContainers.forEach(container => { container.addEventListener('touchstart', touchStart, { passive: false }); }); }); // 保存卡片顺序 async function saveCardOrder() { if (!await validateToken()) { return; } const containers = document.querySelectorAll('.card-container'); let newPublicLinks = []; let newPrivateLinks = []; let newCategories = {}; containers.forEach(container => { const category = container.id; newCategories[category] = []; [...container.children].forEach(card => { const url = card.querySelector('.card-url').textContent; const name = card.querySelector('.card-title').textContent; const isPrivate = card.dataset.isPrivate === 'true'; card.dataset.category = category; const link = { name, url, category, isPrivate }; if (isPrivate) { newPrivateLinks.push(link); } else { newPublicLinks.push(link); } newCategories[category].push(link); }); }); publicLinks.length = 0; publicLinks.push(...newPublicLinks); privateLinks.length = 0; privateLinks.push(...newPrivateLinks); Object.keys(categories).forEach(key => delete categories[key]); Object.assign(categories, newCategories); logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); try { const response = await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('authToken') }, body: JSON.stringify({ userId: 'testUser', links: [...newPublicLinks, ...newPrivateLinks], categories: newCategories }), }); const result = await response.json(); if (!result.success) { throw new Error('Failed to save order'); } logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); } catch (error) { logAction('保存顺序失败', { error: error.message }); alert('保存顺序失败,请重试'); } } // 设置状态重新加载卡片 function reloadCardsAsAdmin() { document.querySelectorAll('.card-container').forEach(container => { container.innerHTML = ''; }); loadLinks().then(() => { if (isDarkTheme) { applyDarkTheme(); } }); logAction('重新加载卡片(管理员模式)'); } // 密码输入框回车事件 document.getElementById('admin-password').addEventListener('keypress', (e) => { if (e.key === 'Enter') { toggleSecretGarden(); } }); // 切换设置状态 async function toggleAdminMode() { const adminBtn = document.getElementById('admin-mode-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); if (!isAdmin && isLoggedIn) { if (!await validateToken()) { return; } // 在进入设置模式之前进行备份 try { const response = await fetch('/api/backupData', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('authToken') }, body: JSON.stringify({ sourceUserId: 'testUser', backupUserId: 'backup' }), }); const result = await response.json(); if (result.success) { logAction('数据备份成功'); } else { throw new Error('备份失败'); } } catch (error) { logAction('数据备份失败', { error: error.message }); if (!confirm('备份失败,是否仍要继续进入设置模式?')) { return; } } isAdmin = true; adminBtn.textContent = "退出设置"; addRemoveControls.style.display = 'flex'; alert('准备设置分类和书签'); reloadCardsAsAdmin(); logAction('进入设置'); } else if (isAdmin) { isAdmin = false; removeMode = false; adminBtn.textContent = "设 置"; addRemoveControls.style.display = 'none'; alert('设置已保存'); reloadCardsAsAdmin(); logAction('离开设置'); } updateUIState(); } // 切换到登录状态 function toggleSecretGarden() { const passwordInput = document.getElementById('admin-password'); if (!isLoggedIn) { verifyPassword(passwordInput.value) .then(result => { if (result.valid) { isLoggedIn = true; localStorage.setItem('authToken', result.token); console.log('Token saved:', result.token); loadLinks(); alert('登录成功!'); logAction('登录成功'); } else { alert('密码错误'); logAction('登录失败', { reason: result.error || '密码错误' }); } updateUIState(); }) .catch(error => { console.error('Login error:', error); alert('登录过程出错,请重试'); }); } else { isLoggedIn = false; isAdmin = false; localStorage.removeItem('authToken'); links = publicLinks; loadSections(); alert('退出登录!'); updateUIState(); passwordInput.value = ''; logAction('退出登录'); } } // 应用暗色主题 function applyDarkTheme() { document.body.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'; document.body.style.color = '#ffffff'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; }); logAction('应用暗色主题'); } // 显示添加链接对话框 function showAddDialog() { document.getElementById('dialog-overlay').style.display = 'flex'; logAction('显示添加链接对话框'); } // 隐藏添加链接对话框 function hideAddDialog() { document.getElementById('dialog-overlay').style.display = 'none'; logAction('隐藏添加链接对话框'); } // 切换删除卡片模式 function toggleRemoveMode() { removeMode = !removeMode; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => { btn.style.display = removeMode ? 'block' : 'none'; }); logAction('切换删除卡片模式', { removeMode }); } //切换删除分类模式 function toggleRemoveCategory() { isRemoveCategoryMode = !isRemoveCategoryMode; const deleteButtons = document.querySelectorAll('.delete-category-btn'); deleteButtons.forEach(btn => { btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; }); logAction('切换删除分类模式', { isRemoveCategoryMode }); } // 切换主题 function toggleTheme() { isDarkTheme = !isDarkTheme; document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff'; document.body.style.color = isDarkTheme ? '#ffffff' : '#333'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = isDarkTheme ? 'rgba(30, 30, 30, 0.9)' : 'rgba(160, 201, 229, 0.6)'; card.style.color = isDarkTheme ? '#ffffff' : '#333'; card.style.boxShadow = isDarkTheme ? '0 4px 8px rgba(0, 0, 0, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.1)'; }); const fixedElements = document.querySelectorAll('.fixed-elements'); fixedElements.forEach(element => { element.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff'; element.style.color = isDarkTheme ? '#ffffff' : '#333'; }); const dialogBox = document.getElementById('dialog-box'); dialogBox.style.backgroundColor = isDarkTheme ? 'rgba(30, 30, 30, 0.9)' : '#ffffff'; dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333'; const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select'); inputs.forEach(input => { input.style.backgroundColor = isDarkTheme ? '#444' : '#fff'; input.style.color = isDarkTheme ? '#fff' : '#333'; input.style.borderColor = isDarkTheme ? '#555' : '#ccc'; }); logAction('切换主题', { isDarkTheme }); } // 验证密码 async function verifyPassword(inputPassword) { const response = await fetch('/api/verifyPassword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: inputPassword }), }); const result = await response.json(); return result; } // 初始化加载 document.addEventListener('DOMContentLoaded', async () => { await validateToken(); loadLinks(); }); // 前端检查是否有 token async function validateToken() { const token = localStorage.getItem('authToken'); if (!token) { isLoggedIn = false; updateUIState(); return false; } try { const response = await fetch('/api/getLinks?userId=testUser', { headers: { 'Authorization': token } }); if (response.status === 401) { await resetToLoginState('token已过期,请重新登录'); return false; } isLoggedIn = true; updateUIState(); return true; } catch (error) { console.error('Token validation error:', error); return false; } } // 重置状态 async function resetToLoginState(message) { alert(message); cleanupDragState(); localStorage.removeItem('authToken'); isLoggedIn = false; isAdmin = false; removeMode = false; isRemoveCategoryMode = false; const passwordInput = document.getElementById('admin-password'); if (passwordInput) { passwordInput.value = ''; } updateUIState(); links = publicLinks; loadSections(); const addRemoveControls = document.querySelector('.add-remove-controls'); if (addRemoveControls) { addRemoveControls.style.display = 'none'; } document.querySelectorAll('.delete-btn').forEach(btn => { btn.style.display = 'none'; }); document.querySelectorAll('.delete-category-btn').forEach(btn => { btn.style.display = 'none'; }); const dialogOverlay = document.getElementById('dialog-overlay'); if (dialogOverlay) { dialogOverlay.style.display = 'none'; } } </script> </body> </html> `; // 服务端 token 验证 async function validateServerToken(authToken, env) { if (!authToken) { return { isValid: false, status: 401, response: { error: 'Unauthorized', message: '未登录或登录已过期' } }; } try { const [timestamp, hash] = authToken.split('.'); const tokenTimestamp = parseInt(timestamp); const now = Date.now(); const FIFTEEN_MINUTES = 15 * 60 * 1000; if (now - tokenTimestamp > FIFTEEN_MINUTES) { return { isValid: false, status: 401, response: { error: 'Token expired', tokenExpired: true, message: '登录已过期,请重新登录' } }; } const tokenData = timestamp + "_" + env.ADMIN_PASSWORD; const encoder = new TextEncoder(); const data = encoder.encode(tokenData); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const expectedHash = btoa(String.fromCharCode(...new Uint8Array(hashBuffer))); if (hash !== expectedHash) { return { isValid: false, status: 401, response: { error: 'Invalid token', tokenInvalid: true, message: '登录状态无效,请重新登录' } }; } return { isValid: true }; } catch (error) { return { isValid: false, status: 401, response: { error: 'Invalid token', tokenInvalid: true, message: '登录验证失败,请重新登录' } }; } } export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/') { return new Response(HTML_CONTENT, { headers: { 'Content-Type': 'text/html' } }); } if (url.pathname === '/api/getLinks') { const userId = url.searchParams.get('userId'); const authToken = request.headers.get('Authorization'); const data = await env.CARD_ORDER.get(userId); if (data) { const parsedData = JSON.parse(data); // 验证 token if (authToken) { const validation = await validateServerToken(authToken, env); if (!validation.isValid) { return new Response(JSON.stringify(validation.response), { status: validation.status, headers: { 'Content-Type': 'application/json' } }); } // Token 有效,返回完整数据 return new Response(JSON.stringify(parsedData), { status: 200, headers: { 'Content-Type': 'application/json' } }); } // 未提供 token,只返回公开数据 const filteredLinks = parsedData.links.filter(link => !link.isPrivate); const filteredCategories = {}; Object.keys(parsedData.categories).forEach(category => { filteredCategories[category] = parsedData.categories[category].filter(link => !link.isPrivate); }); return new Response(JSON.stringify({ links: filteredLinks, categories: filteredCategories }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ links: [], categories: {} }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/saveOrder' && request.method === 'POST') { const authToken = request.headers.get('Authorization'); const validation = await validateServerToken(authToken, env); if (!validation.isValid) { return new Response(JSON.stringify(validation.response), { status: validation.status, headers: { 'Content-Type': 'application/json' } }); } const { userId, links, categories } = await request.json(); await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories })); return new Response(JSON.stringify({ success: true, message: '保存成功' }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/verifyPassword' && request.method === 'POST') { try { const { password } = await request.json(); const isValid = password === env.ADMIN_PASSWORD; if (isValid) { // 生成包含时间戳的加密 token const timestamp = Date.now(); const tokenData = timestamp + "_" + env.ADMIN_PASSWORD; const encoder = new TextEncoder(); const data = encoder.encode(tokenData); const hashBuffer = await crypto.subtle.digest('SHA-256', data); // 使用指定格式:timestamp.hash const token = timestamp + "." + btoa(String.fromCharCode(...new Uint8Array(hashBuffer))); return new Response(JSON.stringify({ valid: true, token: token }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ valid: false, error: 'Invalid password' }), { status: 403, headers: { 'Content-Type': 'application/json' } }); } catch (error) { return new Response(JSON.stringify({ valid: false, error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } if (url.pathname === '/api/backupData' && request.method === 'POST') { const { sourceUserId } = await request.json(); const result = await this.backupData(env, sourceUserId); return new Response(JSON.stringify(result), { status: result.success ? 200 : 404, headers: { 'Content-Type': 'application/json' } }); } return new Response('Not Found', { status: 404 }); }, async backupData(env, sourceUserId) { const MAX_BACKUPS = 10; const sourceData = await env.CARD_ORDER.get(sourceUserId); if (sourceData) { try { const currentDate = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-'); const backupId = `backup_${currentDate}`; const backups = await env.CARD_ORDER.list({ prefix: 'backup_' }); const backupKeys = backups.keys.map(key => key.name).sort((a, b) => { const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime(); const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime(); return timeB - timeA; // 降序排序,最新的在前 }); await env.CARD_ORDER.put(backupId, sourceData); const allBackups = [...backupKeys, backupId].sort((a, b) => { const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime(); const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime(); return timeB - timeA; }); const backupsToDelete = allBackups.slice(MAX_BACKUPS); if (backupsToDelete.length > 0) { await Promise.all( backupsToDelete.map(key => env.CARD_ORDER.delete(key)) ); } return { success: true, backupId, remainingBackups: MAX_BACKUPS, deletedCount: backupsToDelete.length }; } catch (error) { return { success: false, error: 'Backup operation failed', details: error.message }; } } return { success: false, error: 'Source data not found' }; } };新版本部署2024.10.30 更新:1)、增加了前端验证,并取消了在浏览器中保存日志。现在超过15分钟需重新登录,及时退出登录能让你的隐私更安全;2)、进入设置之前会在自动备份书签,KV里将保存最近10次的备份;3)、小幅更改了配色。1、原workes,const HTML_CONTENT = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Card Tab</title> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>⭐</text></svg>"> <style> /* 全局样式 */ body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #e8f4ea; transition: background-color 0.3s ease; } /* 固定元素样式 */ .fixed-elements { position: fixed; top: 0; left: 0; right: 0; background-color: #e8f4ea; z-index: 1000; padding: 10px; transition: background-color 0.3s ease; height: 130px; } .fixed-elements h3 { position: absolute; top: 10px; left: 20px; margin: 0; } /* 中心内容样式 */ .center-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; max-width: 600px; text-align: center; } /* 管理员控制面板样式 */ .admin-controls { position: fixed; top: 10px; right: 10px; font-size: 60%; } /* 添加/删除控制按钮样式 */ .add-remove-controls { display: none; flex-direction: column; position: fixed; right: 20px; top: 50%; transform: translateY(-50%); align-items: center; gap: 10px; } .round-btn { background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; margin: 5px 0; } .add-btn { order: 1; } .remove-btn { order: 2; } .category-btn { order: 3; } .remove-category-btn { order: 4; } /* 主要内容区域样式 */ .content { margin-top: 140px; padding: 20px; } /* 搜索栏样式 */ .search-container { margin-top: 10px; } .search-bar { display: flex; justify-content: center; margin-bottom: 10px; } .search-bar input { width: 70%; padding: 5px; border: 1px solid #ccc; border-radius: 5px 0 0 5px; } .search-bar button { padding: 5px 10px; border: 1px solid #ccc; border-left: none; background-color: #f8f8; border-radius: 0 5px 5px 0; cursor: pointer; } /* 搜索引擎按钮样式 */ .search-engines { display: flex; justify-content: center; gap: 10px; } .search-engine { padding: 5px 10px; border: 1px solid #ccc; background-color: #f0f0f0; border-radius: 5px; cursor: pointer; } /* 主题切换按钮样式 */ #theme-toggle { position: fixed; bottom: 50px; right: 20px; background-color: #b8c9d9; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); transition: background-color 0.3s ease; } #theme-toggle:hover { background-color: #007bff; } /* 对话框样式 */ #dialog-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } #dialog-box { background-color: white; padding: 20px; border-radius: 5px; width: 300px; } #dialog-box input, #dialog-box select { width: 100%; margin-bottom: 10px; padding: 5px; } /* 分类和卡片样式 */ .section { margin-bottom: 20px; } .section-title-container { display: flex; align-items: center; margin-bottom: 10px; } .section-title { font-size: 18px; font-weight: bold; } .delete-category-btn { background-color: #ff9800; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer; } .card-container { display: flex; flex-wrap: wrap; gap: 10px; } .card { background-color: #b8c9d9; border-radius: 5px; padding: 10px; width: 150px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); cursor: pointer; transition: transform 0.2s; position: relative; user-select: none; } .card:hover { transform: translateY(-5px); } .card-top { display: flex; align-items: center; margin-bottom: 5px; } .card-icon { width: 16px; height: 16px; margin-right: 5px; } .card-title { font-size: 14px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-url { font-size: 12px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .private-tag { background-color: #ff9800; color: white; font-size: 10px; padding: 2px 5px; border-radius: 3px; position: absolute; top: 5px; right: 5px; } .delete-btn { position: absolute; top: -10px; right: -10px; background-color: red; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; text-align: center; font-size: 14px; line-height: 20px; cursor: pointer; display: none; } /* 版权信息样式 */ #copyright { position: fixed; bottom: 0; left: 0; width: 100%; height: 40px; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; font-size: 14px; z-index: 1000; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); } #copyright p { margin: 0; } #copyright a { color: #007bff; text-decoration: none; } #copyright a:hover { text-decoration: underline; } /* 响应式设计 */ @media (max-width: 480px) { .fixed-elements { position: relative; padding: 5px; } .content { margin-top: 10px; } .admin-controls input, .admin-controls button { height: 30%; } .card-container { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } .card { width: 80%; max-width: 100%; padding: 5px; } .card-title { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 130px; } .card-url { font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 130px; } .add-remove-controls { right: 2px; } .round-btn, #theme-toggle { right: 5px; display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; font-size: 24px; } } </style> </head> <body> <div class="fixed-elements"> <h3>我的导航</h3> <div class="center-content"> <!-- 一言模块 --> <p id="hitokoto"> <a href="#" id="hitokoto_text"></a> </p> <script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script> <!-- 搜索栏 --> <div class="search-container"> <div class="search-bar"> <input type="text" id="search-input" placeholder=""> <button id="search-button">🔍</button> </div> <div class="search-engines"> <button class="search-engine" data-engine="baidu">百度</button> <button class="search-engine" data-engine="bing">必应</button> <button class="search-engine" data-engine="google">谷歌</button> </div> </div> </div> <!-- 管理员控制面板 --> <div class="admin-controls"> <input type="password" id="admin-password" placeholder="输入密码"> <button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button> <button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button> </div> </div> <div class="content"> <!-- 添加/删除控制按钮 --> <div class="add-remove-controls"> <button class="round-btn add-btn" onclick="showAddDialog()">+</button> <button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button> <button class="round-btn category-btn" onclick="addCategory()">C+</button> <button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button> </div> <!-- 分类和卡片容器 --> <div id="sections-container"></div> <!-- 主题切换按钮 --> <button id="theme-toggle" onclick="toggleTheme()">◑</button> <!-- 添加链接对话框 --> <div id="dialog-overlay"> <div id="dialog-box"> <label for="name-input">名称</label> <input type="text" id="name-input"> <label for="url-input">地址</label> <input type="text" id="url-input"> <label for="category-select">选择分类</label> <select id="category-select"></select> <div class="private-link-container"> <label for="private-checkbox">私密链接</label> <input type="checkbox" id="private-checkbox"> </div> <button onclick="addLink()">确定</button> <button onclick="hideAddDialog()">取消</button> </div> </div> <!-- 版权信息 --> <div id="copyright" class="copyright"> <!--请不要删除--> <p>项目地址:<a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 如果喜欢,烦请点个star!</p> </div> </div> <script> // 搜索引擎配置 const searchEngines = { baidu: "https://www.baidu.com/s?wd=", bing: "https://www.bing.com/search?q=", google: "https://www.google.com/search?q=" }; let currentEngine = "baidu"; // 日志记录函数 function logAction(action, details) { const timestamp = new Date().toISOString(); const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details); console.log(logEntry); } // 设置当前搜索引擎 function setActiveEngine(engine) { currentEngine = engine; document.querySelectorAll('.search-engine').forEach(btn => { btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0'; }); logAction('设置搜索引擎', { engine }); } // 搜索引擎按钮点击事件 document.querySelectorAll('.search-engine').forEach(button => { button.addEventListener('click', () => setActiveEngine(button.dataset.engine)); }); // 搜索按钮点击事件 document.getElementById('search-button').addEventListener('click', () => { const query = document.getElementById('search-input').value; if (query) { logAction('执行搜索', { engine: currentEngine, query }); window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank'); } }); // 搜索输入框回车事件 document.getElementById('search-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('search-button').click(); } }); // 初始化搜索引擎 setActiveEngine(currentEngine); // 全局变量 let publicLinks = []; let privateLinks = []; let isAdmin = false; let isLoggedIn = false; let removeMode = false; let isRemoveCategoryMode = false; let isDarkTheme = false; let links = []; const categories = {}; // 添加新分类 async function addCategory() { if (!await validateToken()) { return; } const categoryName = prompt('请输入新分类名称:'); if (categoryName && !categories[categoryName]) { categories[categoryName] = []; updateCategorySelect(); renderCategories(); saveLinks(); logAction('添加分类', { categoryName, currentLinkCount: links.length }); } else if (categories[categoryName]) { alert('该分类已存在'); logAction('添加分类失败', { categoryName, reason: '分类已存在' }); } } // 删除分类 async function deleteCategory(category) { if (!await validateToken()) { return; } if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) { delete categories[category]; links = links.filter(link => link.category !== category); publicLinks = publicLinks.filter(link => link.category !== category); privateLinks = privateLinks.filter(link => link.category !== category); updateCategorySelect(); saveLinks(); renderCategories(); logAction('删除分类', { category }); } } // 渲染分类(不重新加载链接) function renderCategories() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); container.appendChild(section); const categoryLinks = links.filter(link => link.category === category); categoryLinks.forEach(link => { createCard(link, cardContainer); }); }); logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length }); } // 读取链接数据 async function loadLinks() { const headers = { 'Content-Type': 'application/json' }; // 如果已登录,从 localStorage 获取 token 并添加到请求头 if (isLoggedIn) { const token = localStorage.getItem('authToken'); if (token) { headers['Authorization'] = token; } } try { const response = await fetch('/api/getLinks?userId=testUser', { headers: headers }); if (!response.ok) { throw new Error("HTTP error! status: " + response.status); } const data = await response.json(); console.log('Received data:', data); if (data.categories) { Object.assign(categories, data.categories); } publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : []; privateLinks = data.links ? data.links.filter(link => link.isPrivate) : []; links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; loadSections(); updateCategorySelect(); updateUIState(); logAction('读取链接', { publicCount: publicLinks.length, privateCount: privateLinks.length, isLoggedIn: isLoggedIn, hasToken: !!localStorage.getItem('authToken') }); } catch (error) { console.error('Error loading links:', error); alert('加载链接时出错,请刷新页面重试'); } } // 更新UI状态 function updateUIState() { const passwordInput = document.getElementById('admin-password'); const adminBtn = document.getElementById('admin-mode-btn'); const secretGardenBtn = document.getElementById('secret-garden-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block'; secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录"; secretGardenBtn.style.display = 'inline-block'; if (isAdmin) { adminBtn.textContent = "离开设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'flex'; } else if (isLoggedIn) { adminBtn.textContent = "设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'none'; } else { adminBtn.style.display = 'none'; addRemoveControls.style.display = 'none'; } logAction('更新UI状态', { isAdmin, isLoggedIn }); } // 登录状态显示(加载所有链接) function showSecretGarden() { if (isLoggedIn) { links = [...publicLinks, ...privateLinks]; loadSections(); // 显示所有私密标签 document.querySelectorAll('.private-tag').forEach(tag => { tag.style.display = 'block'; }); logAction('显示私密花园'); } } // 加载分类和链接 function loadSections() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); let privateCount = 0; let linkCount = 0; links.forEach(link => { if (link.category === category) { if (link.isPrivate) privateCount++; linkCount++; createCard(link, cardContainer); } }); if (privateCount < linkCount || isLoggedIn) { container.appendChild(section); } }); logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length }); } // 创建卡片 function createCard(link, container) { const card = document.createElement('div'); card.className = 'card'; card.setAttribute('draggable', isAdmin); card.dataset.isPrivate = link.isPrivate; const cardTop = document.createElement('div'); cardTop.className = 'card-top'; // 定义默认的 SVG 图标 const defaultIconSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>' + '<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>' + '</svg>'; // 创建图标元素 const icon = document.createElement('img'); icon.className = 'card-icon'; // icon.src = 'https://api.iowen.cn/favicon/' + extractDomain(link.url) + '.png'; icon.src = 'https://www.faviconextractor.com/favicon/' + extractDomain(link.url); icon.alt = 'Website Icon'; // 如果图片加载失败,使用默认的 SVG 图标 icon.onerror = function() { const svgBlob = new Blob([defaultIconSVG], {type: 'image/svg+xml'}); const svgUrl = URL.createObjectURL(svgBlob); this.src = svgUrl; this.onload = () => URL.revokeObjectURL(svgUrl); }; function extractDomain(url) { let domain; try { domain = new URL(url).hostname; } catch (e) { domain = url; } return domain; } const title = document.createElement('div'); title.className = 'card-title'; title.textContent = link.name; cardTop.appendChild(icon); cardTop.appendChild(title); const url = document.createElement('div'); url.className = 'card-url'; url.textContent = link.url; card.appendChild(cardTop); card.appendChild(url); if (link.isPrivate) { const privateTag = document.createElement('div'); privateTag.className = 'private-tag'; privateTag.textContent = '私密'; card.appendChild(privateTag); } const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url; if (!isAdmin) { card.addEventListener('click', () => { window.open(correctedUrl, '_blank'); logAction('打开链接', { name: link.name, url: correctedUrl }); }); } const deleteBtn = document.createElement('button'); deleteBtn.textContent = '–'; deleteBtn.className = 'delete-btn'; deleteBtn.onclick = function (event) { event.stopPropagation(); removeCard(card); }; card.appendChild(deleteBtn); updateCardStyle(card); card.addEventListener('dragstart', dragStart); card.addEventListener('dragover', dragOver); card.addEventListener('dragend', dragEnd); card.addEventListener('drop', drop); card.addEventListener('touchstart', touchStart, { passive: false }); if (isAdmin && removeMode) { deleteBtn.style.display = 'block'; } if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) { container.appendChild(card); } // logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate }); } // 更新卡片样式 function updateCardStyle(card) { if (isDarkTheme) { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; } else { card.style.backgroundColor = '#b8c9d9'; card.style.color = '#333'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; } } // 更新分类选择下拉框 function updateCategorySelect() { const categorySelect = document.getElementById('category-select'); categorySelect.innerHTML = ''; Object.keys(categories).forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; categorySelect.appendChild(option); }); logAction('更新分类选择', { categoryCount: Object.keys(categories).length }); } // 保存链接数据 async function saveLinks() { if (isAdmin && !(await validateToken())) { return; } let allLinks = [...publicLinks, ...privateLinks]; try { await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('authToken') }, body: JSON.stringify({ userId: 'testUser', links: allLinks, categories: categories }), }); logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length }); } catch (error) { logAction('保存链接失败', { error: error.message }); alert('保存链接失败,请重试'); } } // 添加卡片弹窗 async function addLink() { if (!await validateToken()) { return; } const name = document.getElementById('name-input').value; const url = document.getElementById('url-input').value; const category = document.getElementById('category-select').value; const isPrivate = document.getElementById('private-checkbox').checked; if (name && url && category) { const newLink = { name, url, category, isPrivate }; if (isPrivate) { privateLinks.push(newLink); } else { publicLinks.push(newLink); } links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) { const container = document.getElementById(category); if (container) { createCard(newLink, container); } else { categories[category] = []; renderCategories(); } } saveLinks(); document.getElementById('name-input').value = ''; document.getElementById('url-input').value = ''; document.getElementById('private-checkbox').checked = false; hideAddDialog(); logAction('添加卡片', { name, url, category, isPrivate }); } } // 删除卡片 async function removeCard(card) { if (!await validateToken()) { return; } const name = card.querySelector('.card-title').textContent; const url = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; links = links.filter(link => link.url !== url); if (isPrivate) { privateLinks = privateLinks.filter(link => link.url !== url); } else { publicLinks = publicLinks.filter(link => link.url !== url); } for (const key in categories) { categories[key] = categories[key].filter(link => link.url !== url); } card.remove(); saveLinks(); logAction('删除卡片', { name, url, isPrivate }); } // 拖拽卡片 let draggedCard = null; let touchStartX, touchStartY; // 触屏端拖拽卡片 function touchStart(event) { if (!isAdmin) { return; } draggedCard = event.target.closest('.card'); if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; touchStartX = touch.clientX; touchStartY = touch.clientY; draggedCard.classList.add('dragging'); document.addEventListener('touchmove', touchMove, { passive: false }); document.addEventListener('touchend', touchEnd); } function touchMove(event) { if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; const currentX = touch.clientX; const currentY = touch.clientY; const deltaX = currentX - touchStartX; const deltaY = currentY - touchStartY; draggedCard.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)"; const target = findCardUnderTouch(currentX, currentY); if (target && target !== draggedCard) { const container = target.parentElement; const targetRect = target.getBoundingClientRect(); if (currentX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } function touchEnd(event) { if (!draggedCard) return; const card = draggedCard; const targetCategory = card.closest('.card-container').id; validateToken().then(isValid => { if (isValid && card) { updateCardCategory(card, targetCategory); saveCardOrder().catch(error => { console.error('Save failed:', error); }); } cleanupDragState(); }); } function findCardUnderTouch(x, y) { const cards = document.querySelectorAll('.card:not(.dragging)'); return Array.from(cards).find(card => { const rect = card.getBoundingClientRect(); return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; }); } // PC端拖拽卡片 function dragStart(event) { if (!isAdmin) { event.preventDefault(); return; } draggedCard = event.target.closest('.card'); if (!draggedCard) return; draggedCard.classList.add('dragging'); event.dataTransfer.effectAllowed = "move"; logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent }); } function dragOver(event) { if (!isAdmin) { event.preventDefault(); return; } event.preventDefault(); const target = event.target.closest('.card'); if (target && target !== draggedCard) { const container = target.parentElement; const mousePositionX = event.clientX; const targetRect = target.getBoundingClientRect(); if (mousePositionX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } // 清理拖拽状态函数 function cleanupDragState() { if (draggedCard) { draggedCard.classList.remove('dragging'); draggedCard.style.transform = ''; draggedCard = null; } document.removeEventListener('touchmove', touchMove); document.removeEventListener('touchend', touchEnd); touchStartX = null; touchStartY = null; } // PC端拖拽结束 function drop(event) { if (!isAdmin) { event.preventDefault(); return; } event.preventDefault(); const card = draggedCard; const targetCategory = event.target.closest('.card-container').id; validateToken().then(isValid => { if (isValid && card) { updateCardCategory(card, targetCategory); saveCardOrder().catch(error => { console.error('Save failed:', error); }); } cleanupDragState(); }); } function dragEnd(event) { if (draggedCard) { draggedCard.classList.remove('dragging'); logAction('拖拽卡片结束'); } } // 更新卡片分类 function updateCardCategory(card, newCategory) { const cardTitle = card.querySelector('.card-title').textContent; const cardUrl = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; const linkIndex = links.findIndex(link => link.url === cardUrl); if (linkIndex !== -1) { links[linkIndex].category = newCategory; } const linkArray = isPrivate ? privateLinks : publicLinks; const arrayIndex = linkArray.findIndex(link => link.url === cardUrl); if (arrayIndex !== -1) { linkArray[arrayIndex].category = newCategory; } card.dataset.category = newCategory; } // 在页面加载完成后添加触摸事件监听器 document.addEventListener('DOMContentLoaded', function() { const cardContainers = document.querySelectorAll('.card-container'); cardContainers.forEach(container => { container.addEventListener('touchstart', touchStart, { passive: false }); }); }); // 保存卡片顺序 async function saveCardOrder() { if (!await validateToken()) { return; } const containers = document.querySelectorAll('.card-container'); let newPublicLinks = []; let newPrivateLinks = []; let newCategories = {}; containers.forEach(container => { const category = container.id; newCategories[category] = []; [...container.children].forEach(card => { const url = card.querySelector('.card-url').textContent; const name = card.querySelector('.card-title').textContent; const isPrivate = card.dataset.isPrivate === 'true'; card.dataset.category = category; const link = { name, url, category, isPrivate }; if (isPrivate) { newPrivateLinks.push(link); } else { newPublicLinks.push(link); } newCategories[category].push(link); }); }); publicLinks.length = 0; publicLinks.push(...newPublicLinks); privateLinks.length = 0; privateLinks.push(...newPrivateLinks); Object.keys(categories).forEach(key => delete categories[key]); Object.assign(categories, newCategories); logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); try { const response = await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('authToken') }, body: JSON.stringify({ userId: 'testUser', links: [...newPublicLinks, ...newPrivateLinks], categories: newCategories }), }); const result = await response.json(); if (!result.success) { throw new Error('Failed to save order'); } logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); } catch (error) { logAction('保存顺序失败', { error: error.message }); alert('保存顺序失败,请重试'); } } // 设置状态重新加载卡片 function reloadCardsAsAdmin() { document.querySelectorAll('.card-container').forEach(container => { container.innerHTML = ''; }); loadLinks().then(() => { if (isDarkTheme) { applyDarkTheme(); } }); logAction('重新加载卡片(管理员模式)'); } // 密码输入框回车事件 document.getElementById('admin-password').addEventListener('keypress', (e) => { if (e.key === 'Enter') { toggleSecretGarden(); } }); // 切换设置状态 async function toggleAdminMode() { const adminBtn = document.getElementById('admin-mode-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); if (!isAdmin && isLoggedIn) { if (!await validateToken()) { return; } // 在进入设置模式之前进行备份 try { const response = await fetch('/api/backupData', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('authToken') }, body: JSON.stringify({ sourceUserId: 'testUser', backupUserId: 'backup' }), }); const result = await response.json(); if (result.success) { logAction('数据备份成功'); } else { throw new Error('备份失败'); } } catch (error) { logAction('数据备份失败', { error: error.message }); if (!confirm('备份失败,是否仍要继续进入设置模式?')) { return; } } isAdmin = true; adminBtn.textContent = "退出设置"; addRemoveControls.style.display = 'flex'; alert('准备设置分类和书签'); reloadCardsAsAdmin(); logAction('进入设置'); } else if (isAdmin) { isAdmin = false; removeMode = false; adminBtn.textContent = "设 置"; addRemoveControls.style.display = 'none'; alert('设置已保存'); reloadCardsAsAdmin(); logAction('离开设置'); } updateUIState(); } // 切换到登录状态 function toggleSecretGarden() { const passwordInput = document.getElementById('admin-password'); if (!isLoggedIn) { verifyPassword(passwordInput.value) .then(result => { if (result.valid) { isLoggedIn = true; localStorage.setItem('authToken', result.token); console.log('Token saved:', result.token); loadLinks(); alert('登录成功!'); logAction('登录成功'); } else { alert('密码错误'); logAction('登录失败', { reason: result.error || '密码错误' }); } updateUIState(); }) .catch(error => { console.error('Login error:', error); alert('登录过程出错,请重试'); }); } else { isLoggedIn = false; isAdmin = false; localStorage.removeItem('authToken'); links = publicLinks; loadSections(); alert('退出登录!'); updateUIState(); passwordInput.value = ''; logAction('退出登录'); } } // 应用暗色主题 function applyDarkTheme() { document.body.style.backgroundColor = '#121212'; document.body.style.color = '#ffffff'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; }); logAction('应用暗色主题'); } // 显示添加链接对话框 function showAddDialog() { document.getElementById('dialog-overlay').style.display = 'flex'; logAction('显示添加链接对话框'); } // 隐藏添加链接对话框 function hideAddDialog() { document.getElementById('dialog-overlay').style.display = 'none'; logAction('隐藏添加链接对话框'); } // 切换删除卡片模式 function toggleRemoveMode() { removeMode = !removeMode; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => { btn.style.display = removeMode ? 'block' : 'none'; }); logAction('切换删除卡片模式', { removeMode }); } //切换删除分类模式 function toggleRemoveCategory() { isRemoveCategoryMode = !isRemoveCategoryMode; const deleteButtons = document.querySelectorAll('.delete-category-btn'); deleteButtons.forEach(btn => { btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; }); logAction('切换删除分类模式', { isRemoveCategoryMode }); } // 切换主题 function toggleTheme() { isDarkTheme = !isDarkTheme; document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#e8f4ea'; document.body.style.color = isDarkTheme ? '#ffffff' : '#333'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#b8c9d9'; card.style.color = isDarkTheme ? '#ffffff' : '#333'; card.style.boxShadow = isDarkTheme ? '0 4px 8px rgba(0, 0, 0, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.1)'; }); const fixedElements = document.querySelectorAll('.fixed-elements'); fixedElements.forEach(element => { element.style.backgroundColor = isDarkTheme ? '#121212' : '#e8f4ea'; element.style.color = isDarkTheme ? '#ffffff' : '#333'; }); const dialogBox = document.getElementById('dialog-box'); dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff'; dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333'; const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select'); inputs.forEach(input => { input.style.backgroundColor = isDarkTheme ? '#444' : '#fff'; input.style.color = isDarkTheme ? '#fff' : '#333'; input.style.borderColor = isDarkTheme ? '#555' : '#ccc'; }); logAction('切换主题', { isDarkTheme }); } // 验证密码 async function verifyPassword(inputPassword) { const response = await fetch('/api/verifyPassword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: inputPassword }), }); const result = await response.json(); return result; } // 初始化加载 document.addEventListener('DOMContentLoaded', async () => { await validateToken(); loadLinks(); }); // 前端检查是否有 token async function validateToken() { const token = localStorage.getItem('authToken'); if (!token) { isLoggedIn = false; updateUIState(); return false; } try { const response = await fetch('/api/getLinks?userId=testUser', { headers: { 'Authorization': token } }); if (response.status === 401) { await resetToLoginState('token已过期,请重新登录'); return false; } isLoggedIn = true; updateUIState(); return true; } catch (error) { console.error('Token validation error:', error); return false; } } // 重置状态 async function resetToLoginState(message) { alert(message); cleanupDragState(); localStorage.removeItem('authToken'); isLoggedIn = false; isAdmin = false; removeMode = false; isRemoveCategoryMode = false; const passwordInput = document.getElementById('admin-password'); if (passwordInput) { passwordInput.value = ''; } updateUIState(); links = publicLinks; loadSections(); const addRemoveControls = document.querySelector('.add-remove-controls'); if (addRemoveControls) { addRemoveControls.style.display = 'none'; } document.querySelectorAll('.delete-btn').forEach(btn => { btn.style.display = 'none'; }); document.querySelectorAll('.delete-category-btn').forEach(btn => { btn.style.display = 'none'; }); const dialogOverlay = document.getElementById('dialog-overlay'); if (dialogOverlay) { dialogOverlay.style.display = 'none'; } } </script> </body> </html> `; // 服务端 token 验证 async function validateServerToken(authToken, env) { if (!authToken) { return { isValid: false, status: 401, response: { error: 'Unauthorized', message: '未登录或登录已过期' } }; } try { const [timestamp, hash] = authToken.split('.'); const tokenTimestamp = parseInt(timestamp); const now = Date.now(); const FIFTEEN_MINUTES = 15 * 60 * 1000; if (now - tokenTimestamp > FIFTEEN_MINUTES) { return { isValid: false, status: 401, response: { error: 'Token expired', tokenExpired: true, message: '登录已过期,请重新登录' } }; } const tokenData = timestamp + "_" + env.ADMIN_PASSWORD; const encoder = new TextEncoder(); const data = encoder.encode(tokenData); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const expectedHash = btoa(String.fromCharCode(...new Uint8Array(hashBuffer))); if (hash !== expectedHash) { return { isValid: false, status: 401, response: { error: 'Invalid token', tokenInvalid: true, message: '登录状态无效,请重新登录' } }; } return { isValid: true }; } catch (error) { return { isValid: false, status: 401, response: { error: 'Invalid token', tokenInvalid: true, message: '登录验证失败,请重新登录' } }; } } export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/') { return new Response(HTML_CONTENT, { headers: { 'Content-Type': 'text/html' } }); } if (url.pathname === '/api/getLinks') { const userId = url.searchParams.get('userId'); const authToken = request.headers.get('Authorization'); const data = await env.CARD_ORDER.get(userId); if (data) { const parsedData = JSON.parse(data); // 验证 token if (authToken) { const validation = await validateServerToken(authToken, env); if (!validation.isValid) { return new Response(JSON.stringify(validation.response), { status: validation.status, headers: { 'Content-Type': 'application/json' } }); } // Token 有效,返回完整数据 return new Response(JSON.stringify(parsedData), { status: 200, headers: { 'Content-Type': 'application/json' } }); } // 未提供 token,只返回公开数据 const filteredLinks = parsedData.links.filter(link => !link.isPrivate); const filteredCategories = {}; Object.keys(parsedData.categories).forEach(category => { filteredCategories[category] = parsedData.categories[category].filter(link => !link.isPrivate); }); return new Response(JSON.stringify({ links: filteredLinks, categories: filteredCategories }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ links: [], categories: {} }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/saveOrder' && request.method === 'POST') { const authToken = request.headers.get('Authorization'); const validation = await validateServerToken(authToken, env); if (!validation.isValid) { return new Response(JSON.stringify(validation.response), { status: validation.status, headers: { 'Content-Type': 'application/json' } }); } const { userId, links, categories } = await request.json(); await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories })); return new Response(JSON.stringify({ success: true, message: '保存成功' }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/verifyPassword' && request.method === 'POST') { try { const { password } = await request.json(); const isValid = password === env.ADMIN_PASSWORD; if (isValid) { // 生成包含时间戳的加密 token const timestamp = Date.now(); const tokenData = timestamp + "_" + env.ADMIN_PASSWORD; const encoder = new TextEncoder(); const data = encoder.encode(tokenData); const hashBuffer = await crypto.subtle.digest('SHA-256', data); // 使用指定格式:timestamp.hash const token = timestamp + "." + btoa(String.fromCharCode(...new Uint8Array(hashBuffer))); return new Response(JSON.stringify({ valid: true, token: token }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ valid: false, error: 'Invalid password' }), { status: 403, headers: { 'Content-Type': 'application/json' } }); } catch (error) { return new Response(JSON.stringify({ valid: false, error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } if (url.pathname === '/api/backupData' && request.method === 'POST') { const { sourceUserId } = await request.json(); const result = await this.backupData(env, sourceUserId); return new Response(JSON.stringify(result), { status: result.success ? 200 : 404, headers: { 'Content-Type': 'application/json' } }); } return new Response('Not Found', { status: 404 }); }, async backupData(env, sourceUserId) { const MAX_BACKUPS = 10; const sourceData = await env.CARD_ORDER.get(sourceUserId); if (sourceData) { try { const currentDate = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-'); const backupId = `backup_${currentDate}`; const backups = await env.CARD_ORDER.list({ prefix: 'backup_' }); const backupKeys = backups.keys.map(key => key.name).sort((a, b) => { const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime(); const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime(); return timeB - timeA; // 降序排序,最新的在前 }); await env.CARD_ORDER.put(backupId, sourceData); const allBackups = [...backupKeys, backupId].sort((a, b) => { const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime(); const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime(); return timeB - timeA; }); const backupsToDelete = allBackups.slice(MAX_BACKUPS); if (backupsToDelete.length > 0) { await Promise.all( backupsToDelete.map(key => env.CARD_ORDER.delete(key)) ); } return { success: true, backupId, remainingBackups: MAX_BACKUPS, deletedCount: backupsToDelete.length }; } catch (error) { return { success: false, error: 'Backup operation failed', details: error.message }; } } return { success: false, error: 'Source data not found' }; } };修改后workes, 效果const HTML_CONTENT = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ws01主页</title> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>💠</text></svg>"> <style> /* 全局样式 */ body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #d4d4d4; transition: background-color 0.3s ease; } ul { padding: 0; margin-block-start: 1em; margin-block-end: 1em; margin-inline-start: 0px; margin-inline-end: 0px; padding-inline-start: 40px; unicode-bidi: isolate; } li { display: list-item; text-align: -webkit-match-parent; unicode-bidi: isolate; margin: 0 8px; } .background { background-image: linear-gradient(#d4d4d4 1px, transparent 0), linear-gradient(90deg, #d4d4d4 1px, transparent 0); background-size: 32px 32px; background-color: #fffcf8; } .header { background-color: #fff; box-shadow: 0 0 5px rgba(0, 0, 0, .1); transition: background-color .5s; } .navbar { display: flex; width: 1280px; margin: auto; height: 40px; } .navbar .brand { display: flex; align-items: center; color: #555; } .brand .logo { max-width: 36px; } .brand .title { margin-left: 5px; font-family: helvetica neue, helvetica, arial, sans-serif; font-size: 24px; font-weight: 700; } .beta { color: #ccc; font-size: 11px; font-weight: 400; position: relative; top: -14px; } .category-list { list-style: none; display: flex; align-items: center; } .sites { width:1286px; background:; border:2px solid auto; margin:15px auto; padding:0px; text-align:center; } .sites1 { width:1286px; background:; border:2px solid auto; margin:15px auto; padding:0px; } .sites dl { height:36px; line-height:36px; display:block; margin:0; } .sites dl.alt { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dl.alt2 { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dt,.sites dd { text-align:center; display:block; float:left; } .sites dt { width:60px; } .sites dd { width:90px; margin:0; } .footer { width:580px; text-align:center; margin:5px auto; padding:2px; } /* 固定元素样式 */ .fixed-elements { position: fixed; top: 0; left: 0; right: 0; background-color: #d4d4d4; z-index: 1000; padding: 10px; transition: background-color 0.3s ease; height: 130px; } .fixed-elements h3 { position: absolute; top: 10px; left: 20px; margin: 0; } /* 中心内容样式 */ .center-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; max-width: 600px; text-align: center; } /* 管理员控制面板样式 */ .admin-controls { position: fixed; top: 10px; right: 10px; font-size: 60%; } /* 添加/删除控制按钮样式 */ .add-remove-controls { display: none; flex-direction: column; position: fixed; right: 20px; top: 50%; transform: translateY(-50%); align-items: center; gap: 10px; } .round-btn { background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; margin: 5px 0; } .add-btn { order: 1; } .remove-btn { order: 2; } .category-btn { order: 3; } .remove-category-btn { order: 4; } /* 主要内容区域样式 */ .content { margin-top: 140px; padding: 20px; } /* 搜索栏样式 */ .search-container { margin-top: 10px; } .search-bar { display: flex; justify-content: center; margin-bottom: 10px; } .search-bar input { width: 50%; padding: 5px; border: 1px solid #ccc; border-radius: 5px 0 0 5px; } .search-bar button { padding: 5px 20px; border: 1px solid #ccc; border-left: none; background-color: rgba(160, 201, 229, 0.6); border-radius: 0 5px 5px 0; cursor: pointer; } /* 搜索引擎按钮样式 */ .search-engines { display: flex; justify-content: center; gap: 10px; } .search-engine { padding: 5px 10px; border: 1px solid #ccc; background-color: #f0f0; border-radius: 5px; cursor: pointer; } /* 主题切换按钮样式 */ #theme-toggle { position: fixed; bottom: 50px; right: 20px; background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; } /* 显示日志按钮样式 */ #view-logs-btn { position: fixed; top: 100px; right: 10px; z-index: 1000; } /* 对话框样式 */ #dialog-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } #dialog-box { background-color: white; padding: 20px; border-radius: 5px; width: 300px; } #dialog-box input, #dialog-box select { width: 100%; margin-bottom: 10px; padding: 5px; } /* 分类和卡片样式 */ .section { margin-bottom: 20px; } .section-title-container { display: flex; align-items: center; margin-bottom: 10px; } .section-title { font-size: 18px; font-weight: bold; margin-left: 20px; /* 分类标签右移添加这一行 */ color: green; /* 分类标签字体颜色添加这一行 */ } .delete-category-btn { background-color: pink; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer; } .card-container { display: flex; flex-wrap: wrap; gap: 10px; } .card { background-color: rgba(160, 201, 229, 0.6); border-radius: 5px; padding: 10px; width: 132px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); cursor: pointer; transition: transform 0.2s; position: relative; } /* 中等屏幕 (平板等) */ @media (max-width: 1024px) { } /* 小屏幕 (手机) */ @media (max-width: 768px) { } .card:hover { transform: translateY(-5px); } .card-top { display: flex; align-items: center; margin-bottom: 5px; } .card-icon { width: 20px; height: 20px; margin-right: 5px; } .card-title { font-size: 18px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-url { font-size: 14px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: left; /* 添加左对齐 */ } /* 超小屏幕 (更小手机) */ @media (max-width: 480px) { .card { padding: 6px; /* 进一步减小卡片内边距6 */ width: 300px; /* 手机上显示4列,235时显示6列 */ } .card-icon { width: 42px; height: 42px; } .card-title { font-size: 36px; } .card-url { font-size: 22px; } } .private-tag { background-color: #ff9800; color: white; font-size: 10px; padding: 2px 5px; border-radius: 3px; position: absolute; top: 5px; right: 5px; } .delete-btn { position: absolute; top: -10px; right: -10px; background-color: red; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; text-align: center; font-size: 14px; line-height: 20px; cursor: pointer; display: none; } /* 版权信息样式 */ #copyright { position: fixed; bottom: 0; left: 0; width: 100%; height: 40px; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; font-size: 14px; z-index: 1000; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); } #copyright p { margin: 0; } #copyright a { color: #007bff; text-decoration: none; } #copyright a:hover { text-decoration: underline; } </style> </head> <body class="background"> </div> <div class="sites"> <!-- 00 --> <header class="header"> <nav class="navbar"> <a href="https://www.199881.xyz/" class="brand"> <img class="debug logo" src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299"> <span class="debug title">WS01の主页</span> <span class="debug beta">beta</span> <!-- 一言模块 --> <p id="hitokoto"> <a href="#" id="hitokoto_text"></a> </p> <script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script> </a> </header> <!-- 搜索栏 --> <div class="search-container"> <div class="search-bar"> <input type="text" id="search-input" placeholder=""> <button id="search-button">🔍</button> </div> <div class="search-engines"> <button class="search-engine" data-engine="baidu">百度</button> <button class="search-engine" data-engine="bing">必应</button> <button class="search-engine" data-engine="toutiao">头条</button> <button class="search-engine" data-engine="sm">神马</button> <button class="search-engine" data-engine="bilibili">哔哩</button> <button class="search-engine" data-engine="github">github</button> <button class="search-engine" data-engine="google">谷歌</button> <button class="search-engine" data-engine="yandex">yandex</button> </div> </div> <div class="head"> <!-- 添加/删除控制按钮 --> <div class="add-remove-controls"> <button class="round-btn add-btn" onclick="showAddDialog()">+</button> <button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button> <button class="round-btn category-btn" onclick="addCategory()">C+</button> <button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button> </div> <div class="sites1"> <!-- 00 --> <!-- 分类和卡片容器 --> <div id="sections-container"></div> <!-- 主题切换按钮 --> <button id="theme-toggle" onclick="toggleTheme()">◑</button> <!-- 显示日志按钮 用于调试--> <!--<button id="view-logs-btn" onclick="viewLogs()">显示日志</button>--> <!-- 添加链接对话框 --> <div id="dialog-overlay"> <div id="dialog-box"> <label for="name-input">名称</label> <input type="text" id="name-input"> <label for="url-input">地址</label> <input type="text" id="url-input"> <label for="category-select">选择分类</label> <select id="category-select"></select> <div class="private-link-container"> <label for="private-checkbox">私密链接</label> <input type="checkbox" id="private-checkbox"> </div> <button onclick="addLink()">确定</button> <button onclick="hideAddDialog()">取消</button> </div> </div> <br /> <!-- body 页脚 --> <div class="footer"> <!-- 管理员控制面板 --> <input type="password" id="admin-password" placeholder="输入密码"> <button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button> <button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button> <br /> <br /> <br /> <!-- 版权信息 --> <div id="copyright" class="copyright"> <!--请不要删除--> <p><a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 项目 <!-- 开站时间开始 --> <span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span> <script language="javascript"> var now = new Date(); function createtime(){ var grt= new Date("09/05/2024 00:00:00");/*---这里是网站的启用时间--*/ now.setTime(now.getTime()+250); days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days); hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum); hnum = Math.floor(hours); if(String(hnum).length ==1 ){hnum = "0" + hnum;} minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum); mnum = Math.floor(minutes); if(String(mnum).length ==1 ){mnum = "0" + mnum;} seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum); snum = Math.round(seconds); if(String(snum).length ==1 ){snum = "0" + snum;} document.getElementById("timeDate").innerHTML = "稳定运行"+dnum+"天"; document.getElementById("times").innerHTML = hnum + "小时" + mnum + "分" + snum + "秒"; } setInterval("createtime()",250); </script> <!-- 开站时间结束 --> <!-- <script defer src="https://four-root-occupation.glitch.me/denglong.js"></script> 国庆快乐--> </p> </div> </div> <script> // 搜索引擎配置 const searchEngines = { baidu: "https://www.baidu.com/s?wd=", bing: "https://www.bing.com/search?q=", sm: "https://m.sm.cn/s?q=", toutiao: "https://so.toutiao.com/search?dvpf=pc&source=trending_card&keyword=", bilibili: "https://search.bilibili.com/all?keyword=", github: "https://github.com/search?q=", google: "https://www.google.com/search?q=", yandex: "https://www.yandex.com/search/?text=" }; let currentEngine = "baidu"; // 日志记录函数 function logAction(action, details) { const timestamp = new Date().toISOString(); const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details); let logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); logs.push(logEntry); // 保留最新的1000条日志 if (logs.length > 1000) { logs = logs.slice(-1000); } localStorage.setItem('cardTabLogs', JSON.stringify(logs)); console.log(logEntry); // 同时在控制台输出日志 } // 查看日志的函数 function viewLogs() { const logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); console.log('Card Tab Logs:'); logs.forEach(log => console.log(log)); alert('日志已在控制台输出,请按F12打开开发者工具查看。'); } // 设置当前搜索引擎 function setActiveEngine(engine) { currentEngine = engine; document.querySelectorAll('.search-engine').forEach(btn => { btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0'; }); logAction('设置搜索引擎', { engine }); } // 搜索引擎按钮点击事件 document.querySelectorAll('.search-engine').forEach(button => { button.addEventListener('click', () => setActiveEngine(button.dataset.engine)); }); // 搜索按钮点击事件 document.getElementById('search-button').addEventListener('click', () => { const query = document.getElementById('search-input').value; if (query) { logAction('执行搜索', { engine: currentEngine, query }); window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank'); } }); // 搜索输入框回车事件 document.getElementById('search-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('search-button').click(); } }); // 初始化搜索引擎 setActiveEngine(currentEngine); // 全局变量 let publicLinks = []; let privateLinks = []; let isAdmin = false; let isLoggedIn = false; let removeMode = false; let isRemoveCategoryMode = false; let isDarkTheme = false; let links = []; const categories = {}; // 添加新分类 async function addCategory() { if (!await validateToken()) { return; } const categoryName = prompt('请输入新分类名称:'); if (categoryName && !categories[categoryName]) { categories[categoryName] = []; updateCategorySelect(); renderCategories(); saveLinks(); logAction('添加分类', { categoryName, currentLinkCount: links.length }); } else if (categories[categoryName]) { alert('该分类已存在'); logAction('添加分类失败', { categoryName, reason: '分类已存在' }); } } // 删除分类 async function deleteCategory(category) { if (!await validateToken()) { return; } if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) { delete categories[category]; links = links.filter(link => link.category !== category); publicLinks = publicLinks.filter(link => link.category !== category); privateLinks = privateLinks.filter(link => link.category !== category); updateCategorySelect(); saveLinks(); renderCategories(); logAction('删除分类', { category }); } } // 渲染分类(不重新加载链接) function renderCategories() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); container.appendChild(section); const categoryLinks = links.filter(link => link.category === category); categoryLinks.forEach(link => { createCard(link, cardContainer); }); }); logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length }); } // 读取链接数据 async function loadLinks() { const headers = { 'Content-Type': 'application/json' }; // 如果已登录,从 localStorage 获取 token 并添加到请求头 if (isLoggedIn) { const token = localStorage.getItem('authToken'); if (token) { headers['Authorization'] = token; } } try { const response = await fetch('/api/getLinks?userId=testUser', { headers: headers }); if (!response.ok) { throw new Error("HTTP error! status: " + response.status); } const data = await response.json(); console.log('Received data:', data); if (data.categories) { Object.assign(categories, data.categories); } publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : []; privateLinks = data.links ? data.links.filter(link => link.isPrivate) : []; links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; loadSections(); updateCategorySelect(); updateUIState(); logAction('读取链接', { publicCount: publicLinks.length, privateCount: privateLinks.length, isLoggedIn: isLoggedIn, hasToken: !!localStorage.getItem('authToken') }); } catch (error) { console.error('Error loading links:', error); alert('加载链接时出错,请刷新页面重试'); } } // 更新UI状态 function updateUIState() { const passwordInput = document.getElementById('admin-password'); const adminBtn = document.getElementById('admin-mode-btn'); const secretGardenBtn = document.getElementById('secret-garden-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block'; secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录"; secretGardenBtn.style.display = 'inline-block'; if (isAdmin) { adminBtn.textContent = "离开设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'flex'; } else if (isLoggedIn) { adminBtn.textContent = "设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'none'; } else { adminBtn.style.display = 'none'; addRemoveControls.style.display = 'none'; } logAction('更新UI状态', { isAdmin, isLoggedIn }); } // 登录状态显示(加载所有链接) function showSecretGarden() { if (isLoggedIn) { links = [...publicLinks, ...privateLinks]; loadSections(); // 显示所有私密标签 document.querySelectorAll('.private-tag').forEach(tag => { tag.style.display = 'block'; }); logAction('显示私密花园'); } } // 加载分类和链接 function loadSections() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); let privateCount = 0; let linkCount = 0; links.forEach(link => { if (link.category === category) { if (link.isPrivate) privateCount++; linkCount++; createCard(link, cardContainer); } }); if (privateCount < linkCount || isLoggedIn) { container.appendChild(section); } }); logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length }); } // 创建卡片 function createCard(link, container) { const card = document.createElement('div'); card.className = 'card'; card.setAttribute('draggable', isAdmin); card.dataset.isPrivate = link.isPrivate; const cardTop = document.createElement('div'); cardTop.className = 'card-top'; // 定义默认的 SVG 图标 const defaultIconSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>' + '<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>' + '</svg>'; // 创建图标元素 const icon = document.createElement('img'); icon.className = 'card-icon'; // icon.src = 'https://api.iowen.cn/favicon/' + extractDomain(link.url) + '.png'; icon.src = 'https://www.faviconextractor.com/favicon/' + extractDomain(link.url); icon.alt = 'Website Icon'; // 如果图片加载失败,使用默认的 SVG 图标 icon.onerror = function() { const svgBlob = new Blob([defaultIconSVG], {type: 'image/svg+xml'}); const svgUrl = URL.createObjectURL(svgBlob); this.src = svgUrl; this.onload = () => URL.revokeObjectURL(svgUrl); }; function extractDomain(url) { let domain; try { domain = new URL(url).hostname; } catch (e) { domain = url; } return domain; } const title = document.createElement('div'); title.className = 'card-title'; title.textContent = link.name; cardTop.appendChild(icon); cardTop.appendChild(title); const url = document.createElement('div'); url.className = 'card-url'; url.textContent = link.url; card.appendChild(cardTop); card.appendChild(url); if (link.isPrivate) { const privateTag = document.createElement('div'); privateTag.className = 'private-tag'; privateTag.textContent = '私密'; card.appendChild(privateTag); } const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url; if (!isAdmin) { card.addEventListener('click', () => { window.open(correctedUrl, '_blank'); logAction('打开链接', { name: link.name, url: correctedUrl }); }); } const deleteBtn = document.createElement('button'); deleteBtn.textContent = '–'; deleteBtn.className = 'delete-btn'; deleteBtn.onclick = function (event) { event.stopPropagation(); removeCard(card); }; card.appendChild(deleteBtn); updateCardStyle(card); card.addEventListener('dragstart', dragStart); card.addEventListener('dragover', dragOver); card.addEventListener('dragend', dragEnd); card.addEventListener('drop', drop); card.addEventListener('touchstart', touchStart, { passive: false }); if (isAdmin && removeMode) { deleteBtn.style.display = 'block'; } if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) { container.appendChild(card); } // logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate }); } // 更新卡片样式 function updateCardStyle(card) { if (isDarkTheme) { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; } else { card.style.backgroundColor = 'rgba(160, 201, 229, 0.6)'; card.style.color = '#333'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; } } // 更新分类选择下拉框 function updateCategorySelect() { const categorySelect = document.getElementById('category-select'); categorySelect.innerHTML = ''; Object.keys(categories).forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; categorySelect.appendChild(option); }); logAction('更新分类选择', { categoryCount: Object.keys(categories).length }); } // 保存链接数据 async function saveLinks() { if (isAdmin && !(await validateToken())) { return; } let allLinks = [...publicLinks, ...privateLinks]; try { await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('authToken') }, body: JSON.stringify({ userId: 'testUser', links: allLinks, categories: categories }), }); logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length }); } catch (error) { logAction('保存链接失败', { error: error.message }); alert('保存链接失败,请重试'); } } // 添加卡片弹窗 async function addLink() { if (!await validateToken()) { return; } const name = document.getElementById('name-input').value; const url = document.getElementById('url-input').value; const category = document.getElementById('category-select').value; const isPrivate = document.getElementById('private-checkbox').checked; if (name && url && category) { const newLink = { name, url, category, isPrivate }; if (isPrivate) { privateLinks.push(newLink); } else { publicLinks.push(newLink); } links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) { const container = document.getElementById(category); if (container) { createCard(newLink, container); } else { categories[category] = []; renderCategories(); } } saveLinks(); document.getElementById('name-input').value = ''; document.getElementById('url-input').value = ''; document.getElementById('private-checkbox').checked = false; hideAddDialog(); logAction('添加卡片', { name, url, category, isPrivate }); } } // 删除卡片 async function removeCard(card) { if (!await validateToken()) { return; } const name = card.querySelector('.card-title').textContent; const url = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; links = links.filter(link => link.url !== url); if (isPrivate) { privateLinks = privateLinks.filter(link => link.url !== url); } else { publicLinks = publicLinks.filter(link => link.url !== url); } for (const key in categories) { categories[key] = categories[key].filter(link => link.url !== url); } card.remove(); saveLinks(); logAction('删除卡片', { name, url, isPrivate }); } // 拖拽卡片 let draggedCard = null; let touchStartX, touchStartY; // 触屏端拖拽卡片 function touchStart(event) { if (!isAdmin) { return; } draggedCard = event.target.closest('.card'); if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; touchStartX = touch.clientX; touchStartY = touch.clientY; draggedCard.classList.add('dragging'); document.addEventListener('touchmove', touchMove, { passive: false }); document.addEventListener('touchend', touchEnd); } function touchMove(event) { if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; const currentX = touch.clientX; const currentY = touch.clientY; const deltaX = currentX - touchStartX; const deltaY = currentY - touchStartY; draggedCard.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)"; const target = findCardUnderTouch(currentX, currentY); if (target && target !== draggedCard) { const container = target.parentElement; const targetRect = target.getBoundingClientRect(); if (currentX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } function touchEnd(event) { if (!draggedCard) return; const card = draggedCard; const targetCategory = card.closest('.card-container').id; validateToken().then(isValid => { if (isValid && card) { updateCardCategory(card, targetCategory); saveCardOrder().catch(error => { console.error('Save failed:', error); }); } cleanupDragState(); }); } function findCardUnderTouch(x, y) { const cards = document.querySelectorAll('.card:not(.dragging)'); return Array.from(cards).find(card => { const rect = card.getBoundingClientRect(); return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; }); } // PC端拖拽卡片 function dragStart(event) { if (!isAdmin) { event.preventDefault(); return; } draggedCard = event.target.closest('.card'); if (!draggedCard) return; draggedCard.classList.add('dragging'); event.dataTransfer.effectAllowed = "move"; logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent }); } function dragOver(event) { if (!isAdmin) { event.preventDefault(); return; } event.preventDefault(); const target = event.target.closest('.card'); if (target && target !== draggedCard) { const container = target.parentElement; const mousePositionX = event.clientX; const targetRect = target.getBoundingClientRect(); if (mousePositionX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } // 清理拖拽状态函数 function cleanupDragState() { if (draggedCard) { draggedCard.classList.remove('dragging'); draggedCard.style.transform = ''; draggedCard = null; } document.removeEventListener('touchmove', touchMove); document.removeEventListener('touchend', touchEnd); touchStartX = null; touchStartY = null; } // PC端拖拽结束 function drop(event) { if (!isAdmin) { event.preventDefault(); return; } event.preventDefault(); const card = draggedCard; const targetCategory = event.target.closest('.card-container').id; validateToken().then(isValid => { if (isValid && card) { updateCardCategory(card, targetCategory); saveCardOrder().catch(error => { console.error('Save failed:', error); }); } cleanupDragState(); }); } function dragEnd(event) { if (draggedCard) { draggedCard.classList.remove('dragging'); logAction('拖拽卡片结束'); } } // 更新卡片分类 function updateCardCategory(card, newCategory) { const cardTitle = card.querySelector('.card-title').textContent; const cardUrl = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; const linkIndex = links.findIndex(link => link.url === cardUrl); if (linkIndex !== -1) { links[linkIndex].category = newCategory; } const linkArray = isPrivate ? privateLinks : publicLinks; const arrayIndex = linkArray.findIndex(link => link.url === cardUrl); if (arrayIndex !== -1) { linkArray[arrayIndex].category = newCategory; } card.dataset.category = newCategory; } // 在页面加载完成后添加触摸事件监听器 document.addEventListener('DOMContentLoaded', function() { const cardContainers = document.querySelectorAll('.card-container'); cardContainers.forEach(container => { container.addEventListener('touchstart', touchStart, { passive: false }); }); }); // 保存卡片顺序 async function saveCardOrder() { if (!await validateToken()) { return; } const containers = document.querySelectorAll('.card-container'); let newPublicLinks = []; let newPrivateLinks = []; let newCategories = {}; containers.forEach(container => { const category = container.id; newCategories[category] = []; [...container.children].forEach(card => { const url = card.querySelector('.card-url').textContent; const name = card.querySelector('.card-title').textContent; const isPrivate = card.dataset.isPrivate === 'true'; card.dataset.category = category; const link = { name, url, category, isPrivate }; if (isPrivate) { newPrivateLinks.push(link); } else { newPublicLinks.push(link); } newCategories[category].push(link); }); }); publicLinks.length = 0; publicLinks.push(...newPublicLinks); privateLinks.length = 0; privateLinks.push(...newPrivateLinks); Object.keys(categories).forEach(key => delete categories[key]); Object.assign(categories, newCategories); logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); try { const response = await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('authToken') }, body: JSON.stringify({ userId: 'testUser', links: [...newPublicLinks, ...newPrivateLinks], categories: newCategories }), }); const result = await response.json(); if (!result.success) { throw new Error('Failed to save order'); } logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); } catch (error) { logAction('保存顺序失败', { error: error.message }); alert('保存顺序失败,请重试'); } } // 设置状态重新加载卡片 function reloadCardsAsAdmin() { document.querySelectorAll('.card-container').forEach(container => { container.innerHTML = ''; }); loadLinks().then(() => { if (isDarkTheme) { applyDarkTheme(); } }); logAction('重新加载卡片(管理员模式)'); } // 密码输入框回车事件 document.getElementById('admin-password').addEventListener('keypress', (e) => { if (e.key === 'Enter') { toggleSecretGarden(); } }); // 切换设置状态 async function toggleAdminMode() { const adminBtn = document.getElementById('admin-mode-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); if (!isAdmin && isLoggedIn) { if (!await validateToken()) { return; } // 在进入设置模式之前进行备份 try { const response = await fetch('/api/backupData', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': localStorage.getItem('authToken') }, body: JSON.stringify({ sourceUserId: 'testUser', backupUserId: 'backup' }), }); const result = await response.json(); if (result.success) { logAction('数据备份成功'); } else { throw new Error('备份失败'); } } catch (error) { logAction('数据备份失败', { error: error.message }); if (!confirm('备份失败,是否仍要继续进入设置模式?')) { return; } } isAdmin = true; adminBtn.textContent = "退出设置"; addRemoveControls.style.display = 'flex'; alert('准备设置分类和书签'); reloadCardsAsAdmin(); logAction('进入设置'); } else if (isAdmin) { isAdmin = false; removeMode = false; adminBtn.textContent = "设 置"; addRemoveControls.style.display = 'none'; alert('设置已保存'); reloadCardsAsAdmin(); logAction('离开设置'); } updateUIState(); } // 切换到登录状态 function toggleSecretGarden() { const passwordInput = document.getElementById('admin-password'); if (!isLoggedIn) { verifyPassword(passwordInput.value) .then(result => { if (result.valid) { isLoggedIn = true; localStorage.setItem('authToken', result.token); console.log('Token saved:', result.token); loadLinks(); alert('登录成功!'); logAction('登录成功'); } else { alert('密码错误'); logAction('登录失败', { reason: result.error || '密码错误' }); } updateUIState(); }) .catch(error => { console.error('Login error:', error); alert('登录过程出错,请重试'); }); } else { isLoggedIn = false; isAdmin = false; localStorage.removeItem('authToken'); links = publicLinks; loadSections(); alert('退出登录!'); updateUIState(); passwordInput.value = ''; logAction('退出登录'); } } // 应用暗色主题 function applyDarkTheme() { document.body.style.backgroundColor = '#121212'; document.body.style.color = '#ffffff'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; }); logAction('应用暗色主题'); } // 显示添加链接对话框 function showAddDialog() { document.getElementById('dialog-overlay').style.display = 'flex'; logAction('显示添加链接对话框'); } // 隐藏添加链接对话框 function hideAddDialog() { document.getElementById('dialog-overlay').style.display = 'none'; logAction('隐藏添加链接对话框'); } // 切换删除卡片模式 function toggleRemoveMode() { removeMode = !removeMode; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => { btn.style.display = removeMode ? 'block' : 'none'; }); logAction('切换删除卡片模式', { removeMode }); } //切换删除分类模式 function toggleRemoveCategory() { isRemoveCategoryMode = !isRemoveCategoryMode; const deleteButtons = document.querySelectorAll('.delete-category-btn'); deleteButtons.forEach(btn => { btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; }); logAction('切换删除分类模式', { isRemoveCategoryMode }); } // 切换主题 function toggleTheme() { isDarkTheme = !isDarkTheme; document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff'; document.body.style.color = isDarkTheme ? '#ffffff' : '#333'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : 'rgba(160, 201, 229, 0.6)'; card.style.color = isDarkTheme ? '#ffffff' : '#333'; card.style.boxShadow = isDarkTheme ? '0 4px 8px rgba(0, 0, 0, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.1)'; }); const fixedElements = document.querySelectorAll('.fixed-elements'); fixedElements.forEach(element => { element.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff'; element.style.color = isDarkTheme ? '#ffffff' : '#333'; }); const dialogBox = document.getElementById('dialog-box'); dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff'; dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333'; const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select'); inputs.forEach(input => { input.style.backgroundColor = isDarkTheme ? '#444' : '#fff'; input.style.color = isDarkTheme ? '#fff' : '#333'; input.style.borderColor = isDarkTheme ? '#555' : '#ccc'; }); logAction('切换主题', { isDarkTheme }); } // 验证密码 async function verifyPassword(inputPassword) { const response = await fetch('/api/verifyPassword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: inputPassword }), }); const result = await response.json(); return result; } // 初始化加载 document.addEventListener('DOMContentLoaded', async () => { await validateToken(); loadLinks(); }); // 前端检查是否有 token async function validateToken() { const token = localStorage.getItem('authToken'); if (!token) { isLoggedIn = false; updateUIState(); return false; } try { const response = await fetch('/api/getLinks?userId=testUser', { headers: { 'Authorization': token } }); if (response.status === 401) { await resetToLoginState('token已过期,请重新登录'); return false; } isLoggedIn = true; updateUIState(); return true; } catch (error) { console.error('Token validation error:', error); return false; } } // 重置状态 async function resetToLoginState(message) { alert(message); cleanupDragState(); localStorage.removeItem('authToken'); isLoggedIn = false; isAdmin = false; removeMode = false; isRemoveCategoryMode = false; const passwordInput = document.getElementById('admin-password'); if (passwordInput) { passwordInput.value = ''; } updateUIState(); links = publicLinks; loadSections(); const addRemoveControls = document.querySelector('.add-remove-controls'); if (addRemoveControls) { addRemoveControls.style.display = 'none'; } document.querySelectorAll('.delete-btn').forEach(btn => { btn.style.display = 'none'; }); document.querySelectorAll('.delete-category-btn').forEach(btn => { btn.style.display = 'none'; }); const dialogOverlay = document.getElementById('dialog-overlay'); if (dialogOverlay) { dialogOverlay.style.display = 'none'; } } </script> </body> </html> `; // 服务端 token 验证 async function validateServerToken(authToken, env) { if (!authToken) { return { isValid: false, status: 401, response: { error: 'Unauthorized', message: '未登录或登录已过期' } }; } try { const [timestamp, hash] = authToken.split('.'); const tokenTimestamp = parseInt(timestamp); const now = Date.now(); const FIFTEEN_MINUTES = 15 * 60 * 1000; if (now - tokenTimestamp > FIFTEEN_MINUTES) { return { isValid: false, status: 401, response: { error: 'Token expired', tokenExpired: true, message: '登录已过期,请重新登录' } }; } const tokenData = timestamp + "_" + env.ADMIN_PASSWORD; const encoder = new TextEncoder(); const data = encoder.encode(tokenData); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const expectedHash = btoa(String.fromCharCode(...new Uint8Array(hashBuffer))); if (hash !== expectedHash) { return { isValid: false, status: 401, response: { error: 'Invalid token', tokenInvalid: true, message: '登录状态无效,请重新登录' } }; } return { isValid: true }; } catch (error) { return { isValid: false, status: 401, response: { error: 'Invalid token', tokenInvalid: true, message: '登录验证失败,请重新登录' } }; } } export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/') { return new Response(HTML_CONTENT, { headers: { 'Content-Type': 'text/html' } }); } if (url.pathname === '/api/getLinks') { const userId = url.searchParams.get('userId'); const authToken = request.headers.get('Authorization'); const data = await env.CARD_ORDER.get(userId); if (data) { const parsedData = JSON.parse(data); // 验证 token if (authToken) { const validation = await validateServerToken(authToken, env); if (!validation.isValid) { return new Response(JSON.stringify(validation.response), { status: validation.status, headers: { 'Content-Type': 'application/json' } }); } // Token 有效,返回完整数据 return new Response(JSON.stringify(parsedData), { status: 200, headers: { 'Content-Type': 'application/json' } }); } // 未提供 token,只返回公开数据 const filteredLinks = parsedData.links.filter(link => !link.isPrivate); const filteredCategories = {}; Object.keys(parsedData.categories).forEach(category => { filteredCategories[category] = parsedData.categories[category].filter(link => !link.isPrivate); }); return new Response(JSON.stringify({ links: filteredLinks, categories: filteredCategories }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ links: [], categories: {} }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/saveOrder' && request.method === 'POST') { const authToken = request.headers.get('Authorization'); const validation = await validateServerToken(authToken, env); if (!validation.isValid) { return new Response(JSON.stringify(validation.response), { status: validation.status, headers: { 'Content-Type': 'application/json' } }); } const { userId, links, categories } = await request.json(); await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories })); return new Response(JSON.stringify({ success: true, message: '保存成功' }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } if (url.pathname === '/api/verifyPassword' && request.method === 'POST') { try { const { password } = await request.json(); const isValid = password === env.ADMIN_PASSWORD; if (isValid) { // 生成包含时间戳的加密 token const timestamp = Date.now(); const tokenData = timestamp + "_" + env.ADMIN_PASSWORD; const encoder = new TextEncoder(); const data = encoder.encode(tokenData); const hashBuffer = await crypto.subtle.digest('SHA-256', data); // 使用指定格式:timestamp.hash const token = timestamp + "." + btoa(String.fromCharCode(...new Uint8Array(hashBuffer))); return new Response(JSON.stringify({ valid: true, token: token }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ valid: false, error: 'Invalid password' }), { status: 403, headers: { 'Content-Type': 'application/json' } }); } catch (error) { return new Response(JSON.stringify({ valid: false, error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } } if (url.pathname === '/api/backupData' && request.method === 'POST') { const { sourceUserId } = await request.json(); const result = await this.backupData(env, sourceUserId); return new Response(JSON.stringify(result), { status: result.success ? 200 : 404, headers: { 'Content-Type': 'application/json' } }); } return new Response('Not Found', { status: 404 }); }, async backupData(env, sourceUserId) { const MAX_BACKUPS = 10; const sourceData = await env.CARD_ORDER.get(sourceUserId); if (sourceData) { try { const currentDate = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-'); const backupId = `backup_${currentDate}`; const backups = await env.CARD_ORDER.list({ prefix: 'backup_' }); const backupKeys = backups.keys.map(key => key.name).sort((a, b) => { const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime(); const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime(); return timeB - timeA; // 降序排序,最新的在前 }); await env.CARD_ORDER.put(backupId, sourceData); const allBackups = [...backupKeys, backupId].sort((a, b) => { const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime(); const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime(); return timeB - timeA; }); const backupsToDelete = allBackups.slice(MAX_BACKUPS); if (backupsToDelete.length > 0) { await Promise.all( backupsToDelete.map(key => env.CARD_ORDER.delete(key)) ); } return { success: true, backupId, remainingBackups: MAX_BACKUPS, deletedCount: backupsToDelete.length }; } catch (error) { return { success: false, error: 'Backup operation failed', details: error.message }; } } return { success: false, error: 'Source data not found' }; } };2024.10.26 修复:原获取网站图标的接口https://favicon.zhusl.com/ico?url= 好像失效了,现更换接口https://www.faviconextractor.com/favicon/ 项目地址:【Github】 ;另一个备份接口为https://api.iowen.cn/favicon 项目地址:【Github】 感谢上述接口作者提供的服务const HTML_CONTENT = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Card Tab</title> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>💠</text></svg>"> <style> /* 全局样式 */ body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #d4d4d4; transition: background-color 0.3s ease; } ul { padding: 0; margin-block-start: 1em; margin-block-end: 1em; margin-inline-start: 0px; margin-inline-end: 0px; padding-inline-start: 40px; unicode-bidi: isolate; } li { display: list-item; text-align: -webkit-match-parent; unicode-bidi: isolate; margin: 0 8px; } .background { background-image: linear-gradient(#d4d4d4 1px, transparent 0), linear-gradient(90deg, #d4d4d4 1px, transparent 0); background-size: 32px 32px; background-color: #fffcf8; } .header { background-color: #fff; box-shadow: 0 0 5px rgba(0, 0, 0, .1); transition: background-color .5s; } .navbar { display: flex; width: 1280px; margin: auto; height: 40px; } .navbar .brand { display: flex; align-items: center; color: #555; } .brand .logo { max-width: 36px; } .brand .title { margin-left: 5px; font-family: helvetica neue, helvetica, arial, sans-serif; font-size: 24px; font-weight: 700; } .beta { color: #ccc; font-size: 11px; font-weight: 400; position: relative; top: -14px; } .category-list { list-style: none; display: flex; align-items: center; } .sites { width:1286px; background:; border:2px solid auto; margin:15px auto; padding:0px; text-align:center; } .sites1 { width:1286px; background:; border:2px solid auto; margin:15px auto; padding:0px; } .sites dl { height:36px; line-height:36px; display:block; margin:0; } .sites dl.alt { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dl.alt2 { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dt,.sites dd { text-align:center; display:block; float:left; } .sites dt { width:60px; } .sites dd { width:90px; margin:0; } .footer { width:580px; text-align:center; margin:5px auto; padding:2px; } /* 固定元素样式 */ .fixed-elements { position: fixed; top: 0; left: 0; right: 0; background-color: #d4d4d4; z-index: 1000; padding: 10px; transition: background-color 0.3s ease; height: 130px; } .fixed-elements h3 { position: absolute; top: 10px; left: 20px; margin: 0; } /* 中心内容样式 */ .center-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; max-width: 600px; text-align: center; } /* 管理员控制面板样式 */ .admin-controls { position: fixed; top: 10px; right: 10px; font-size: 60%; } /* 添加/删除控制按钮样式 */ .add-remove-controls { display: none; flex-direction: column; position: fixed; right: 20px; top: 50%; transform: translateY(-50%); align-items: center; gap: 10px; } .round-btn { background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; margin: 5px 0; } .add-btn { order: 1; } .remove-btn { order: 2; } .category-btn { order: 3; } .remove-category-btn { order: 4; } /* 主要内容区域样式 */ .content { margin-top: 140px; padding: 20px; } /* 搜索栏样式 */ .search-container { margin-top: 10px; } .search-bar { display: flex; justify-content: center; margin-bottom: 10px; } .search-bar input { width: 50%; padding: 5px; border: 1px solid #ccc; border-radius: 5px 0 0 5px; } .search-bar button { padding: 5px 10px; border: 1px solid #ccc; border-left: none; background-color: #a0c9e5; border-radius: 0 5px 5px 0; cursor: pointer; } /* 搜索引擎按钮样式 */ .search-engines { display: flex; justify-content: center; gap: 10px; } .search-engine { padding: 5px 10px; border: 1px solid #ccc; background-color: #f0f0; border-radius: 5px; cursor: pointer; } /* 主题切换按钮样式 */ #theme-toggle { position: fixed; bottom: 50px; right: 20px; background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; } /* 显示日志按钮样式 */ #view-logs-btn { position: fixed; top: 100px; right: 10px; z-index: 1000; } /* 对话框样式 */ #dialog-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } #dialog-box { background-color: white; padding: 20px; border-radius: 5px; width: 300px; } #dialog-box input, #dialog-box select { width: 100%; margin-bottom: 10px; padding: 5px; } /* 分类和卡片样式 */ .section { margin-bottom: 20px; } .section-title-container { display: flex; align-items: center; margin-bottom: 10px; } .section-title { font-size: 18px; font-weight: bold; } .delete-category-btn { background-color: pink; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer; } .card-container { display: flex; flex-wrap: wrap; gap: 10px; } .card { background-color: #a0c9e5; border-radius: 5px; padding: 10px; width: 132px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); cursor: pointer; transition: transform 0.2s; position: relative; user-select: none; } /* 中等屏幕 (平板等) */ @media (max-width: 1024px) { } /* 小屏幕 (手机) */ @media (max-width: 768px) { } /* 超小屏幕 (更小手机) */ @media (max-width: 480px) { .card { padding: 6px; /* 进一步减小卡片内边距6 */ width: 235px; } } .card:hover { transform: translateY(-5px); } .card-top { display: flex; align-items: center; margin-bottom: 5px; } .card-icon { width: 20px; height: 20px; margin-right: 5px; } .card-title { font-size: 18px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-url { font-size: 12px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .private-tag { background-color: #ff9800; color: white; font-size: 10px; padding: 2px 5px; border-radius: 3px; position: absolute; top: 5px; right: 5px; } .delete-btn { position: absolute; top: -10px; right: -10px; background-color: red; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; text-align: center; font-size: 14px; line-height: 20px; cursor: pointer; display: none; } /* 版权信息样式 */ #copyright { position: fixed; bottom: 0; left: 0; width: 100%; height: 40px; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; font-size: 14px; z-index: 1000; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); } #copyright p { margin: 0; } #copyright a { color: #007bff; text-decoration: none; } #copyright a:hover { text-decoration: underline; } /* 响应式设计 */ @media (max-width: 480px) { .fixed-elements { position: relative; padding: 5px; } .content { margin-top: 10px; } .admin-controls input, .admin-controls button { height: 30%; } .card-container { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } .card { width: 80%; max-width: 100%; padding: 5px; } .card-title { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 130px; } .card-url { font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 130px; } .add-remove-controls { right: 2px; } .round-btn, #theme-toggle { right: 5px; display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; font-size: 24px; } } </style> </head> <body class="background"> </div> <div class="sites"> <!-- 00 --> <header class="header"> <nav class="navbar"> <a href="https://www.199881.xyz/" class="brand"> <img class="debug logo" src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299"> <span class="debug title">WS01の主页</span> <span class="debug beta">beta</span> <!-- 一言模块 --> <p id="hitokoto"> <a href="#" id="hitokoto_text"></a> </p> <script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script> </a> </header> <!-- 搜索栏 --> <div class="search-container"> <div class="search-bar"> <input type="text" id="search-input" placeholder=""> <button id="search-button">🔍</button> </div> <div class="search-engines"> <button class="search-engine" data-engine="baidu">百度</button> <button class="search-engine" data-engine="bing">必应</button> <button class="search-engine" data-engine="toutiao">头条</button> <button class="search-engine" data-engine="sm">神马</button> <button class="search-engine" data-engine="bilibili">哔哩</button> <button class="search-engine" data-engine="github">github</button> <button class="search-engine" data-engine="google">谷歌</button> <button class="search-engine" data-engine="yandex">yandex</button> </div> </div> <div class="head"> <!-- 添加/删除控制按钮 --> <div class="add-remove-controls"> <button class="round-btn add-btn" onclick="showAddDialog()">+</button> <button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button> <button class="round-btn category-btn" onclick="addCategory()">C+</button> <button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button> </div> <div class="sites1"> <!-- 00 --> <!-- 分类和卡片容器 --> <div id="sections-container"></div> <!-- 主题切换按钮 --> <button id="theme-toggle" onclick="toggleTheme()">◑</button> <!-- 显示日志按钮 用于调试--> <!--<button id="view-logs-btn" onclick="viewLogs()">显示日志</button>--> <!-- 添加链接对话框 --> <div id="dialog-overlay"> <div id="dialog-box"> <label for="name-input">名称</label> <input type="text" id="name-input"> <label for="url-input">地址</label> <input type="text" id="url-input"> <label for="category-select">选择分类</label> <select id="category-select"></select> <div class="private-link-container"> <label for="private-checkbox">私密链接</label> <input type="checkbox" id="private-checkbox"> </div> <button onclick="addLink()">确定</button> <button onclick="hideAddDialog()">取消</button> </div> </div> <br /> <!-- body 页脚 --> <div class="footer"> <!-- 管理员控制面板 --> <input type="password" id="admin-password" placeholder="输入密码"> <button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button> <button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button> <br /> <br /> <br /> <!-- 版权信息 --> <div id="copyright" class="copyright"> <!--请不要删除--> <p><a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 项目 <!-- 开站时间开始 --> <span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span> <script language="javascript"> var now = new Date(); function createtime(){ var grt= new Date("09/05/2024 00:00:00");/*---这里是网站的启用时间--*/ now.setTime(now.getTime()+250); days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days); hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum); hnum = Math.floor(hours); if(String(hnum).length ==1 ){hnum = "0" + hnum;} minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum); mnum = Math.floor(minutes); if(String(mnum).length ==1 ){mnum = "0" + mnum;} seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum); snum = Math.round(seconds); if(String(snum).length ==1 ){snum = "0" + snum;} document.getElementById("timeDate").innerHTML = "稳定运行"+dnum+"天"; document.getElementById("times").innerHTML = hnum + "小时" + mnum + "分" + snum + "秒"; } setInterval("createtime()",250); </script> <!-- 开站时间结束 --> <!-- <script defer src="https://four-root-occupation.glitch.me/denglong.js"></script> 国庆快乐--> </p> </div> </div> <script> // 搜索引擎配置 const searchEngines = { baidu: "https://www.baidu.com/s?wd=", bing: "https://www.bing.com/search?q=", sm: "https://m.sm.cn/s?q=", toutiao: "https://so.toutiao.com/search?dvpf=pc&source=trending_card&keyword=", bilibili: "https://search.bilibili.com/all?keyword=", github: "https://github.com/search?q=", google: "https://www.google.com/search?q=", yandex: "https://www.yandex.com/search/?text=" }; let currentEngine = "baidu"; // 日志记录函数 function logAction(action, details) { const timestamp = new Date().toISOString(); const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details); let logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); logs.push(logEntry); // 保留最新的1000条日志 if (logs.length > 1000) { logs = logs.slice(-1000); } localStorage.setItem('cardTabLogs', JSON.stringify(logs)); console.log(logEntry); // 同时在控制台输出日志 } // 查看日志的函数 function viewLogs() { const logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); console.log('Card Tab Logs:'); logs.forEach(log => console.log(log)); alert('日志已在控制台输出,请按F12打开开发者工具查看。'); } // 设置当前搜索引擎 function setActiveEngine(engine) { currentEngine = engine; document.querySelectorAll('.search-engine').forEach(btn => { btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0'; }); logAction('设置搜索引擎', { engine }); } // 搜索引擎按钮点击事件 document.querySelectorAll('.search-engine').forEach(button => { button.addEventListener('click', () => setActiveEngine(button.dataset.engine)); }); // 搜索按钮点击事件 document.getElementById('search-button').addEventListener('click', () => { const query = document.getElementById('search-input').value; if (query) { logAction('执行搜索', { engine: currentEngine, query }); window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank'); } }); // 搜索输入框回车事件 document.getElementById('search-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('search-button').click(); } }); // 初始化搜索引擎 setActiveEngine(currentEngine); // 全局变量 let publicLinks = []; let privateLinks = []; let isAdmin = false; let isLoggedIn = false; let removeMode = false; let isRemoveCategoryMode = false; let isDarkTheme = false; let links = []; const categories = {}; // 添加新分类 function addCategory() { const categoryName = prompt('请输入新分类名称:'); if (categoryName && !categories[categoryName]) { categories[categoryName] = []; updateCategorySelect(); renderCategories(); saveLinks(); logAction('添加分类', { categoryName, currentLinkCount: links.length }); } else if (categories[categoryName]) { alert('该分类已存在'); logAction('添加分类失败', { categoryName, reason: '分类已存在' }); } } // 删除分类 function deleteCategory(category) { if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) { delete categories[category]; links = links.filter(link => link.category !== category); publicLinks = publicLinks.filter(link => link.category !== category); privateLinks = privateLinks.filter(link => link.category !== category); updateCategorySelect(); saveLinks(); renderCategories(); logAction('删除分类', { category }); } } // 渲染分类(不重新加载链接) function renderCategories() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); container.appendChild(section); const categoryLinks = links.filter(link => link.category === category); categoryLinks.forEach(link => { createCard(link, cardContainer); }); }); logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length }); } // 读取链接数据 async function loadLinks() { const response = await fetch('/api/getLinks?userId=testUser'); const data = await response.json(); if (data.categories) { Object.assign(categories, data.categories); } publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : []; privateLinks = data.links ? data.links.filter(link => link.isPrivate) : []; links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; loadSections(); updateCategorySelect(); updateUIState(); logAction('读取链接', { publicCount: publicLinks.length, privateCount: privateLinks.length }); } // 更新UI状态 function updateUIState() { const passwordInput = document.getElementById('admin-password'); const adminBtn = document.getElementById('admin-mode-btn'); const secretGardenBtn = document.getElementById('secret-garden-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block'; secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录"; secretGardenBtn.style.display = 'inline-block'; if (isAdmin) { adminBtn.textContent = "离开设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'flex'; } else if (isLoggedIn) { adminBtn.textContent = "设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'none'; } else { adminBtn.style.display = 'none'; addRemoveControls.style.display = 'none'; } logAction('更新UI状态', { isAdmin, isLoggedIn }); } // 登录状态显示(加载所有链接) function showSecretGarden() { if (isLoggedIn) { links = [...publicLinks, ...privateLinks]; loadSections(); // 显示所有私密标签 document.querySelectorAll('.private-tag').forEach(tag => { tag.style.display = 'block'; }); logAction('显示私密花园'); } } // 加载分类和链接 function loadSections() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); let privateCount = 0; let linkCount = 0; links.forEach(link => { if (link.category === category) { if (link.isPrivate) privateCount++; linkCount++; createCard(link, cardContainer); } }); if (privateCount < linkCount || isLoggedIn) { container.appendChild(section); } }); logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length }); } // 创建卡片 function createCard(link, container) { const card = document.createElement('div'); card.className = 'card'; card.setAttribute('draggable', isAdmin); card.dataset.isPrivate = link.isPrivate; const cardTop = document.createElement('div'); cardTop.className = 'card-top'; // const icon = document.createElement('img'); // icon.className = 'card-icon'; // icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url; // icon.alt = 'Website Icon'; // 定义默认的 SVG 图标 const defaultIconSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>' + '<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>' + '</svg>'; // 创建图标元素 const icon = document.createElement('img'); icon.className = 'card-icon'; // icon.src = 'https://api.iowen.cn/favicon/' + extractDomain(link.url) + '.png'; icon.src = 'https://www.faviconextractor.com/favicon/' + extractDomain(link.url); icon.alt = 'Website Icon'; // 错误处理:如果图片加载失败,使用默认的 SVG 图标 icon.onerror = function() { const svgBlob = new Blob([defaultIconSVG], {type: 'image/svg+xml'}); const svgUrl = URL.createObjectURL(svgBlob); this.src = svgUrl; // 清理:当图片不再需要时,撤销对象 URL this.onload = () => URL.revokeObjectURL(svgUrl); }; // 辅助函数:从 URL 中提取域名 function extractDomain(url) { let domain; try { domain = new URL(url).hostname; } catch (e) { // 如果 URL 无效,返回原始输入 domain = url; } return domain; } const title = document.createElement('div'); title.className = 'card-title'; title.textContent = link.name; cardTop.appendChild(icon); cardTop.appendChild(title); const url = document.createElement('div'); url.className = 'card-url'; url.textContent = link.url; card.appendChild(cardTop); card.appendChild(url); if (link.isPrivate) { const privateTag = document.createElement('div'); privateTag.className = 'private-tag'; privateTag.textContent = '私密'; card.appendChild(privateTag); } const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url; if (!isAdmin) { card.addEventListener('click', () => { window.open(correctedUrl, '_blank'); logAction('打开链接', { name: link.name, url: correctedUrl }); }); } const deleteBtn = document.createElement('button'); deleteBtn.textContent = '–'; deleteBtn.className = 'delete-btn'; deleteBtn.onclick = function (event) { event.stopPropagation(); removeCard(card); }; card.appendChild(deleteBtn); updateCardStyle(card); card.addEventListener('dragstart', dragStart); card.addEventListener('dragover', dragOver); card.addEventListener('dragend', dragEnd); card.addEventListener('drop', drop); card.addEventListener('touchstart', touchStart, { passive: false }); if (isAdmin && removeMode) { deleteBtn.style.display = 'block'; } if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) { container.appendChild(card); } // logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate }); } // 更新卡片样式 function updateCardStyle(card) { if (isDarkTheme) { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; } else { card.style.backgroundColor = '#a0c9e5'; card.style.color = '#333'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; } } // 更新分类选择下拉框 function updateCategorySelect() { const categorySelect = document.getElementById('category-select'); categorySelect.innerHTML = ''; Object.keys(categories).forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; categorySelect.appendChild(option); }); logAction('更新分类选择', { categoryCount: Object.keys(categories).length }); } // 保存链接数据 async function saveLinks() { let allLinks = [...publicLinks, ...privateLinks]; try { await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links: allLinks, categories: categories }), }); logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length }); } catch (error) { console.error('Error saving links:', error); logAction('保存链接失败', { error: error.message }); alert('保存链接失败,请重试'); } } // 添加卡片弹窗 function addLink() { const name = document.getElementById('name-input').value; const url = document.getElementById('url-input').value; const category = document.getElementById('category-select').value; const isPrivate = document.getElementById('private-checkbox').checked; if (name && url && category) { const newLink = { name, url, category, isPrivate }; if (isPrivate) { privateLinks.push(newLink); } else { publicLinks.push(newLink); } links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) { const container = document.getElementById(category); if (container) { createCard(newLink, container); } else { categories[category] = []; renderCategories(); } } saveLinks(); document.getElementById('name-input').value = ''; document.getElementById('url-input').value = ''; document.getElementById('private-checkbox').checked = false; hideAddDialog(); logAction('添加卡片', { name, url, category, isPrivate }); } } // 删除卡片 function removeCard(card) { const name = card.querySelector('.card-title').textContent; const url = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; links = links.filter(link => link.url !== url); if (isPrivate) { privateLinks = privateLinks.filter(link => link.url !== url); } else { publicLinks = publicLinks.filter(link => link.url !== url); } for (const key in categories) { categories[key] = categories[key].filter(link => link.url !== url); } card.remove(); saveLinks(); logAction('删除卡片', { name, url, isPrivate }); } // 拖拽卡片 let draggedCard = null; let touchStartX, touchStartY; // 触屏端拖拽卡片 function touchStart(event) { if (!isAdmin) return; draggedCard = event.target.closest('.card'); if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; touchStartX = touch.clientX; touchStartY = touch.clientY; draggedCard.classList.add('dragging'); document.addEventListener('touchmove', touchMove, { passive: false }); document.addEventListener('touchend', touchEnd); } function touchMove(event) { if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; const currentX = touch.clientX; const currentY = touch.clientY; const deltaX = currentX - touchStartX; const deltaY = currentY - touchStartY; draggedCard.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)"; const target = findCardUnderTouch(currentX, currentY); if (target && target !== draggedCard) { const container = target.parentElement; const targetRect = target.getBoundingClientRect(); if (currentX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } function touchEnd(event) { if (!draggedCard) return; document.removeEventListener('touchmove', touchMove); document.removeEventListener('touchend', touchEnd); draggedCard.classList.remove('dragging'); draggedCard.style.transform = ''; const targetCategory = draggedCard.closest('.card-container').id; updateCardCategory(draggedCard, targetCategory); saveCardOrder(); draggedCard = null; } function findCardUnderTouch(x, y) { const cards = document.querySelectorAll('.card:not(.dragging)'); return Array.from(cards).find(card => { const rect = card.getBoundingClientRect(); return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; }); } // PC端拖拽卡片 function dragStart(event) { if (!isAdmin) return; draggedCard = event.target.closest('.card'); if (!draggedCard) return; draggedCard.classList.add('dragging'); event.dataTransfer.effectAllowed = "move"; logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent }); } function dragOver(event) { if (!isAdmin || !draggedCard) return; event.preventDefault(); const target = event.target.closest('.card'); if (target && target !== draggedCard) { const container = target.parentElement; const mousePositionX = event.clientX; const targetRect = target.getBoundingClientRect(); if (mousePositionX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } function drop(event) { if (!isAdmin || !draggedCard) return; event.preventDefault(); draggedCard.classList.remove('dragging'); const targetCategory = event.target.closest('.card-container').id; updateCardCategory(draggedCard, targetCategory); logAction('放下卡片', { name: draggedCard.querySelector('.card-title').textContent, category: targetCategory }); draggedCard = null; saveCardOrder(); } function dragEnd(event) { if (draggedCard) { draggedCard.classList.remove('dragging'); logAction('拖拽卡片结束'); draggedCard = null; } } // 更新卡片分类 function updateCardCategory(card, newCategory) { const cardTitle = card.querySelector('.card-title').textContent; const cardUrl = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; const linkIndex = links.findIndex(link => link.url === cardUrl); if (linkIndex !== -1) { links[linkIndex].category = newCategory; } const linkArray = isPrivate ? privateLinks : publicLinks; const arrayIndex = linkArray.findIndex(link => link.url === cardUrl); if (arrayIndex !== -1) { linkArray[arrayIndex].category = newCategory; } card.dataset.category = newCategory; } // 在页面加载完成后添加触摸事件监听器 document.addEventListener('DOMContentLoaded', function() { const cardContainers = document.querySelectorAll('.card-container'); cardContainers.forEach(container => { container.addEventListener('touchstart', touchStart, { passive: false }); }); }); // 保存卡片顺序 async function saveCardOrder() { if (!isAdmin) return; const containers = document.querySelectorAll('.card-container'); let newPublicLinks = []; let newPrivateLinks = []; let newCategories = {}; containers.forEach(container => { const category = container.id; newCategories[category] = []; [...container.children].forEach(card => { const url = card.querySelector('.card-url').textContent; const name = card.querySelector('.card-title').textContent; const isPrivate = card.dataset.isPrivate === 'true'; card.dataset.category = category; const link = { name, url, category, isPrivate }; if (isPrivate) { newPrivateLinks.push(link); } else { newPublicLinks.push(link); } newCategories[category].push(link); }); }); publicLinks.length = 0; publicLinks.push(...newPublicLinks); privateLinks.length = 0; privateLinks.push(...newPrivateLinks); Object.keys(categories).forEach(key => delete categories[key]); Object.assign(categories, newCategories); logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); try { const response = await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links: [...newPublicLinks, ...newPrivateLinks], categories: newCategories }), }); const result = await response.json(); if (!result.success) { throw new Error('Failed to save order'); } logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); } catch (error) { console.error('Error saving order:', error); logAction('保存顺序失败', { error: error.message }); alert('保存顺序失败,请重试'); } } // 设置状态重新加载卡片 function reloadCardsAsAdmin() { document.querySelectorAll('.card-container').forEach(container => { container.innerHTML = ''; }); loadLinks().then(() => { if (isDarkTheme) { applyDarkTheme(); } }); logAction('重新加载卡片(管理员模式)'); } // 密码输入框回车事件 document.getElementById('admin-password').addEventListener('keypress', (e) => { if (e.key === 'Enter') { toggleSecretGarden(); } }); // 切换设置状态 function toggleAdminMode() { const adminBtn = document.getElementById('admin-mode-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); if (!isAdmin && isLoggedIn) { isAdmin = true; adminBtn.textContent = "退出设置"; addRemoveControls.style.display = 'flex'; alert('准备设置分类和书签'); reloadCardsAsAdmin(); logAction('进入设置'); } else if (isAdmin) { isAdmin = false; removeMode = false; adminBtn.textContent = "设 置"; addRemoveControls.style.display = 'none'; alert('设置已保存'); reloadCardsAsAdmin(); logAction('离开设置'); } updateUIState(); } // 切换到登录状态 function toggleSecretGarden() { const passwordInput = document.getElementById('admin-password'); if (!isLoggedIn) { verifyPassword(passwordInput.value).then(isValid => { if (isValid) { isLoggedIn = true; links = [...publicLinks, ...privateLinks]; loadSections(); alert('登录成功!'); logAction('登录成功'); } else { alert('密码错误'); logAction('登录失败', { reason: '密码错误' }); } updateUIState(); }); } else { isLoggedIn = false; isAdmin = false; links = publicLinks; loadSections(); alert('退出登录!'); updateUIState(); passwordInput.value = ''; logAction('退出登录'); } } // 应用暗色主题 function applyDarkTheme() { document.body.style.backgroundColor = '#121212'; document.body.style.color = '#ffffff'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; }); logAction('应用暗色主题'); } // 显示添加链接对话框 function showAddDialog() { document.getElementById('dialog-overlay').style.display = 'flex'; logAction('显示添加链接对话框'); } // 隐藏添加链接对话框 function hideAddDialog() { document.getElementById('dialog-overlay').style.display = 'none'; logAction('隐藏添加链接对话框'); } // 切换删除卡片模式 function toggleRemoveMode() { removeMode = !removeMode; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => { btn.style.display = removeMode ? 'block' : 'none'; }); logAction('切换删除卡片模式', { removeMode }); } //切换删除分类模式 function toggleRemoveCategory() { isRemoveCategoryMode = !isRemoveCategoryMode; const deleteButtons = document.querySelectorAll('.delete-category-btn'); deleteButtons.forEach(btn => { btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; }); logAction('切换删除分类模式', { isRemoveCategoryMode }); } // 切换主题 function toggleTheme() { isDarkTheme = !isDarkTheme; document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff'; document.body.style.color = isDarkTheme ? '#ffffff' : '#333'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#a0c9e5'; card.style.color = isDarkTheme ? '#ffffff' : '#333'; card.style.boxShadow = isDarkTheme ? '0 4px 8px rgba(0, 0, 0, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.1)'; }); const fixedElements = document.querySelectorAll('.fixed-elements'); fixedElements.forEach(element => { element.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff'; element.style.color = isDarkTheme ? '#ffffff' : '#333'; }); const dialogBox = document.getElementById('dialog-box'); dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff'; dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333'; const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select'); inputs.forEach(input => { input.style.backgroundColor = isDarkTheme ? '#444' : '#fff'; input.style.color = isDarkTheme ? '#fff' : '#333'; input.style.borderColor = isDarkTheme ? '#555' : '#ccc'; }); logAction('切换主题', { isDarkTheme }); } // 验证密码 async function verifyPassword(inputPassword) { const response = await fetch('/api/verifyPassword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: inputPassword }), }); const result = await response.json(); return result.valid; } // 初始化加载链接 loadLinks(); </script> </body> </html> `; export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/') { return new Response(HTML_CONTENT, { headers: { 'Content-Type': 'text/html' } }); } if (url.pathname === '/api/getLinks') { const userId = url.searchParams.get('userId'); const links = await env.CARD_ORDER.get(userId); return new Response(links || JSON.stringify([]), { status: 200 }); } if (url.pathname === '/api/saveOrder' && request.method === 'POST') { const { userId, links, categories } = await request.json(); await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories })); //保存链接和分类 return new Response(JSON.stringify({ success: true }), { status: 200 }); } if (url.pathname === '/api/verifyPassword' && request.method === 'POST') { const { password } = await request.json(); const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码 return new Response(JSON.stringify({ valid: isValid }), { status: isValid ? 200 : 403, headers: { 'Content-Type': 'application/json' }, }); } return new Response('Not Found', { status: 404 }); } };2024.09.09 更新:1)、增加私密书签,登录后可见2)、增加网站分类管理,现在你无需编辑代码,通过页面即可进行网站分类的添加和删除操作3)、增加搜索框和一言接口1、原workes, 效果const HTML_CONTENT = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Card Tab</title> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>⭐</text></svg>"> <style> /* 全局样式 */ body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #d8eac4; transition: background-color 0.3s ease; } /* 固定元素样式 */ .fixed-elements { position: fixed; top: 0; left: 0; right: 0; background-color: #d8eac4; z-index: 1000; padding: 10px; transition: background-color 0.3s ease; height: 130px; } .fixed-elements h3 { position: absolute; top: 10px; left: 20px; margin: 0; } /* 中心内容样式 */ .center-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; max-width: 600px; text-align: center; } /* 管理员控制面板样式 */ .admin-controls { position: fixed; top: 10px; right: 10px; font-size: 60%; } /* 添加/删除控制按钮样式 */ .add-remove-controls { display: none; flex-direction: column; position: fixed; right: 20px; top: 50%; transform: translateY(-50%); align-items: center; gap: 10px; } .round-btn { background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; margin: 5px 0; } .add-btn { order: 1; } .remove-btn { order: 2; } .category-btn { order: 3; } .remove-category-btn { order: 4; } /* 主要内容区域样式 */ .content { margin-top: 140px; padding: 20px; } /* 搜索栏样式 */ .search-container { margin-top: 10px; } .search-bar { display: flex; justify-content: center; margin-bottom: 10px; } .search-bar input { width: 70%; padding: 5px; border: 1px solid #ccc; border-radius: 5px 0 0 5px; } .search-bar button { padding: 5px 10px; border: 1px solid #ccc; border-left: none; background-color: #f8f8; border-radius: 0 5px 5px 0; cursor: pointer; } /* 搜索引擎按钮样式 */ .search-engines { display: flex; justify-content: center; gap: 10px; } .search-engine { padding: 5px 10px; border: 1px solid #ccc; background-color: #f0f0; border-radius: 5px; cursor: pointer; } /* 主题切换按钮样式 */ #theme-toggle { position: fixed; bottom: 50px; right: 20px; background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; } /* 显示日志按钮样式 */ #view-logs-btn { position: fixed; top: 100px; right: 10px; z-index: 1000; } /* 对话框样式 */ #dialog-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } #dialog-box { background-color: white; padding: 20px; border-radius: 5px; width: 300px; } #dialog-box input, #dialog-box select { width: 100%; margin-bottom: 10px; padding: 5px; } /* 分类和卡片样式 */ .section { margin-bottom: 20px; } .section-title-container { display: flex; align-items: center; margin-bottom: 10px; } .section-title { font-size: 18px; font-weight: bold; } .delete-category-btn { background-color: pink; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer; } .card-container { display: flex; flex-wrap: wrap; gap: 10px; } .card { background-color: #a0c9e5; border-radius: 5px; padding: 10px; width: 150px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); cursor: pointer; transition: transform 0.2s; position: relative; user-select: none; } .card:hover { transform: translateY(-5px); } .card-top { display: flex; align-items: center; margin-bottom: 5px; } .card-icon { width: 16px; height: 16px; margin-right: 5px; } .card-title { font-size: 14px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-url { font-size: 12px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .private-tag { background-color: #ff9800; color: white; font-size: 10px; padding: 2px 5px; border-radius: 3px; position: absolute; top: 5px; right: 5px; } .delete-btn { position: absolute; top: -10px; right: -10px; background-color: red; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; text-align: center; font-size: 14px; line-height: 20px; cursor: pointer; display: none; } /* 版权信息样式 */ #copyright { position: fixed; bottom: 0; left: 0; width: 100%; height: 40px; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; font-size: 14px; z-index: 1000; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); } #copyright p { margin: 0; } #copyright a { color: #007bff; text-decoration: none; } #copyright a:hover { text-decoration: underline; } /* 响应式设计 */ @media (max-width: 480px) { .fixed-elements { position: relative; padding: 5px; } .content { margin-top: 10px; } .admin-controls input, .admin-controls button { height: 30%; } .card-container { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } .card { width: 80%; max-width: 100%; padding: 5px; } .card-title { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 130px; } .card-url { font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 130px; } .add-remove-controls { right: 2px; } .round-btn, #theme-toggle { right: 5px; display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; font-size: 24px; } } </style> </head> <body> <div class="fixed-elements"> <h3>我的导航</h3> <div class="center-content"> <!-- 一言模块 --> <p id="hitokoto"> <a href="#" id="hitokoto_text"></a> </p> <script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script> <!-- 搜索栏 --> <div class="search-container"> <div class="search-bar"> <input type="text" id="search-input" placeholder=""> <button id="search-button">🔍</button> </div> <div class="search-engines"> <button class="search-engine" data-engine="baidu">百度</button> <button class="search-engine" data-engine="bing">必应</button> <button class="search-engine" data-engine="google">谷歌</button> </div> </div> </div> <!-- 管理员控制面板 --> <div class="admin-controls"> <input type="password" id="admin-password" placeholder="输入密码"> <button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button> <button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button> </div> </div> <div class="content"> <!-- 添加/删除控制按钮 --> <div class="add-remove-controls"> <button class="round-btn add-btn" onclick="showAddDialog()">+</button> <button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button> <button class="round-btn category-btn" onclick="addCategory()">C+</button> <button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button> </div> <!-- 分类和卡片容器 --> <div id="sections-container"></div> <!-- 主题切换按钮 --> <button id="theme-toggle" onclick="toggleTheme()">◑</button> <!-- 显示日志按钮 用于调试--> <!--<button id="view-logs-btn" onclick="viewLogs()">显示日志</button>--> <!-- 添加链接对话框 --> <div id="dialog-overlay"> <div id="dialog-box"> <label for="name-input">名称</label> <input type="text" id="name-input"> <label for="url-input">地址</label> <input type="text" id="url-input"> <label for="category-select">选择分类</label> <select id="category-select"></select> <div class="private-link-container"> <label for="private-checkbox">私密链接</label> <input type="checkbox" id="private-checkbox"> </div> <button onclick="addLink()">确定</button> <button onclick="hideAddDialog()">取消</button> </div> </div> <!-- 版权信息 --> <div id="copyright" class="copyright"> <!--请不要删除--> <p>项目地址:<a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 如果喜欢,烦请点个star!</p> </div> </div> <script> // 搜索引擎配置 const searchEngines = { baidu: "https://www.baidu.com/s?wd=", bing: "https://www.bing.com/search?q=", google: "https://www.google.com/search?q=" }; let currentEngine = "baidu"; // 日志记录函数 function logAction(action, details) { const timestamp = new Date().toISOString(); const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details); let logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); logs.push(logEntry); // 保留最新的1000条日志 if (logs.length > 1000) { logs = logs.slice(-1000); } localStorage.setItem('cardTabLogs', JSON.stringify(logs)); console.log(logEntry); // 同时在控制台输出日志 } // 查看日志的函数 function viewLogs() { const logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); console.log('Card Tab Logs:'); logs.forEach(log => console.log(log)); alert('日志已在控制台输出,请按F12打开开发者工具查看。'); } // 设置当前搜索引擎 function setActiveEngine(engine) { currentEngine = engine; document.querySelectorAll('.search-engine').forEach(btn => { btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0'; }); logAction('设置搜索引擎', { engine }); } // 搜索引擎按钮点击事件 document.querySelectorAll('.search-engine').forEach(button => { button.addEventListener('click', () => setActiveEngine(button.dataset.engine)); }); // 搜索按钮点击事件 document.getElementById('search-button').addEventListener('click', () => { const query = document.getElementById('search-input').value; if (query) { logAction('执行搜索', { engine: currentEngine, query }); window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank'); } }); // 搜索输入框回车事件 document.getElementById('search-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('search-button').click(); } }); // 初始化搜索引擎 setActiveEngine(currentEngine); // 全局变量 let publicLinks = []; let privateLinks = []; let isAdmin = false; let isLoggedIn = false; let removeMode = false; let isRemoveCategoryMode = false; let isDarkTheme = false; let links = []; const categories = {}; // 添加新分类 function addCategory() { const categoryName = prompt('请输入新分类名称:'); if (categoryName && !categories[categoryName]) { categories[categoryName] = []; updateCategorySelect(); renderCategories(); saveLinks(); logAction('添加分类', { categoryName, currentLinkCount: links.length }); } else if (categories[categoryName]) { alert('该分类已存在'); logAction('添加分类失败', { categoryName, reason: '分类已存在' }); } } // 删除分类 function deleteCategory(category) { if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) { delete categories[category]; links = links.filter(link => link.category !== category); publicLinks = publicLinks.filter(link => link.category !== category); privateLinks = privateLinks.filter(link => link.category !== category); updateCategorySelect(); saveLinks(); renderCategories(); logAction('删除分类', { category }); } } // 渲染分类(不重新加载链接) function renderCategories() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); container.appendChild(section); const categoryLinks = links.filter(link => link.category === category); categoryLinks.forEach(link => { createCard(link, cardContainer); }); }); logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length }); } // 读取链接数据 async function loadLinks() { const response = await fetch('/api/getLinks?userId=testUser'); const data = await response.json(); if (data.categories) { Object.assign(categories, data.categories); } publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : []; privateLinks = data.links ? data.links.filter(link => link.isPrivate) : []; links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; loadSections(); updateCategorySelect(); updateUIState(); logAction('读取链接', { publicCount: publicLinks.length, privateCount: privateLinks.length }); } // 更新UI状态 function updateUIState() { const passwordInput = document.getElementById('admin-password'); const adminBtn = document.getElementById('admin-mode-btn'); const secretGardenBtn = document.getElementById('secret-garden-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block'; secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录"; secretGardenBtn.style.display = 'inline-block'; if (isAdmin) { adminBtn.textContent = "离开设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'flex'; } else if (isLoggedIn) { adminBtn.textContent = "设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'none'; } else { adminBtn.style.display = 'none'; addRemoveControls.style.display = 'none'; } logAction('更新UI状态', { isAdmin, isLoggedIn }); } // 登录状态显示(加载所有链接) function showSecretGarden() { if (isLoggedIn) { links = [...publicLinks, ...privateLinks]; loadSections(); // 显示所有私密标签 document.querySelectorAll('.private-tag').forEach(tag => { tag.style.display = 'block'; }); logAction('显示私密花园'); } } // 加载分类和链接 function loadSections() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); let privateCount = 0; let linkCount = 0; links.forEach(link => { if (link.category === category) { if (link.isPrivate) privateCount++; linkCount++; createCard(link, cardContainer); } }); if (privateCount < linkCount || isLoggedIn) { container.appendChild(section); } }); logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length }); } // 创建卡片 function createCard(link, container) { const card = document.createElement('div'); card.className = 'card'; card.setAttribute('draggable', isAdmin); card.dataset.isPrivate = link.isPrivate; const cardTop = document.createElement('div'); cardTop.className = 'card-top'; const icon = document.createElement('img'); icon.className = 'card-icon'; icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url; icon.alt = 'Website Icon'; const title = document.createElement('div'); title.className = 'card-title'; title.textContent = link.name; cardTop.appendChild(icon); cardTop.appendChild(title); const url = document.createElement('div'); url.className = 'card-url'; url.textContent = link.url; card.appendChild(cardTop); card.appendChild(url); if (link.isPrivate) { const privateTag = document.createElement('div'); privateTag.className = 'private-tag'; privateTag.textContent = '私密'; card.appendChild(privateTag); } const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url; if (!isAdmin) { card.addEventListener('click', () => { window.open(correctedUrl, '_blank'); logAction('打开链接', { name: link.name, url: correctedUrl }); }); } const deleteBtn = document.createElement('button'); deleteBtn.textContent = '–'; deleteBtn.className = 'delete-btn'; deleteBtn.onclick = function (event) { event.stopPropagation(); removeCard(card); }; card.appendChild(deleteBtn); updateCardStyle(card); card.addEventListener('dragstart', dragStart); card.addEventListener('dragover', dragOver); card.addEventListener('dragend', dragEnd); card.addEventListener('drop', drop); card.addEventListener('touchstart', touchStart, { passive: false }); if (isAdmin && removeMode) { deleteBtn.style.display = 'block'; } if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) { container.appendChild(card); } // logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate }); } // 更新卡片样式 function updateCardStyle(card) { if (isDarkTheme) { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; } else { card.style.backgroundColor = '#a0c9e5'; card.style.color = '#333'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; } } // 更新分类选择下拉框 function updateCategorySelect() { const categorySelect = document.getElementById('category-select'); categorySelect.innerHTML = ''; Object.keys(categories).forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; categorySelect.appendChild(option); }); logAction('更新分类选择', { categoryCount: Object.keys(categories).length }); } // 保存链接数据 async function saveLinks() { let allLinks = [...publicLinks, ...privateLinks]; try { await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links: allLinks, categories: categories }), }); logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length }); } catch (error) { console.error('Error saving links:', error); logAction('保存链接失败', { error: error.message }); alert('保存链接失败,请重试'); } } // 添加卡片弹窗 function addLink() { const name = document.getElementById('name-input').value; const url = document.getElementById('url-input').value; const category = document.getElementById('category-select').value; const isPrivate = document.getElementById('private-checkbox').checked; if (name && url && category) { const newLink = { name, url, category, isPrivate }; if (isPrivate) { privateLinks.push(newLink); } else { publicLinks.push(newLink); } links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) { const container = document.getElementById(category); if (container) { createCard(newLink, container); } else { categories[category] = []; renderCategories(); } } saveLinks(); document.getElementById('name-input').value = ''; document.getElementById('url-input').value = ''; document.getElementById('private-checkbox').checked = false; hideAddDialog(); logAction('添加卡片', { name, url, category, isPrivate }); } } // 删除卡片 function removeCard(card) { const name = card.querySelector('.card-title').textContent; const url = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; links = links.filter(link => link.url !== url); if (isPrivate) { privateLinks = privateLinks.filter(link => link.url !== url); } else { publicLinks = publicLinks.filter(link => link.url !== url); } for (const key in categories) { categories[key] = categories[key].filter(link => link.url !== url); } card.remove(); saveLinks(); logAction('删除卡片', { name, url, isPrivate }); } // 拖拽卡片 let draggedCard = null; let touchStartX, touchStartY; // 触屏端拖拽卡片 function touchStart(event) { if (!isAdmin) return; draggedCard = event.target.closest('.card'); if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; touchStartX = touch.clientX; touchStartY = touch.clientY; draggedCard.classList.add('dragging'); document.addEventListener('touchmove', touchMove, { passive: false }); document.addEventListener('touchend', touchEnd); } function touchMove(event) { if (!draggedCard) return; event.preventDefault(); const touch = event.touches[0]; const currentX = touch.clientX; const currentY = touch.clientY; const deltaX = currentX - touchStartX; const deltaY = currentY - touchStartY; draggedCard.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)"; const target = findCardUnderTouch(currentX, currentY); if (target && target !== draggedCard) { const container = target.parentElement; const targetRect = target.getBoundingClientRect(); if (currentX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } function touchEnd(event) { if (!draggedCard) return; document.removeEventListener('touchmove', touchMove); document.removeEventListener('touchend', touchEnd); draggedCard.classList.remove('dragging'); draggedCard.style.transform = ''; const targetCategory = draggedCard.closest('.card-container').id; updateCardCategory(draggedCard, targetCategory); saveCardOrder(); draggedCard = null; } function findCardUnderTouch(x, y) { const cards = document.querySelectorAll('.card:not(.dragging)'); return Array.from(cards).find(card => { const rect = card.getBoundingClientRect(); return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; }); } // PC端拖拽卡片 function dragStart(event) { if (!isAdmin) return; draggedCard = event.target.closest('.card'); if (!draggedCard) return; draggedCard.classList.add('dragging'); event.dataTransfer.effectAllowed = "move"; logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent }); } function dragOver(event) { if (!isAdmin || !draggedCard) return; event.preventDefault(); const target = event.target.closest('.card'); if (target && target !== draggedCard) { const container = target.parentElement; const mousePositionX = event.clientX; const targetRect = target.getBoundingClientRect(); if (mousePositionX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } function drop(event) { if (!isAdmin || !draggedCard) return; event.preventDefault(); draggedCard.classList.remove('dragging'); const targetCategory = event.target.closest('.card-container').id; updateCardCategory(draggedCard, targetCategory); logAction('放下卡片', { name: draggedCard.querySelector('.card-title').textContent, category: targetCategory }); draggedCard = null; saveCardOrder(); } function dragEnd(event) { if (draggedCard) { draggedCard.classList.remove('dragging'); logAction('拖拽卡片结束'); draggedCard = null; } } // 更新卡片分类 function updateCardCategory(card, newCategory) { const cardTitle = card.querySelector('.card-title').textContent; const cardUrl = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; const linkIndex = links.findIndex(link => link.url === cardUrl); if (linkIndex !== -1) { links[linkIndex].category = newCategory; } const linkArray = isPrivate ? privateLinks : publicLinks; const arrayIndex = linkArray.findIndex(link => link.url === cardUrl); if (arrayIndex !== -1) { linkArray[arrayIndex].category = newCategory; } card.dataset.category = newCategory; } // 在页面加载完成后添加触摸事件监听器 document.addEventListener('DOMContentLoaded', function() { const cardContainers = document.querySelectorAll('.card-container'); cardContainers.forEach(container => { container.addEventListener('touchstart', touchStart, { passive: false }); }); }); // 保存卡片顺序 async function saveCardOrder() { if (!isAdmin) return; const containers = document.querySelectorAll('.card-container'); let newPublicLinks = []; let newPrivateLinks = []; let newCategories = {}; containers.forEach(container => { const category = container.id; newCategories[category] = []; [...container.children].forEach(card => { const url = card.querySelector('.card-url').textContent; const name = card.querySelector('.card-title').textContent; const isPrivate = card.dataset.isPrivate === 'true'; card.dataset.category = category; const link = { name, url, category, isPrivate }; if (isPrivate) { newPrivateLinks.push(link); } else { newPublicLinks.push(link); } newCategories[category].push(link); }); }); publicLinks.length = 0; publicLinks.push(...newPublicLinks); privateLinks.length = 0; privateLinks.push(...newPrivateLinks); Object.keys(categories).forEach(key => delete categories[key]); Object.assign(categories, newCategories); logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); try { const response = await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links: [...newPublicLinks, ...newPrivateLinks], categories: newCategories }), }); const result = await response.json(); if (!result.success) { throw new Error('Failed to save order'); } logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); } catch (error) { console.error('Error saving order:', error); logAction('保存顺序失败', { error: error.message }); alert('保存顺序失败,请重试'); } } // 设置状态重新加载卡片 function reloadCardsAsAdmin() { document.querySelectorAll('.card-container').forEach(container => { container.innerHTML = ''; }); loadLinks().then(() => { if (isDarkTheme) { applyDarkTheme(); } }); logAction('重新加载卡片(管理员模式)'); } // 密码输入框回车事件 document.getElementById('admin-password').addEventListener('keypress', (e) => { if (e.key === 'Enter') { toggleSecretGarden(); } }); // 切换设置状态 function toggleAdminMode() { const adminBtn = document.getElementById('admin-mode-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); if (!isAdmin && isLoggedIn) { isAdmin = true; adminBtn.textContent = "退出设置"; addRemoveControls.style.display = 'flex'; alert('准备设置分类和书签'); reloadCardsAsAdmin(); logAction('进入设置'); } else if (isAdmin) { isAdmin = false; removeMode = false; adminBtn.textContent = "设 置"; addRemoveControls.style.display = 'none'; alert('设置已保存'); reloadCardsAsAdmin(); logAction('离开设置'); } updateUIState(); } // 切换到登录状态 function toggleSecretGarden() { const passwordInput = document.getElementById('admin-password'); if (!isLoggedIn) { verifyPassword(passwordInput.value).then(isValid => { if (isValid) { isLoggedIn = true; links = [...publicLinks, ...privateLinks]; loadSections(); alert('登录成功!'); logAction('登录成功'); } else { alert('密码错误'); logAction('登录失败', { reason: '密码错误' }); } updateUIState(); }); } else { isLoggedIn = false; isAdmin = false; links = publicLinks; loadSections(); alert('退出登录!'); updateUIState(); passwordInput.value = ''; logAction('退出登录'); } } // 应用暗色主题 function applyDarkTheme() { document.body.style.backgroundColor = '#121212'; document.body.style.color = '#ffffff'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; }); logAction('应用暗色主题'); } // 显示添加链接对话框 function showAddDialog() { document.getElementById('dialog-overlay').style.display = 'flex'; logAction('显示添加链接对话框'); } // 隐藏添加链接对话框 function hideAddDialog() { document.getElementById('dialog-overlay').style.display = 'none'; logAction('隐藏添加链接对话框'); } // 切换删除卡片模式 function toggleRemoveMode() { removeMode = !removeMode; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => { btn.style.display = removeMode ? 'block' : 'none'; }); logAction('切换删除卡片模式', { removeMode }); } //切换删除分类模式 function toggleRemoveCategory() { isRemoveCategoryMode = !isRemoveCategoryMode; const deleteButtons = document.querySelectorAll('.delete-category-btn'); deleteButtons.forEach(btn => { btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; }); logAction('切换删除分类模式', { isRemoveCategoryMode }); } // 切换主题 function toggleTheme() { isDarkTheme = !isDarkTheme; document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#d8eac4'; document.body.style.color = isDarkTheme ? '#ffffff' : '#333'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#a0c9e5'; card.style.color = isDarkTheme ? '#ffffff' : '#333'; card.style.boxShadow = isDarkTheme ? '0 4px 8px rgba(0, 0, 0, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.1)'; }); const fixedElements = document.querySelectorAll('.fixed-elements'); fixedElements.forEach(element => { element.style.backgroundColor = isDarkTheme ? '#121212' : '#d8eac4'; element.style.color = isDarkTheme ? '#ffffff' : '#333'; }); const dialogBox = document.getElementById('dialog-box'); dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff'; dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333'; const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select'); inputs.forEach(input => { input.style.backgroundColor = isDarkTheme ? '#444' : '#fff'; input.style.color = isDarkTheme ? '#fff' : '#333'; input.style.borderColor = isDarkTheme ? '#555' : '#ccc'; }); logAction('切换主题', { isDarkTheme }); } // 验证密码 async function verifyPassword(inputPassword) { const response = await fetch('/api/verifyPassword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: inputPassword }), }); const result = await response.json(); return result.valid; } // 初始化加载链接 loadLinks(); </script> </body> </html> `; export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/') { return new Response(HTML_CONTENT, { headers: { 'Content-Type': 'text/html' } }); } if (url.pathname === '/api/getLinks') { const userId = url.searchParams.get('userId'); const links = await env.CARD_ORDER.get(userId); return new Response(links || JSON.stringify([]), { status: 200 }); } if (url.pathname === '/api/saveOrder' && request.method === 'POST') { const { userId, links, categories } = await request.json(); await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories })); //保存链接和分类 return new Response(JSON.stringify({ success: true }), { status: 200 }); } if (url.pathname === '/api/verifyPassword' && request.method === 'POST') { const { password } = await request.json(); const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码 return new Response(JSON.stringify({ valid: isValid }), { status: isValid ? 200 : 403, headers: { 'Content-Type': 'application/json' }, }); } return new Response('Not Found', { status: 404 }); } };修改后workes, 效果const HTML_CONTENT = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ws01主页</title> <link rel="icon" href="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299"> <style> /* 全局样式 */ body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #d4d4d4; transition: background-color 0.3s ease; } ul { padding: 0; margin-block-start: 1em; margin-block-end: 1em; margin-inline-start: 0px; margin-inline-end: 0px; padding-inline-start: 40px; unicode-bidi: isolate; } li { display: list-item; text-align: -webkit-match-parent; unicode-bidi: isolate; margin: 0 8px; } .background { background-image: linear-gradient(#d4d4d4 1px, transparent 0), linear-gradient(90deg, #d4d4d4 1px, transparent 0); background-size: 32px 32px; background-color: #fffcf8; } .header { background-color: #fff; box-shadow: 0 0 5px rgba(0, 0, 0, .1); transition: background-color .5s; } .navbar { display: flex; width: 1280px; margin: auto; height: 40px; } .navbar .brand { display: flex; align-items: center; color: #555; } .brand .logo { max-width: 36px; } .brand .title { margin-left: 5px; font-family: helvetica neue, helvetica, arial, sans-serif; font-size: 24px; font-weight: 700; } .beta { color: #ccc; font-size: 11px; font-weight: 400; position: relative; top: -14px; } .category-list { list-style: none; display: flex; align-items: center; } .sites { width:1286px; background:; border:2px solid auto; margin:15px auto; padding:0px; text-align:center; } .sites1 { width:1286px; background:; border:2px solid auto; margin:15px auto; padding:0px; } .sites dl { height:36px; line-height:36px; display:block; margin:0; } .sites dl.alt { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dl.alt2 { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dt,.sites dd { text-align:center; display:block; float:left; } .sites dt { width:60px; } .sites dd { width:90px; margin:0; } .footer { width:580px; text-align:center; margin:5px auto; padding:2px; } /* 固定元素样式 */ .fixed-elements { position: fixed; top: 0; left: 0; right: 0; background-color: #d4d4d4; z-index: 1000; padding: 10px; transition: background-color 0.3s ease; height: 130px; } .fixed-elements h3 { position: absolute; top: 10px; left: 20px; margin: 0; } /* 中心内容样式 */ .center-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; max-width: 600px; text-align: center; } /* 管理员控制面板样式 */ .admin-controls { position: fixed; top: 10px; right: 10px; font-size: 60%; } /* 添加/删除控制按钮样式 */ .add-remove-controls { display: none; flex-direction: column; position: fixed; right: 20px; top: 50%; transform: translateY(-50%); align-items: center; gap: 10px; } .round-btn { background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; margin: 5px 0; } .add-btn { order: 1; } .remove-btn { order: 2; } .category-btn { order: 3; } .remove-category-btn { order: 4; } /* 主要内容区域样式 */ .content { margin-top: 140px; padding: 20px; } /* 搜索栏样式 */ .search-container { margin-top: 10px; } .search-bar { display: flex; justify-content: center; margin-bottom: 10px; } .search-bar input { width: 50%; padding: 5px; border: 1px solid #ccc; border-radius: 5px 0 0 5px; } .search-bar button { padding: 5px 20px; border: 1px solid #ccc; border-left: none; background-color: #a0c9e5; border-radius: 0 5px 5px 0; cursor: pointer; } /* 搜索引擎按钮样式 */ .search-engines { display: flex; justify-content: center; gap: 10px; } .search-engine { padding: 5px 10px; border: 1px solid #ccc; background-color: #f0f0; border-radius: 5px; cursor: pointer; } /* 主题切换按钮样式 */ #theme-toggle { position: fixed; bottom: 50px; right: 20px; background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; } /* 显示日志按钮样式 */ #view-logs-btn { position: fixed; top: 100px; right: 10px; z-index: 1000; } /* 对话框样式 */ #dialog-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } #dialog-box { background-color: white; padding: 20px; border-radius: 5px; width: 300px; } #dialog-box input, #dialog-box select { width: 100%; margin-bottom: 10px; padding: 5px; } /* 分类和卡片样式 */ .section { margin-bottom: 20px; } .section-title-container { display: flex; align-items: center; margin-bottom: 10px; } .section-title { font-size: 18px; font-weight: bold; margin-left: 20px; /* 分类标签右移添加这一行 */ color: green; /* 分类标签字体颜色添加这一行 */ } .delete-category-btn { background-color: pink; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer; } .card-container { display: flex; flex-wrap: wrap; gap: 10px; } .card { background-color: #a0c9e5; border-radius: 5px; padding: 10px; width: 132px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); cursor: pointer; transition: transform 0.2s; position: relative; } /* 中等屏幕 (平板等) */ @media (max-width: 1024px) { } /* 小屏幕 (手机) */ @media (max-width: 768px) { } /* 超小屏幕 (更小手机) */ @media (max-width: 480px) { .card { padding: 6px; /* 进一步减小卡片内边距6 */ width: 235px; } } .card:hover { transform: translateY(-5px); } .card-top { display: flex; align-items: center; margin-bottom: 5px; } .card-icon { width: 20px; height: 20px; margin-right: 5px; } .card-title { font-size: 18px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-url { font-size: 12px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .private-tag { background-color: #ff9800; color: white; font-size: 10px; padding: 2px 5px; border-radius: 3px; position: absolute; top: 5px; right: 5px; } .delete-btn { position: absolute; top: -10px; right: -10px; background-color: red; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; text-align: center; font-size: 14px; line-height: 20px; cursor: pointer; display: none; } /* 版权信息样式 */ #copyright { position: fixed; bottom: 0; left: 0; width: 100%; height: 40px; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; font-size: 14px; z-index: 1000; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); } #copyright p { margin: 0; } #copyright a { color: #007bff; text-decoration: none; } #copyright a:hover { text-decoration: underline; } </style> </head> <body class="background"> </div> <div class="sites"> <!-- 00 --> <header class="header"> <nav class="navbar"> <a href="https://www.199881.xyz/" class="brand"> <img class="debug logo" src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299"> <span class="debug title">WS01の主页</span> <span class="debug beta">beta</span> <!-- 一言模块 --> <p id="hitokoto"> <a href="#" id="hitokoto_text"></a> </p> <script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script> </a> </header> <!-- 搜索栏 --> <div class="search-container"> <div class="search-bar"> <input type="text" id="search-input" placeholder=""> <button id="search-button">🔍</button> </div> <div class="search-engines"> <button class="search-engine" data-engine="baidu">百度</button> <button class="search-engine" data-engine="bing">必应</button> <button class="search-engine" data-engine="toutiao">头条</button> <button class="search-engine" data-engine="sm">神马</button> <button class="search-engine" data-engine="bilibili">哔哩</button> <button class="search-engine" data-engine="github">github</button> <button class="search-engine" data-engine="google">谷歌</button> <button class="search-engine" data-engine="yandex">yandex</button> </div> </div> <div class="head"> <!-- 添加/删除控制按钮 --> <div class="add-remove-controls"> <button class="round-btn add-btn" onclick="showAddDialog()">+</button> <button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button> <button class="round-btn category-btn" onclick="addCategory()">C+</button> <button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button> </div> <div class="sites1"> <!-- 00 --> <!-- 分类和卡片容器 --> <div id="sections-container"></div> <!-- 主题切换按钮 --> <button id="theme-toggle" onclick="toggleTheme()">◑</button> <!-- 显示日志按钮 用于调试--> <!--<button id="view-logs-btn" onclick="viewLogs()">显示日志</button>--> <!-- 添加链接对话框 --> <div id="dialog-overlay"> <div id="dialog-box"> <label for="name-input">名称</label> <input type="text" id="name-input"> <label for="url-input">地址</label> <input type="text" id="url-input"> <label for="category-select">选择分类</label> <select id="category-select"></select> <div class="private-link-container"> <label for="private-checkbox">私密链接</label> <input type="checkbox" id="private-checkbox"> </div> <button onclick="addLink()">确定</button> <button onclick="hideAddDialog()">取消</button> </div> </div> <br /> <!-- body 页脚 --> <div class="footer"> <!-- 管理员控制面板 --> <input type="password" id="admin-password" placeholder="输入密码"> <button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button> <button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button> <br /> <br /> <br /> <!-- 版权信息 --> <div id="copyright" class="copyright"> <!--请不要删除--> <p><a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 项目 <!-- 开站时间开始 --> <span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span> <script language="javascript"> var now = new Date(); function createtime(){ var grt= new Date("09/05/2024 00:00:00");/*---这里是网站的启用时间--*/ now.setTime(now.getTime()+250); days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days); hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum); hnum = Math.floor(hours); if(String(hnum).length ==1 ){hnum = "0" + hnum;} minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum); mnum = Math.floor(minutes); if(String(mnum).length ==1 ){mnum = "0" + mnum;} seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum); snum = Math.round(seconds); if(String(snum).length ==1 ){snum = "0" + snum;} document.getElementById("timeDate").innerHTML = "稳定运行"+dnum+"天"; document.getElementById("times").innerHTML = hnum + "小时" + mnum + "分" + snum + "秒"; } setInterval("createtime()",250); </script> <!-- 开站时间结束 --> </p> </div> </div> <script> // 搜索引擎配置 const searchEngines = { baidu: "https://www.baidu.com/s?wd=", bing: "https://www.bing.com/search?q=", sm: "https://m.sm.cn/s?q=", toutiao: "https://so.toutiao.com/search?dvpf=pc&source=trending_card&keyword=", bilibili: "https://search.bilibili.com/all?keyword=", github: "https://github.com/search?q=", google: "https://www.google.com/search?q=", yandex: "https://www.yandex.com/search/?text=" }; let currentEngine = "baidu"; // 日志记录函数 function logAction(action, details) { const timestamp = new Date().toISOString(); const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details); let logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); logs.push(logEntry); // 保留最新的1000条日志 if (logs.length > 1000) { logs = logs.slice(-1000); } localStorage.setItem('cardTabLogs', JSON.stringify(logs)); console.log(logEntry); // 同时在控制台输出日志 } // 查看日志的函数 function viewLogs() { const logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]'); console.log('Card Tab Logs:'); logs.forEach(log => console.log(log)); alert('日志已在控制台输出,请按F12打开开发者工具查看。'); } // 设置当前搜索引擎 function setActiveEngine(engine) { currentEngine = engine; document.querySelectorAll('.search-engine').forEach(btn => { btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0'; }); logAction('设置搜索引擎', { engine }); } // 搜索引擎按钮点击事件 document.querySelectorAll('.search-engine').forEach(button => { button.addEventListener('click', () => setActiveEngine(button.dataset.engine)); }); // 搜索按钮点击事件 document.getElementById('search-button').addEventListener('click', () => { const query = document.getElementById('search-input').value; if (query) { logAction('执行搜索', { engine: currentEngine, query }); window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank'); } }); // 搜索输入框回车事件 document.getElementById('search-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('search-button').click(); } }); // 初始化搜索引擎 setActiveEngine(currentEngine); // 全局变量 let publicLinks = []; let privateLinks = []; let isAdmin = false; let isLoggedIn = false; let removeMode = false; let isRemoveCategoryMode = false; let isDarkTheme = false; let links = []; const categories = {}; // 添加新分类 function addCategory() { const categoryName = prompt('请输入新分类名称:'); if (categoryName && !categories[categoryName]) { categories[categoryName] = []; updateCategorySelect(); renderCategories(); saveLinks(); logAction('添加分类', { categoryName, currentLinkCount: links.length }); } else if (categories[categoryName]) { alert('该分类已存在'); logAction('添加分类失败', { categoryName, reason: '分类已存在' }); } } // 删除分类 function deleteCategory(category) { if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) { delete categories[category]; links = links.filter(link => link.category !== category); publicLinks = publicLinks.filter(link => link.category !== category); privateLinks = privateLinks.filter(link => link.category !== category); updateCategorySelect(); saveLinks(); renderCategories(); logAction('删除分类', { category }); } } // 渲染分类(不重新加载链接) function renderCategories() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); container.appendChild(section); // 只渲染属于当前分类的链接 const categoryLinks = links.filter(link => link.category === category); categoryLinks.forEach(link => { createCard(link, cardContainer); }); }); logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length }); } // 读取链接数据 async function loadLinks() { const response = await fetch('/api/getLinks?userId=testUser'); const data = await response.json(); if (data.categories) { Object.assign(categories, data.categories); } publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : []; privateLinks = data.links ? data.links.filter(link => link.isPrivate) : []; links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; loadSections(); updateCategorySelect(); updateUIState(); logAction('读取链接', { publicCount: publicLinks.length, privateCount: privateLinks.length }); } // 更新UI状态 function updateUIState() { const passwordInput = document.getElementById('admin-password'); const adminBtn = document.getElementById('admin-mode-btn'); const secretGardenBtn = document.getElementById('secret-garden-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block'; secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录"; secretGardenBtn.style.display = 'inline-block'; if (isAdmin) { adminBtn.textContent = "离开设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'flex'; } else if (isLoggedIn) { adminBtn.textContent = "设置"; adminBtn.style.display = 'inline-block'; addRemoveControls.style.display = 'none'; } else { adminBtn.style.display = 'none'; addRemoveControls.style.display = 'none'; } logAction('更新UI状态', { isAdmin, isLoggedIn }); } // 登录状态显示(加载所有链接) function showSecretGarden() { if (isLoggedIn) { links = [...publicLinks, ...privateLinks]; loadSections(); // 显示所有私密标签 document.querySelectorAll('.private-tag').forEach(tag => { tag.style.display = 'block'; }); logAction('显示私密花园'); } } // 加载分类和链接 function loadSections() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const titleContainer = document.createElement('div'); titleContainer.className = 'section-title-container'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; titleContainer.appendChild(title); if (isAdmin) { const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除分类'; deleteBtn.className = 'delete-category-btn'; deleteBtn.style.display = 'none'; deleteBtn.onclick = () => deleteCategory(category); titleContainer.appendChild(deleteBtn); } const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(titleContainer); section.appendChild(cardContainer); let privateCount = 0; let linkCount = 0; links.forEach(link => { if (link.category === category) { if (link.isPrivate) privateCount++; linkCount++; createCard(link, cardContainer); } }); if (privateCount < linkCount || isLoggedIn) { container.appendChild(section); } }); logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length }); } // 创建卡片 function createCard(link, container) { const card = document.createElement('div'); card.className = 'card'; card.setAttribute('draggable', isAdmin); card.dataset.isPrivate = link.isPrivate; const cardTop = document.createElement('div'); cardTop.className = 'card-top'; const icon = document.createElement('img'); icon.className = 'card-icon'; icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url; icon.alt = 'Website Icon'; const title = document.createElement('div'); title.className = 'card-title'; title.textContent = link.name; cardTop.appendChild(icon); cardTop.appendChild(title); const url = document.createElement('div'); url.className = 'card-url'; url.textContent = link.url; card.appendChild(cardTop); card.appendChild(url); if (link.isPrivate) { const privateTag = document.createElement('div'); privateTag.className = 'private-tag'; privateTag.textContent = '私密'; card.appendChild(privateTag); } const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url; if (!isAdmin) { card.addEventListener('click', () => { window.open(correctedUrl, '_blank'); logAction('打开链接', { name: link.name, url: correctedUrl }); }); } const deleteBtn = document.createElement('button'); deleteBtn.textContent = '–'; deleteBtn.className = 'delete-btn'; deleteBtn.onclick = function (event) { event.stopPropagation(); removeCard(card); }; card.appendChild(deleteBtn); updateCardStyle(card); card.addEventListener('dragstart', dragStart); card.addEventListener('dragover', dragOver); card.addEventListener('dragend', dragEnd); card.addEventListener('drop', drop); if (isAdmin && removeMode) { deleteBtn.style.display = 'block'; } if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) { container.appendChild(card); } // logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate }); } // 更新卡片样式 function updateCardStyle(card) { if (isDarkTheme) { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; } else { card.style.backgroundColor = '#a0c9e5'; card.style.color = '#333'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; } } // 更新分类选择下拉框 function updateCategorySelect() { const categorySelect = document.getElementById('category-select'); categorySelect.innerHTML = ''; Object.keys(categories).forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; categorySelect.appendChild(option); }); logAction('更新分类选择', { categoryCount: Object.keys(categories).length }); } // 保存链接数据 async function saveLinks() { let allLinks = [...publicLinks, ...privateLinks]; try { await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links: allLinks, categories: categories }), }); logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length }); } catch (error) { console.error('Error saving links:', error); logAction('保存链接失败', { error: error.message }); alert('保存链接失败,请重试'); } } // 添加卡片弹窗 function addLink() { const name = document.getElementById('name-input').value; const url = document.getElementById('url-input').value; const category = document.getElementById('category-select').value; const isPrivate = document.getElementById('private-checkbox').checked; if (name && url && category) { const newLink = { name, url, category, isPrivate }; if (isPrivate) { privateLinks.push(newLink); } else { publicLinks.push(newLink); } links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks; if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) { const container = document.getElementById(category); if (container) { createCard(newLink, container); } else { categories[category] = []; renderCategories(); } } saveLinks(); document.getElementById('name-input').value = ''; document.getElementById('url-input').value = ''; document.getElementById('private-checkbox').checked = false; hideAddDialog(); logAction('添加卡片', { name, url, category, isPrivate }); } } // 删除卡片 function removeCard(card) { const name = card.querySelector('.card-title').textContent; const url = card.querySelector('.card-url').textContent; const isPrivate = card.dataset.isPrivate === 'true'; links = links.filter(link => link.url !== url); if (isPrivate) { privateLinks = privateLinks.filter(link => link.url !== url); } else { publicLinks = publicLinks.filter(link => link.url !== url); } for (const key in categories) { categories[key] = categories[key].filter(link => link.url !== url); } card.remove(); saveLinks(); logAction('删除卡片', { name, url, isPrivate }); } // 拖拽卡片 let draggedCard = null; function dragStart(event) { if (!isAdmin) return; draggedCard = event.target; draggedCard.classList.add('dragging'); event.dataTransfer.effectAllowed = "move"; logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent }); } function dragOver(event) { if (!isAdmin) return; event.preventDefault(); const target = event.target.closest('.card'); if (target && target !== draggedCard) { const container = target.parentElement; const mousePositionX = event.clientX; const targetRect = target.getBoundingClientRect(); if (mousePositionX < targetRect.left + targetRect.width /2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } //logAction('拖拽卡片中', { over: target ? target.querySelector('.card-title').textContent : 'unknown' }); } function drop(event) { if (!isAdmin) return; event.preventDefault(); draggedCard.classList.remove('dragging'); const targetCategory = event.target.closest('.card-container').id; // 更新卡片的分类信息 const cardTitle = draggedCard.querySelector('.card-title').textContent; const cardUrl = draggedCard.querySelector('.card-url').textContent; const isPrivate = draggedCard.dataset.isPrivate === 'true'; // 在 links 数组中更新卡片信息 const linkIndex = links.findIndex(link => link.url === cardUrl); if (linkIndex !== -1) { links[linkIndex].category = targetCategory; } // 在 publicLinks 或 privateLinks 中更新卡片信息 const linkArray = isPrivate ? privateLinks : publicLinks; const arrayIndex = linkArray.findIndex(link => link.url === cardUrl); if (arrayIndex !== -1) { linkArraycategory = targetCategory; } draggedCard.dataset.category = targetCategory; logAction('放下卡片', { name: cardTitle, category: targetCategory }); draggedCard = null; saveCardOrder(); } function dragEnd(event) { if (draggedCard) { draggedCard.classList.remove('dragging'); logAction('拖拽卡片结束'); } } // 保存卡片顺序 async function saveCardOrder() { if (!isAdmin) return; const containers = document.querySelectorAll('.card-container'); let newPublicLinks = []; let newPrivateLinks = []; let newCategories = {}; containers.forEach(container => { const category = container.id; newCategories[category] = []; [...container.children].forEach(card => { const url = card.querySelector('.card-url').textContent; const name = card.querySelector('.card-title').textContent; const isPrivate = card.dataset.isPrivate === 'true'; card.dataset.category = category; const link = { name, url, category, isPrivate }; if (isPrivate) { newPrivateLinks.push(link); } else { newPublicLinks.push(link); } newCategories[category].push(link); }); }); publicLinks.length = 0; publicLinks.push(...newPublicLinks); privateLinks.length = 0; privateLinks.push(...newPrivateLinks); Object.keys(categories).forEach(key => delete categories[key]); Object.assign(categories, newCategories); logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); try { const response = await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links: [...newPublicLinks, ...newPrivateLinks], categories: newCategories }), }); const result = await response.json(); if (!result.success) { throw new Error('Failed to save order'); } logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length }); } catch (error) { console.error('Error saving order:', error); logAction('保存顺序失败', { error: error.message }); alert('保存顺序失败,请重试'); } } // 设置状态重新加载卡片 function reloadCardsAsAdmin() { document.querySelectorAll('.card-container').forEach(container => { container.innerHTML = ''; }); loadLinks().then(() => { if (isDarkTheme) { applyDarkTheme(); } }); logAction('重新加载卡片(管理员模式)'); } // 密码输入框回车事件 document.getElementById('admin-password').addEventListener('keypress', (e) => { if (e.key === 'Enter') { toggleSecretGarden(); } }); // 切换设置状态 function toggleAdminMode() { const adminBtn = document.getElementById('admin-mode-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); if (!isAdmin && isLoggedIn) { isAdmin = true; adminBtn.textContent = "退出设置"; addRemoveControls.style.display = 'flex'; alert('准备设置分类和书签'); reloadCardsAsAdmin(); logAction('进入设置'); } else if (isAdmin) { isAdmin = false; removeMode = false; adminBtn.textContent = "设 置"; addRemoveControls.style.display = 'none'; alert('设置已保存'); reloadCardsAsAdmin(); logAction('离开'); } updateUIState(); } // 切换到登录状态 function toggleSecretGarden() { const passwordInput = document.getElementById('admin-password'); if (!isLoggedIn) { verifyPassword(passwordInput.value).then(isValid => { if (isValid) { isLoggedIn = true; links = [...publicLinks, ...privateLinks]; loadSections(); alert('登录成功!'); logAction('登录成功'); } else { alert('密码错误'); logAction('登录失败', { reason: '密码错误' }); } updateUIState(); }); } else { isLoggedIn = false; isAdmin = false; links = publicLinks; loadSections(); alert('退出登录!'); updateUIState(); passwordInput.value = ''; logAction('退出登录'); } } // 应用暗色主题 function applyDarkTheme() { document.body.style.backgroundColor = '#121212'; document.body.style.color = '#ffffff'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; }); logAction('应用暗色主题'); } // 显示添加链接对话框 function showAddDialog() { document.getElementById('dialog-overlay').style.display = 'flex'; logAction('显示添加链接对话框'); } // 隐藏添加链接对话框 function hideAddDialog() { document.getElementById('dialog-overlay').style.display = 'none'; logAction('隐藏添加链接对话框'); } // 切换删除卡片模式 function toggleRemoveMode() { removeMode = !removeMode; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => { btn.style.display = removeMode ? 'block' : 'none'; }); logAction('切换删除卡片模式', { removeMode }); } //切换删除分类模式 function toggleRemoveCategory() { isRemoveCategoryMode = !isRemoveCategoryMode; const deleteButtons = document.querySelectorAll('.delete-category-btn'); deleteButtons.forEach(btn => { btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none'; }); logAction('切换删除分类模式', { isRemoveCategoryMode }); } // 切换主题 function toggleTheme() { isDarkTheme = !isDarkTheme; document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff'; document.body.style.color = isDarkTheme ? '#ffffff' : '#333'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#a0c9e5'; card.style.color = isDarkTheme ? '#ffffff' : '#333'; card.style.boxShadow = isDarkTheme ? '0 4px 8px rgba(0, 0, 0, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.1)'; }); const fixedElements = document.querySelectorAll('.fixed-elements'); fixedElements.forEach(element => { element.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff'; element.style.color = isDarkTheme ? '#ffffff' : '#333'; }); const dialogBox = document.getElementById('dialog-box'); dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff'; dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333'; const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select'); inputs.forEach(input => { input.style.backgroundColor = isDarkTheme ? '#444' : '#fff'; input.style.color = isDarkTheme ? '#fff' : '#333'; input.style.borderColor = isDarkTheme ? '#555' : '#ccc'; }); logAction('切换主题', { isDarkTheme }); } // 验证密码 async function verifyPassword(inputPassword) { const response = await fetch('/api/verifyPassword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: inputPassword }), }); const result = await response.json(); return result.valid; } // 初始化加载链接 loadLinks(); </script> </body> </html> `; export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/') { return new Response(HTML_CONTENT, { headers: { 'Content-Type': 'text/html' } }); } if (url.pathname === '/api/getLinks') { const userId = url.searchParams.get('userId'); const links = await env.CARD_ORDER.get(userId); return new Response(links || JSON.stringify([]), { status: 200 }); } if (url.pathname === '/api/saveOrder' && request.method === 'POST') { const { userId, links, categories } = await request.json(); await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories })); //保存链接和分类 return new Response(JSON.stringify({ success: true }), { status: 200 }); } if (url.pathname === '/api/verifyPassword' && request.method === 'POST') { const { password } = await request.json(); const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码 return new Response(JSON.stringify({ valid: isValid }), { status: isValid ? 200 : 403, headers: { 'Content-Type': 'application/json' }, }); } return new Response('Not Found', { status: 404 }); } };{dotted startColor="#ff6c6c" endColor="#1989fa"/}旧版本1、原workes, 效果const HTML_CONTENT = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Card Tab</title> <style> body { font-family: Arial, sans-serif; // background-color: #f4f4f4; background-color: #d8eac4; margin: 0; padding: 20px; display: flex; flex-direction: column; align-items: center; transition: background-color 0.3s ease; } .card-container { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; } .card { display: flex; flex-direction: column; position: relative; background-color: #a0c9e5; padding: 10px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); cursor: grab; transition: transform 0.2s ease, box-shadow 0.2s ease; width: 200px; height: auto; } .card-top { display: flex; align-items: center; margin-bottom: 5px; } .card-icon { width: 24px; height: 24px; margin-right: 10px; } .card-title { font-size: 16px; font-weight: bold; } .card-url { color: #555; font-size: 12px; word-break: break-all; } .card.dragging { opacity: 0.8; transform: scale(1.05); cursor: grabbing; } .card:hover { transform: translateY(-5px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); } .delete-btn { position: absolute; top: -10px; right: -10px; background-color: red; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; text-align: center; font-size: 14px; line-height: 20px; cursor: pointer; display: none; } .admin-controls { position: fixed; top: 10px; right: 10px; font-size: 60%; } .admin-controls input { padding: 5px; font-size: 60%; } .admin-controls button { padding: 5px 10px; font-size: 60%; margin-left: 10px; } .add-remove-controls { display: none; margin-top: 10px; } .round-btn { background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; margin: 0 10px; } #theme-toggle { position: fixed; bottom: 10px; left: 10px; background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; } #dialog-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } #dialog-box { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } #dialog-box label { display: block; margin-bottom: 5px; } #dialog-box input, #dialog-box select { width: 100%; padding: 5px; margin-bottom: 10px; } #dialog-box button { padding: 5px 10px; margin-right: 10px; } .section { margin-bottom: 20px; } .section-title { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 10px; } </style> </head> <body> <h1>我的导航</h1> <div class="admin-controls"> <input type="password" id="admin-password" placeholder="输入密码"> <button id="admin-mode-btn" onclick="toggleAdminMode()">进入管理模式</button> </div> <div class="add-remove-controls"> <button class="round-btn" onclick="showAddDialog()">+</button> <button class="round-btn" onclick="toggleRemoveMode()">-</button> </div> <div id="sections-container"> <!-- 分类将在这里动态生成 --> </div> <button id="theme-toggle" onclick="toggleTheme()">◑</button> <div id="dialog-overlay"> <div id="dialog-box"> <label for="name-input">名称</label> <input type="text" id="name-input"> <label for="url-input">地址</label> <input type="text" id="url-input"> <label for="category-select">选择分类</label> <select id="category-select"> <!-- 分类选项将在这里动态生成 --> </select> <button onclick="addLink()">确定</button> <button onclick="hideAddDialog()">取消</button> </div> </div> <div class="copyright"> <!-- 请不要删除 --> <p> 项目地址: <a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 烦请点个star! </div> <script> let isAdmin = false; let removeMode = false; let isDarkTheme = false; let links = []; const categories = { "常用网站": [], // **编辑自己的网站分类** "工具导航": [], "游戏娱乐": [], "影音视听": [], "技术论坛": [] }; async function loadLinks() { const response = await fetch('/api/getLinks?userId=testUser'); links = await response.json(); Object.keys(categories).forEach(key => { categories[key] = []; }); links.forEach(link => { if (categories[link.category]) { categories[link.category].push(link); } }); loadSections(); updateCategorySelect(); // applyTheme(); } function loadSections() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(title); section.appendChild(cardContainer); categories[category].forEach(link => { createCard(link, cardContainer); }); container.appendChild(section); }); } function createCard(link, container) { const card = document.createElement('div'); card.className = 'card'; card.setAttribute('draggable', isAdmin); const cardTop = document.createElement('div'); cardTop.className = 'card-top'; const icon = document.createElement('img'); icon.className = 'card-icon'; // icon.src = 'https://www.google.com/s2/favicons?domain=' + link.url; icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url; icon.alt = 'Website Icon'; const title = document.createElement('div'); title.className = 'card-title'; title.textContent = link.name; cardTop.appendChild(icon); cardTop.appendChild(title); const url = document.createElement('div'); url.className = 'card-url'; url.textContent = link.url; card.appendChild(cardTop); card.appendChild(url); // URL 检查和修正 function correctUrl(url) { if (url.startsWith('http://') || url.startsWith('https://')) { return url; } else { return 'http://' + url; } } let correctedUrl = correctUrl(link.url); if (!isAdmin) { card.addEventListener('click', () => { window.open(correctedUrl, '_blank'); }); } const deleteBtn = document.createElement('button'); deleteBtn.textContent = '–'; deleteBtn.className = 'delete-btn'; deleteBtn.onclick = function (event) { event.stopPropagation(); removeCard(card); }; card.appendChild(deleteBtn); if (isDarkTheme) { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; } else { card.style.backgroundColor = '#a0c9e5'; card.style.color = '#333'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; } card.addEventListener('dragstart', dragStart); card.addEventListener('dragover', dragOver); card.addEventListener('dragend', dragEnd); card.addEventListener('drop', drop); if (isAdmin && removeMode) { deleteBtn.style.display = 'block'; } container.appendChild(card); } function updateCategorySelect() { const categorySelect = document.getElementById('category-select'); categorySelect.innerHTML = ''; Object.keys(categories).forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; categorySelect.appendChild(option); }); } async function saveLinks() { let links = []; for (const category in categories) { links = links.concat(categories[category]); } await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links }), }); } function addLink() { const name = document.getElementById('name-input').value; const url = document.getElementById('url-input').value; const category = document.getElementById('category-select').value; if (name && url && category) { const newLink = { name, url, category }; if (!categories[category]) { categories[category] = []; } categories[category].push(newLink); const container = document.getElementById(category); createCard(newLink, container); saveLinks(); document.getElementById('name-input').value = ''; document.getElementById('url-input').value = ''; hideAddDialog(); } } function removeCard(card) { const url = card.querySelector('.card-url').textContent; let category; for (const key in categories) { const index = categories[key].findIndex(link => link.url === url); if (index !== -1) { categories[key].splice(index, 1); category = key; break; } } card.remove(); saveLinks(); } let draggedCard = null; function dragStart(event) { if (!isAdmin) return; draggedCard = event.target; draggedCard.classList.add('dragging'); event.dataTransfer.effectAllowed = "move"; } function dragOver(event) { if (!isAdmin) return; event.preventDefault(); const target = event.target.closest('.card'); if (target && target !== draggedCard) { const container = target.parentElement; const mousePositionX = event.clientX; const targetRect = target.getBoundingClientRect(); if (mousePositionX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } function drop(event) { if (!isAdmin) return; event.preventDefault(); draggedCard.classList.remove('dragging'); draggedCard = null; saveCardOrder(); } // function dragEnd(event) { // draggedCard.classList.remove('dragging'); function dragEnd(event) { if (draggedCard) { draggedCard.classList.remove('dragging'); } } async function saveCardOrder() { if (!isAdmin) return; const containers = document.querySelectorAll('.card-container'); let newLinks = []; containers.forEach(container => { const category = container.id; categories[category] = []; [...container.children].forEach(card => { const url = card.querySelector('.card-url').textContent; const name = card.querySelector('.card-title').textContent; const link = { name, url, category }; categories[category].push(link); newLinks.push(link); }); }); links = newLinks; await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links: newLinks }), }); } function toggleAdminMode() { const passwordInput = document.getElementById('admin-password'); const adminBtn = document.getElementById('admin-mode-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); if (!isAdmin) { verifyPassword(passwordInput.value) .then(isValid => { if (isValid) { isAdmin = true; adminBtn.textContent = "退出管理模式"; alert('已进入管理模式'); addRemoveControls.style.display = 'block'; reloadCardsAsAdmin(); } else { alert('密码错误'); } }); } else { isAdmin = false; removeMode = false; adminBtn.textContent = "进入管理模式"; alert('已退出管理模式'); addRemoveControls.style.display = 'none'; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => btn.style.display = 'none'); reloadCardsAsAdmin(); } passwordInput.value = ''; } function reloadCardsAsAdmin() { document.querySelectorAll('.card-container').forEach(container => { container.innerHTML = ''; }); loadLinks().then(() => { if (isDarkTheme) { applyDarkTheme(); } }); } function applyDarkTheme() { document.body.style.backgroundColor = '#121212'; document.body.style.color = '#ffffff'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; }); } function showAddDialog() { document.getElementById('dialog-overlay').style.display = 'flex'; } function hideAddDialog() { document.getElementById('dialog-overlay').style.display = 'none'; } function toggleRemoveMode() { removeMode = !removeMode; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => { btn.style.display = removeMode ? 'block' : 'none'; }); } function toggleTheme() { isDarkTheme = !isDarkTheme; // 设置暗色主题和亮色主题的背景色 document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#d8eac4'; // 设置暗色主题和亮色主题的文本颜色 document.body.style.color = isDarkTheme ? '#ffffff' : '#333'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { // 卡片背景和文本颜色设置 card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#a0c9e5'; card.style.color = isDarkTheme ? '#ffffff' : '#333'; // 卡片阴影的设置,增强暗色主题的阴影 card.style.boxShadow = isDarkTheme ? '0 4px 8px rgba(0, 0, 0, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.1)'; }); const dialogBox = document.getElementById('dialog-box'); // 对话框背景和文本颜色设置 dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff'; dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333'; const inputs = dialogBox.querySelectorAll('input, select'); inputs.forEach(input => { // 输入框背景和文本颜色设置 input.style.backgroundColor = isDarkTheme ? '#333333' : '#ffffff'; input.style.color = isDarkTheme ? '#ffffff' : '#333'; }); } async function verifyPassword(inputPassword) { const response = await fetch('/api/verifyPassword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: inputPassword }), }); const result = await response.json(); return result.valid; } loadLinks(); </script> </body> </html> `; export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/') { return new Response(HTML_CONTENT, { headers: { 'Content-Type': 'text/html' } }); } if (url.pathname === '/api/getLinks') { const userId = url.searchParams.get('userId'); const links = await env.CARD_ORDER.get(userId); return new Response(links || JSON.stringify([]), { status: 200 }); } if (url.pathname === '/api/saveOrder' && request.method === 'POST') { const { userId, links } = await request.json(); await env.CARD_ORDER.put(userId, JSON.stringify(links)); return new Response(JSON.stringify({ success: true }), { status: 200 }); } if (url.pathname === '/api/verifyPassword' && request.method === 'POST') { const { password } = await request.json(); const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码 return new Response(JSON.stringify({ valid: isValid }), { status: isValid ? 200 : 403, headers: { 'Content-Type': 'application/json' }, }); } return new Response('Not Found', { status: 404 }); } };2、修改后的workes, 效果const HTML_CONTENT = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WS01の主页</title> <style> body { font-family: Arial, sans-serif; // background-color: #f4f4f4; background-color: #d4d4d4; margin: 0; padding: 20px; display: flex; flex-direction: column; align-items: center; transition: background-color 0.3s ease; } ul { padding: 0; margin-block-start: 1em; margin-block-end: 1em; margin-inline-start: 0px; margin-inline-end: 0px; padding-inline-start: 40px; unicode-bidi: isolate; } li { display: list-item; text-align: -webkit-match-parent; unicode-bidi: isolate; margin: 0 8px; } .background { background-image: linear-gradient(#d4d4d4 1px, transparent 0), linear-gradient(90deg, #d4d4d4 1px, transparent 0); background-size: 32px 32px; background-color: #fffcf8; } .header { background-color: #fff; box-shadow: 0 0 5px rgba(0, 0, 0, .1); transition: background-color .5s; } .navbar { display: flex; width: 1280px; margin: auto; height: 40px; } .navbar .brand { display: flex; align-items: center; color: #555; } .brand .logo { max-width: 36px; } .brand .title { margin-left: 5px; font-family: helvetica neue, helvetica, arial, sans-serif; font-size: 24px; font-weight: 700; } .beta { color: #ccc; font-size: 11px; font-weight: 400; position: relative; top: -14px; } .category-list { list-style: none; display: flex; align-items: center; } .sites { width:1280px; background:; border:2px solid auto; margin:15px auto; padding:0px; text-align:center; } .sites1 { width:1280px; background:; border:2px solid auto; margin:15px auto; padding:0px; } .sites dl { height:36px; line-height:36px; display:block; margin:0; } .sites dl.alt { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dl.alt2 { background:#d4d4d4; border-top:1px solid #ffffff; border-bottom:1px solid #ffffff; } .sites dt,.sites dd { text-align:center; display:block; float:left; } .sites dt { width:60px; } .sites dd { width:90px; margin:0; } .footer { width:580px; text-align:center; margin:5px auto; padding:2px; } .card-container { display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px; width: 100%; /* 宽度设为100%,以适应不同设备 */ } .card { display: flex; flex-direction: column; position: relative; background-color: #d4d4d4; padding: 10px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); cursor: grab; transition: transform 0.2s ease, box-shadow 0.2s ease; word-break: break-word; /* 防止超出边界 */ } /* 中等屏幕 (平板等) */ @media (max-width: 1024px) { .card-container { grid-template-columns: repeat(8, 1fr); /* 中等屏幕显示4列 */ } } /* 小屏幕 (手机) */ @media (max-width: 768px) { .card-container { grid-template-columns: repeat(6, 1fr); /* 小屏幕显示2列 */ } .card { padding: 8px; /* 减小卡片内边距 */ font-size: 0.9rem; /* 调整字体大小0.9 */ } .admin-controls { top: 5px; right: 5px; } .round-btn, #theme-toggle { width: 30px; height: 30px; font-size: 18px; line-height: 30px; } } /* 超小屏幕 (更小手机) */ @media (max-width: 480px) { .card-container { grid-template-columns: repeat(5, 1fr); /* 超小屏幕显示1列 */ } .card { padding: 6px; /* 进一步减小卡片内边距5 */ font-size: 0.8rem; /* 再次缩小字体0.8 */ } .round-btn, #theme-toggle { width: 25px; height: 25px; font-size: 16px; line-height: 25px; } } .card-top { display: flex; align-items: center; margin-bottom: 5px; } .card-icon { width: 20px; height: 20px; margin-right: 8px; } .card-title { font-size: 16px; font-weight: bold; } .card-url { color: #555; font-size: 12px; word-break: break-all; } .card.dragging { opacity: 0.8; transform: scale(1.05); cursor: grabbing; } .card:hover { transform: translateY(-5px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } .delete-btn { position: absolute; top: -10px; right: -10px; background-color: red; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; text-align: center; font-size: 14px; line-height: 20px; cursor: pointer; display: none; } .admin-controls { position: fixed; top: 10px; right: 10px; font-size: 60%; } .admin-controls input { padding: 5px; font-size: 60%; } .admin-controls button { padding: 5px 10px; font-size: 60%; margin-left: 10px; } .add-remove-controls { display: none; margin-top: 10px; } .round-btn { background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; margin: 0 10px; } #theme-toggle { position: fixed; bottom: 10px; left: 10px; background-color: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 40px; cursor: pointer; } #dialog-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } #dialog-box { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } #dialog-box label { display: block; margin-bottom: 5px; } #dialog-box input, #dialog-box select { width: 100%; padding: 5px; margin-bottom: 10px; } #dialog-box button { padding: 5px 10px; margin-right: 10px; } .section { margin-bottom: 20px; } .section-title { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 10px; } </style> </head> <body class="background"> </div> <div class="sites"> <!-- 00 --> <header class="header"> <nav class="navbar"> <a href="https://ddo.us.kg/" class="brand"> <img class="debug logo" src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299"> <span class="debug title">WS01の主页</span> <span class="debug beta">beta</span> </a> </header> </div> </div> <div class="add-remove-controls"> <button class="round-btn" onclick="showAddDialog()">+</button> <button class="round-btn" onclick="toggleRemoveMode()">-</button> </div> <div class="sites1"> <!-- 00 --> <div id="sections-container"> <!-- 分类将在这里动态生成 --> </div> <button id="theme-toggle" onclick="toggleTheme()">◑</button> <div id="dialog-overlay"> <div id="dialog-box"> <label for="name-input">名称</label> <input type="text" id="name-input"> <label for="url-input">地址</label> <input type="text" id="url-input"> <label for="category-select">选择分类</label> <select id="category-select"> <!-- 分类选项将在这里动态生成 --> </select> <button onclick="addLink()">确定</button> <button onclick="hideAddDialog()">取消</button> </div> </div> <div class="copyright"> <!-- 请不要删除 --> <br /> <!-- body 页脚 --> <div class="footer"> <input type="password" id="admin-password" placeholder="输入密码"> <button id="admin-mode-btn" onclick="toggleAdminMode()">进入管理模式</button> <p> <a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 项目 <!-- 开站时间开始 --> <span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span> <script language="javascript"> var now = new Date(); function createtime(){ var grt= new Date("09/05/2024 00:00:00");/*---这里是网站的启用时间--*/ now.setTime(now.getTime()+250); days = (now - grt ) / 1000 / 60 / 60 / 24; dnum = Math.floor(days); hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum); hnum = Math.floor(hours); if(String(hnum).length ==1 ){hnum = "0" + hnum;} minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum); mnum = Math.floor(minutes); if(String(mnum).length ==1 ){mnum = "0" + mnum;} seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum); snum = Math.round(seconds); if(String(snum).length ==1 ){snum = "0" + snum;} document.getElementById("timeDate").innerHTML = "稳定运行"+dnum+"天"; document.getElementById("times").innerHTML = hnum + "小时" + mnum + "分" + snum + "秒"; } setInterval("createtime()",250); </script> <!-- 开站时间结束 --> </div> <script> let isAdmin = false; let removeMode = false; let isDarkTheme = false; let links = []; const categories = { "常·用": [], // **编辑自己的网站分类** "工·具": [], "影·音": [], "N·B·A": [], "论·坛": [], "主·页": [], "玩·具": [], "v·p·s": [], "下·载": [], "商·城": [], "搜·译": [], "学·习": [], "其·它": [] }; async function loadLinks() { const response = await fetch('/api/getLinks?userId=testUser'); links = await response.json(); Object.keys(categories).forEach(key => { categories[key] = []; }); links.forEach(link => { if (categories[link.category]) { categories[link.category].push(link); } }); loadSections(); updateCategorySelect(); // applyTheme(); } function loadSections() { const container = document.getElementById('sections-container'); container.innerHTML = ''; Object.keys(categories).forEach(category => { const section = document.createElement('div'); section.className = 'section'; const title = document.createElement('div'); title.className = 'section-title'; title.textContent = category; const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.id = category; section.appendChild(title); section.appendChild(cardContainer); categories[category].forEach(link => { createCard(link, cardContainer); }); container.appendChild(section); }); } function createCard(link, container) { const card = document.createElement('div'); card.className = 'card'; card.setAttribute('draggable', isAdmin); const cardTop = document.createElement('div'); cardTop.className = 'card-top'; const icon = document.createElement('img'); icon.className = 'card-icon'; // icon.src = 'https://www.google.com/s2/favicons?domain=' + link.url; icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url; icon.alt = 'Website Icon'; const title = document.createElement('div'); title.className = 'card-title'; title.textContent = link.name; cardTop.appendChild(icon); cardTop.appendChild(title); const url = document.createElement('div'); url.className = 'card-url'; url.textContent = link.url; card.appendChild(cardTop); card.appendChild(url); // URL 检查和修正 function correctUrl(url) { if (url.startsWith('http://') || url.startsWith('https://')) { return url; } else { return 'http://' + url; } } let correctedUrl = correctUrl(link.url); if (!isAdmin) { card.addEventListener('click', () => { window.open(correctedUrl, '_blank'); }); } const deleteBtn = document.createElement('button'); deleteBtn.textContent = '–'; deleteBtn.className = 'delete-btn'; deleteBtn.onclick = function (event) { event.stopPropagation(); removeCard(card); }; card.appendChild(deleteBtn); if (isDarkTheme) { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; } else { card.style.backgroundColor = '#d4d4d4'; card.style.color = '#333'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; } card.addEventListener('dragstart', dragStart); card.addEventListener('dragover', dragOver); card.addEventListener('dragend', dragEnd); card.addEventListener('drop', drop); if (isAdmin && removeMode) { deleteBtn.style.display = 'block'; } container.appendChild(card); } function updateCategorySelect() { const categorySelect = document.getElementById('category-select'); categorySelect.innerHTML = ''; Object.keys(categories).forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; categorySelect.appendChild(option); }); } async function saveLinks() { let links = []; for (const category in categories) { links = links.concat(categories[category]); } await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links }), }); } function addLink() { const name = document.getElementById('name-input').value; const url = document.getElementById('url-input').value; const category = document.getElementById('category-select').value; if (name && url && category) { const newLink = { name, url, category }; if (!categories[category]) { categories[category] = []; } categories[category].push(newLink); const container = document.getElementById(category); createCard(newLink, container); saveLinks(); document.getElementById('name-input').value = ''; document.getElementById('url-input').value = ''; hideAddDialog(); } } function removeCard(card) { const url = card.querySelector('.card-url').textContent; let category; for (const key in categories) { const index = categories[key].findIndex(link => link.url === url); if (index !== -1) { categories[key].splice(index, 1); category = key; break; } } card.remove(); saveLinks(); } let draggedCard = null; function dragStart(event) { if (!isAdmin) return; draggedCard = event.target; draggedCard.classList.add('dragging'); event.dataTransfer.effectAllowed = "move"; } function dragOver(event) { if (!isAdmin) return; event.preventDefault(); const target = event.target.closest('.card'); if (target && target !== draggedCard) { const container = target.parentElement; const mousePositionX = event.clientX; const targetRect = target.getBoundingClientRect(); if (mousePositionX < targetRect.left + targetRect.width / 2) { container.insertBefore(draggedCard, target); } else { container.insertBefore(draggedCard, target.nextSibling); } } } function drop(event) { if (!isAdmin) return; event.preventDefault(); draggedCard.classList.remove('dragging'); draggedCard = null; saveCardOrder(); } // function dragEnd(event) { // draggedCard.classList.remove('dragging'); function dragEnd(event) { if (draggedCard) { draggedCard.classList.remove('dragging'); } } async function saveCardOrder() { if (!isAdmin) return; const containers = document.querySelectorAll('.card-container'); let newLinks = []; containers.forEach(container => { const category = container.id; categories[category] = []; [...container.children].forEach(card => { const url = card.querySelector('.card-url').textContent; const name = card.querySelector('.card-title').textContent; const link = { name, url, category }; categories[category].push(link); newLinks.push(link); }); }); links = newLinks; await fetch('/api/saveOrder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId: 'testUser', links: newLinks }), }); } function toggleAdminMode() { const passwordInput = document.getElementById('admin-password'); const adminBtn = document.getElementById('admin-mode-btn'); const addRemoveControls = document.querySelector('.add-remove-controls'); if (!isAdmin) { verifyPassword(passwordInput.value) .then(isValid => { if (isValid) { isAdmin = true; adminBtn.textContent = "退出管理模式"; alert('已进入管理模式'); addRemoveControls.style.display = 'block'; reloadCardsAsAdmin(); } else { alert('密码错误'); } }); } else { isAdmin = false; removeMode = false; adminBtn.textContent = "进入管理模式"; alert('已退出管理模式'); addRemoveControls.style.display = 'none'; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => btn.style.display = 'none'); reloadCardsAsAdmin(); } passwordInput.value = ''; } function reloadCardsAsAdmin() { document.querySelectorAll('.card-container').forEach(container => { container.innerHTML = ''; }); loadLinks().then(() => { if (isDarkTheme) { applyDarkTheme(); } }); } function applyDarkTheme() { document.body.style.backgroundColor = '#121212'; document.body.style.color = '#ffffff'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { card.style.backgroundColor = '#1e1e1e'; card.style.color = '#ffffff'; card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)'; }); } function showAddDialog() { document.getElementById('dialog-overlay').style.display = 'flex'; } function hideAddDialog() { document.getElementById('dialog-overlay').style.display = 'none'; } function toggleRemoveMode() { removeMode = !removeMode; const deleteButtons = document.querySelectorAll('.delete-btn'); deleteButtons.forEach(btn => { btn.style.display = removeMode ? 'block' : 'none'; }); } function toggleTheme() { isDarkTheme = !isDarkTheme; // 设置暗色主题和亮色主题的背景色 document.body.style.backgroundColor = isDarkTheme ? '#696969' : '#FFFFFF'; // 设置暗色主题和亮色主题的文本颜色 document.body.style.color = isDarkTheme ? '#ffffff' : '#333'; const cards = document.querySelectorAll('.card'); cards.forEach(card => { // 卡片背景和文本颜色设置 card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#d4d4d4'; card.style.color = isDarkTheme ? '#ffffff' : '#333'; // 卡片阴影的设置,增强暗色主题的阴影 card.style.boxShadow = isDarkTheme ? '0 4px 8px rgba(0, 0, 0, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.1)'; }); const dialogBox = document.getElementById('dialog-box'); // 对话框背景和文本颜色设置 dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff'; dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333'; const inputs = dialogBox.querySelectorAll('input, select'); inputs.forEach(input => { // 输入框背景和文本颜色设置 input.style.backgroundColor = isDarkTheme ? '#333333' : '#ffffff'; input.style.color = isDarkTheme ? '#ffffff' : '#333'; }); } async function verifyPassword(inputPassword) { const response = await fetch('/api/verifyPassword', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: inputPassword }), }); const result = await response.json(); return result.valid; } loadLinks(); </script> </body> </html> `; export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/') { return new Response(HTML_CONTENT, { headers: { 'Content-Type': 'text/html' } }); } if (url.pathname === '/api/getLinks') { const userId = url.searchParams.get('userId'); const links = await env.CARD_ORDER.get(userId); return new Response(links || JSON.stringify([]), { status: 200 }); } if (url.pathname === '/api/saveOrder' && request.method === 'POST') { const { userId, links } = await request.json(); await env.CARD_ORDER.put(userId, JSON.stringify(links)); return new Response(JSON.stringify({ success: true }), { status: 200 }); } if (url.pathname === '/api/verifyPassword' && request.method === 'POST') { const { password } = await request.json(); const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码 return new Response(JSON.stringify({ valid: isValid }), { status: isValid ? 200 : 403, headers: { 'Content-Type': 'application/json' }, }); } return new Response('Not Found', { status: 404 }); } };二、添加变量1、先建立一个kv空间,名字为: CARD_ORDER ,再在 变量-KV 命名空间绑定 刚才建立的kv空间。2、变量-环境变量 中添加 ADMIN_PASSWORD ,值是你的后台管理员密码3、有域名的可邦定域名,完成。
2024年09月08日
0 阅读
1 评论
0 点赞
2024-08-27
套cf后 站点WAF防火墙规则设定 这2+1就够了 全能防御恶意流量攻击!
套cf后 站点WAF防火墙规则设定 这2+1就够了 全能防御恶意流量攻击!转自 科技L 自己网站受攻击是正常的,下面说说怎么在cf防范打开cf网站受攻击的域名,安全性---WAF---自定义规则,一般可免费创建五个规则,下面是网友建好的,当然规则是死的,人是活的,许多地方可以举一反三,灵活修改【遇到强烈攻击时,宁可错杀一千不放过一个ip的原则选择交互式质询或阻止,规则先后秩序一般是 先放行后阻止 】1、放行跳过机器人扫描规则-KJL(cf.client.bot) or (http.user_agent contains "duckduckgo") or (http.user_agent contains "facebookexternalhit") or (http.user_agent contains "Feedfetcher-Google") or (http.user_agent contains "LinkedInBot") or (http.user_agent contains "Mediapartners-Google") or (http.user_agent contains "msnbot") or (http.user_agent contains "Slackbot") or (http.user_agent contains "TwitterBot") or (http.user_agent contains "ia_archive") or (http.user_agent contains "yahoo")2、全球用户js质询访问记录全球所有的用户访问都会有ip记录,方便你追踪,通过5s盾保护安全(ip.geoip.continent eq "AF") or (ip.geoip.continent eq "AN") or (ip.geoip.continent eq "AS") or (ip.geoip.continent eq "EU") or (ip.geoip.continent eq "NA") or (ip.geoip.continent eq "OC") or (ip.geoip.continent eq "SA") or (ip.geoip.continent eq "T1")3、恶意流量托管质询规则-KJL【可修改威胁分数值,值越大越严格,当然也可以把托管质询修改为更严格的 交互式质询或阻止 】2025年8月8日修改(cf.threat_score ge 15 and not cf.client.bot) or (not http.request.version in {"HTTP/1.1" "HTTP/2" "HTTP/3"}) or (not http.user_agent contains "Mozilla/")原来规则(cf.threat_score ge 5 and not cf.client.bot) or (not http.request.version in {"HTTP/1.2" "HTTP/2" "HTTP/3" "SPDY/3.1"}) or (not http.user_agent contains "Mozilla/")4、可适当添加速率限制规则限制时间窗口(比如 10 秒)限制请求数(比如 10 次)【可修改为20次、30次等】超过就 阻止/质询(block/challenge)//login/api/三个网站页面(http.request.uri.path eq "/") or (http.request.uri.path eq "/login") or (http.request.uri.path eq "/api/")
2024年08月27日
0 阅读
0 评论
0 点赞
1
2