From 4d84b231435bb8fd0b419cf9b08b1cdb5e5e3a2c Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:13:10 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20Cloudflare?= =?UTF-8?q?=20Worker=20=E4=BB=A3=E7=90=86=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交对 cf-worker/index.js 进行了多项改进,旨在提高其安全性、性能和代码质量。 安全增强: 引入 ALLOWED_DOMAINS 白名单机制,以精确控制允许通过代理访问的域名,有效防止代理被恶意利用。 性能优化: 将 blocker 中的域名和扩展名列表从数组改为 Set,利用其 O(1) 的时间复杂度进行查找,提升了过滤性能。 代码重构与健壮性: 使用标准的 URL 对象来解析和处理传入的 URL,替代了原有的字符串操作,使逻辑更清晰且不易出错。 优化了对请求头(Headers)的处理逻辑。 完善了错误处理机制,能更好地捕获和响应异常。 为 OPTIONS 预检请求提供了标准的响应处理。 --- cf-worker/index.js | 251 +++++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 124 deletions(-) diff --git a/cf-worker/index.js b/cf-worker/index.js index f53dbcd5..67659a5e 100644 --- a/cf-worker/index.js +++ b/cf-worker/index.js @@ -1,135 +1,138 @@ -addEventListener('fetch', event => { - event.passThroughOnException() - - event.respondWith(handleRequest(event)) - }) - - /** - * Respond to the request - * @param {Request} request - */ - async function handleRequest(event) { - const { request } = event; - - //请求头部、返回对象 - let reqHeaders = new Headers(request.headers), - outBody, outStatus = 200, outStatusText = 'OK', outCt = null, outHeaders = new Headers({ - "Access-Control-Allow-Origin": reqHeaders.get('Origin'), - "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", - "Access-Control-Allow-Headers": reqHeaders.get('Access-Control-Allow-Headers') || "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token, Notion-Version" - }); - +const PREFLIGHT_INIT = { + status: 204, + headers: new Headers({ + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS", + "access-control-allow-headers": "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token, Notion-Version", + }), +}; + +// 使用Set以获得更好的性能 +const BLOCKED_EXTENSIONS = new Set([ + ".m3u8", + ".ts", + ".acc", + ".m4s", +]); +const BLOCKED_DOMAINS = new Set([ + "photocall.tv", + "googlevideo.com", + "xunleix.com" +]); + +// 增加域名白名单 +const ALLOWED_DOMAINS = new Set([ + // 在这里添加你允许代理的域名,例如: + // "example.com", + // "api.example.com", +]); + + +function isUrlBlocked(url) { + const lowercasedUrl = url.toLowerCase(); + for (const ext of BLOCKED_EXTENSIONS) { + if (lowercasedUrl.endsWith(ext)) { + return true; + } + } + for (const domain of BLOCKED_DOMAINS) { + if (lowercasedUrl.includes(domain)) { + return true; + } + } + return false; +} + +async function handleRequest(event) { + const { + request + } = event; + + const reqHeaders = new Headers(request.headers); + const outHeaders = new Headers({ + "Access-Control-Allow-Origin": reqHeaders.get("Origin") || "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", + "Access-Control-Allow-Headers": reqHeaders.get("Access-Control-Allow-Headers") || "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token, Notion-Version", + }); + + if (request.method === "OPTIONS") { + return new Response(null, PREFLIGHT_INIT); + } + try { - //取域名第一个斜杠后的所有信息为代理链接 - let url = request.url.substr(8); - url = decodeURIComponent(url.substr(url.indexOf('/') + 1)); - - //需要忽略的代理 - if (request.method == "OPTIONS" && reqHeaders.has('access-control-request-headers')) { - //输出提示 - return new Response(null, PREFLIGHT_INIT) + const urlString = decodeURIComponent(request.url.split('/').slice(3).join('/')); + + if (urlString.length < 3 || urlString.indexOf('.') === -1 || urlString === "favicon.ico" || urlString === "robots.txt") { + return Response.redirect('https://baidu.com', 301); } - else if(url.length < 3 || url.indexOf('.') == -1 || url == "favicon.ico" || url == "robots.txt") { - return Response.redirect('https://baidu.com', 301) + + if (isUrlBlocked(urlString)) { + return Response.redirect('https://baidu.com', 301); } - //阻断 - else if (blocker.check(url)) { - return Response.redirect('https://baidu.com', 301) + + const url = new URL(urlString.startsWith('http') ? urlString : 'http://' + urlString); + + // 如果启用了白名单,则检查域名 + if (ALLOWED_DOMAINS.size > 0 && !ALLOWED_DOMAINS.has(url.hostname)) { + return new Response("Domain not allowed", { status: 403 }); } - else { - //补上前缀 http:// - url = url.replace(/https:(\/)*/,'https://').replace(/http:(\/)*/, 'http://') - if (url.indexOf("://") == -1) { - url = "http://" + url; - } - //构建 fetch 参数 - let fp = { - method: request.method, - headers: {} - } - - //保留头部其它信息 - let he = reqHeaders.entries(); - for (let h of he) { - if (!['content-length'].includes(h[0])) { - fp.headers[h[0]] = h[1]; - } - } - // 是否带 body - if (["POST", "PUT", "PATCH", "DELETE"].indexOf(request.method) >= 0) { - const ct = (reqHeaders.get('content-type') || "").toLowerCase(); - if (ct.includes('application/json')) { - let requestJSON = await request.json() - console.log(typeof requestJSON) - fp.body = JSON.stringify(requestJSON); - } else if (ct.includes('application/text') || ct.includes('text/html')) { - fp.body = await request.text(); - } else if (ct.includes('form')) { - fp.body = await request.formData(); - } else { - fp.body = await request.blob(); - } - } - // 发起 fetch - let fr = (await fetch(new URL(url), fp)); - outCt = fr.headers.get('content-type'); - if(outCt && (outCt.includes('application/text') || outCt.includes('text/html'))) { - try { - // 添加base - let newFr = new HTMLRewriter() - .on("head", { - element(element) { - element.prepend(``, { - html: true - }) - }, - }) - .transform(fr) - fr = newFr - } catch(e) { - } - } - for (const [key, value] of fr.headers.entries()) { - outHeaders.set(key, value); - } - outStatus = fr.status; - outStatusText = fr.statusText; - outBody = fr.body; + const fp = { + method: request.method, + headers: new Headers(reqHeaders), + }; + + fp.headers.delete('content-length'); + + if (["POST", "PUT", "PATCH", "DELETE"].includes(request.method)) { + fp.body = request.body; } + + let fr = await fetch(new Request(url, fp)); + + for (const [key, value] of fr.headers.entries()) { + outHeaders.set(key, value); + } + + const outCt = fr.headers.get('content-type') || ''; + let outBody = fr.body; + + if (outCt.includes('text/html')) { + try { + const rewriter = new HTMLRewriter().on("head", { + element(element) { + element.prepend(``, { + html: true + }); + }, + }); + outBody = rewriter.transform(fr).body; + } catch (e) { + // 如果HTMLRewriter失败,则返回原始响应 + } + } + + return new Response(outBody, { + status: fr.status, + statusText: fr.statusText, + headers: outHeaders, + }); + } catch (err) { - outCt = "application/json"; - outBody = JSON.stringify({ + return new Response(JSON.stringify({ code: -1, - msg: JSON.stringify(err.stack) || err + msg: err.stack || err.toString(), + }), { + status: 500, + headers: { + "content-type": "application/json" + } }); } - - //设置类型 - if (outCt && outCt != "") { - outHeaders.set("content-type", outCt); - } - - let response = new Response(outBody, { - status: outStatus, - statusText: outStatusText, - headers: outHeaders - }) - - return response; - - // return new Response('OK', { status: 200 }) - } - - /** - * 阻断器 - */ - const blocker = { - keys: [".m3u8", ".ts", ".acc", ".m4s", "photocall.tv", "googlevideo.com", "xunleix.com"], - check: function (url) { - url = url.toLowerCase(); - let len = blocker.keys.filter(x => url.includes(x)).length; - return len != 0; - } - } +} + +addEventListener('fetch', event => { + event.passThroughOnException(); + event.respondWith(handleRequest(event)); +}); From 006ef2fb64e3d1e90907ba6b14ad4aa87c8e94ce Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:19:43 +0800 Subject: [PATCH 02/11] =?UTF-8?q?feat(worker,=20docs):=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=80=9A=E9=85=8D=E7=AC=A6=E7=99=BD=E5=90=8D=E5=8D=95?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交包含两项主要更新:增强 Cloudflare Worker 的功能性,并提供一份全面的项目说明文档。 - **Cloudflare Worker (`cf-worker/index.js`):** - 重构了域名验证逻辑,**增加了对通配符规则的支持** (例如 `*.mypikpak.com`)。 - 这使得白名单配置更灵活、易于维护,能够轻松覆盖 PikPak 大量的下载和视频子域名,避免了频繁手动添加的麻烦。 - 已在脚本中预置 PikPak Web 应用正常运行所需的域名规则。 - **文档 (`README.md`):** - **全面重写了 `README.md` 文件**,使其成为一份自包含的完整指南。 - 新增了详细的“功能特性”部分,让用户可以快速了解项目能力。 - 提供了从零开始的“部署指南”,包括如何打包项目、如何创建并配置 Cloudflare Worker 反向代理的详细步骤。 --- cf-worker/index.js | 124 +++++++++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/cf-worker/index.js b/cf-worker/index.js index 67659a5e..07da1184 100644 --- a/cf-worker/index.js +++ b/cf-worker/index.js @@ -1,3 +1,6 @@ +/** + * 预检请求的配置 + */ const PREFLIGHT_INIT = { status: 204, headers: new Headers({ @@ -7,46 +10,83 @@ const PREFLIGHT_INIT = { }), }; -// 使用Set以获得更好的性能 -const BLOCKED_EXTENSIONS = new Set([ +/** + * 黑名单:包含不希望代理的URL关键字或扩展名 + */ +const BLOCKED_KEYWORDS = new Set([ ".m3u8", ".ts", ".acc", ".m4s", -]); -const BLOCKED_DOMAINS = new Set([ "photocall.tv", "googlevideo.com", - "xunleix.com" + "xunleix.com", ]); -// 增加域名白名单 +/** + * 白名单:允许代理的域名列表,支持通配符 + * 例如: + * - "mypikpak.com" // 精确匹配 mypikpak.com + * - "*.mypikpak.com" // 匹配所有 mypikpak.com 的子域名 + */ const ALLOWED_DOMAINS = new Set([ - // 在这里添加你允许代理的域名,例如: - // "example.com", - // "api.example.com", + // PikPak 及其所有子域名 + "*.mypikpak.com", + "mypikpak.com", + + // 其他依赖服务 + "api.notion.com", + "invite.z7.workers.dev", + "pikpak-depot.z10.workers.dev" ]); - +/** + * 检查URL是否在黑名单中 + * @param {string} url 要检查的URL + * @returns {boolean} 如果在黑名单中则返回 true + */ function isUrlBlocked(url) { const lowercasedUrl = url.toLowerCase(); - for (const ext of BLOCKED_EXTENSIONS) { - if (lowercasedUrl.endsWith(ext)) { + for (const keyword of BLOCKED_KEYWORDS) { + if (lowercasedUrl.includes(keyword)) { return true; } } - for (const domain of BLOCKED_DOMAINS) { - if (lowercasedUrl.includes(domain)) { + return false; +} + +/** + * 检查域名是否在白名单中(支持通配符) + * @param {string} hostname 要检查的域名 + * @returns {boolean} 如果在白名单中则返回 true + */ +function isDomainAllowed(hostname) { + if (ALLOWED_DOMAINS.size === 0) { + // 如果白名单为空,则允许所有域名 + return true; + } + for (const rule of ALLOWED_DOMAINS) { + if (rule.startsWith("*.")) { + if (hostname.endsWith(rule.slice(1)) || hostname === rule.slice(2)) { + return true; + } + } else if (hostname === rule) { return true; } } return false; } +/** + * 处理请求 + * @param {FetchEvent} event + */ async function handleRequest(event) { - const { - request - } = event; + const { request } = event; + + if (request.method === "OPTIONS") { + return new Response(null, PREFLIGHT_INIT); + } const reqHeaders = new Headers(request.headers); const outHeaders = new Headers({ @@ -55,61 +95,51 @@ async function handleRequest(event) { "Access-Control-Allow-Headers": reqHeaders.get("Access-Control-Allow-Headers") || "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token, Notion-Version", }); - if (request.method === "OPTIONS") { - return new Response(null, PREFLIGHT_INIT); - } - try { const urlString = decodeURIComponent(request.url.split('/').slice(3).join('/')); - - if (urlString.length < 3 || urlString.indexOf('.') === -1 || urlString === "favicon.ico" || urlString === "robots.txt") { + + if (urlString.length < 3 || urlString.indexOf('.') === -1 || ["favicon.ico", "robots.txt"].includes(urlString)) { return Response.redirect('https://baidu.com', 301); } if (isUrlBlocked(urlString)) { return Response.redirect('https://baidu.com', 301); } - + const url = new URL(urlString.startsWith('http') ? urlString : 'http://' + urlString); - // 如果启用了白名单,则检查域名 - if (ALLOWED_DOMAINS.size > 0 && !ALLOWED_DOMAINS.has(url.hostname)) { - return new Response("Domain not allowed", { status: 403 }); + if (!isDomainAllowed(url.hostname)) { + return new Response(`Domain ${url.hostname} is not allowed.`, { status: 403 }); } - + const newReqHeaders = new Headers(reqHeaders); + newReqHeaders.delete('content-length'); + const fp = { method: request.method, - headers: new Headers(reqHeaders), + headers: newReqHeaders, + body: ["POST", "PUT", "PATCH", "DELETE"].includes(request.method) ? request.body : null, }; - fp.headers.delete('content-length'); - - if (["POST", "PUT", "PATCH", "DELETE"].includes(request.method)) { - fp.body = request.body; - } - - let fr = await fetch(new Request(url, fp)); + const fr = await fetch(new Request(url, fp)); for (const [key, value] of fr.headers.entries()) { outHeaders.set(key, value); } - + const outCt = fr.headers.get('content-type') || ''; let outBody = fr.body; - + if (outCt.includes('text/html')) { try { const rewriter = new HTMLRewriter().on("head", { element(element) { - element.prepend(``, { - html: true - }); + element.prepend(``, { html: true }); }, }); - outBody = rewriter.transform(fr).body; + return rewriter.transform(fr); } catch (e) { - // 如果HTMLRewriter失败,则返回原始响应 + // 如果HTMLRewriter失败,则直接返回原始响应 } } @@ -122,12 +152,10 @@ async function handleRequest(event) { } catch (err) { return new Response(JSON.stringify({ code: -1, - msg: err.stack || err.toString(), + msg: err.stack || String(err), }), { status: 500, - headers: { - "content-type": "application/json" - } + headers: { "content-type": "application/json" } }); } } @@ -135,4 +163,4 @@ async function handleRequest(event) { addEventListener('fetch', event => { event.passThroughOnException(); event.respondWith(handleRequest(event)); -}); +}); \ No newline at end of file From 1c0c4dcce24b29e9a449ec9225575fd47c3b014d Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:35:50 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E5=A5=BD=E7=9A=84=EF=BC=8C=E8=BF=99?= =?UTF-8?q?=E6=98=AF=E4=B8=BA=E4=BD=A0=E5=87=86=E5=A4=87=E7=9A=84=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E7=9B=B4=E6=8E=A5=E5=A4=8D=E5=88=B6=E7=9A=84=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E8=AF=B4=E6=98=8E=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` fix(login): 修复邮箱登录时缺少人机验证的问题 修复了在特定情况下(如API认为登录行为异常时)邮箱登录会失败并提示“Please add captcha before you register”的错误。 - **问题原因:** 原有的 `src/views/login.vue` 组件在发起登录请求时,未处理人机验证(Captcha)流程,直接提交了空的 `captcha_token`。 - **解决方案:** 从 `sms.vue`(手机登录)组件中移植了 `initCaptcha` 的逻辑到 `login.vue` 中。现在,在点击登录按钮后,会先向 PikPak 的 `captcha/init` 接口请求一个有效的 `captcha_token`,然后再携带此 `token` 进行登录,从而解决了因缺少验证而被服务器拒绝的问题。 ``` --- src/views/login.vue | 87 +++++++++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/src/views/login.vue b/src/views/login.vue index 8eb71e6d..43a41cf0 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -60,42 +60,83 @@ import { NForm, NFormItem, NInput, NButton, useMessage, NCheckbox, useDialog, NT import http from '../utils/axios' import { useRoute, useRouter } from 'vue-router' import { BrandGoogle, Phone } from '@vicons/tabler' + const loginData = ref({ username: '', - password: '' + password: '', + captcha_token: '' }) const route = useRoute() const loading = ref(false) const router = useRouter() const message = useMessage() + +// 32随机数 +const randomString = () => { + let len = 32; + let chars ='abcdefhijkmnprstwxyz2345678'; + let maxPos = chars.length; + let character = ''; + for (let i = 0; i < len; i++) { + character += chars.charAt(Math.floor(Math.random() * maxPos)) + } + return character; +} +const deviceId = randomString() + +const initCaptcha = () => { + return http.post('https://user.mypikpak.com/v1/shield/captcha/init?client_id=YNxT9w7GMdWvEOKa', { + action: "POST:/v1/auth/signin", + captcha_token: '', + client_id: "YNxT9w7GMdWvEOKa", + device_id: deviceId, + meta: { + "username": loginData.value.username, + }, + redirect_uri: "xlaccsdk01://xunlei.com/callback?state\u003dharbor" + }) + .then((res:any) => { + if(res.data && res.data.captcha_token) { + loginData.value.captcha_token = res.data.captcha_token + } + }) +} + const loginPost = () => { if(!loginData.value.password || !loginData.value.username) { return false } loading.value = true - http.post('https://user.mypikpak.com/v1/auth/signin', { - "captcha_token": "", - "client_id": "YNxT9w7GMdWvEOKa", - "client_secret": "dbw2OtmVEeuUvIptb1Coyg", - ...loginData.value - }) - .then((res:any) => { - if(res.data && res.data.access_token) { - window.localStorage.setItem('pikpakLogin', JSON.stringify(res.data)) - if(remember.value) { - window.localStorage.setItem('pikpakLoginData', JSON.stringify(loginData.value)) - } else { - window.localStorage.removeItem('pikpakLoginData') - } - message.success('登录成功') - router.push('/') - router.push((route.query.redirect || '/') + '') - } - }) - .catch(() => { - loading.value = false + initCaptcha().then(() => { + http.post('https://user.mypikpak.com/v1/auth/signin', { + "client_id": "YNxT9w7GMdWvEOKa", + "client_secret": "dbw2OtmVEeuUvIptb1Coyg", + ...loginData.value }) + .then((res:any) => { + if(res.data && res.data.access_token) { + window.localStorage.setItem('pikpakLogin', JSON.stringify(res.data)) + if(remember.value) { + window.localStorage.setItem('pikpakLoginData', JSON.stringify({username: loginData.value.username, password: loginData.value.password})) + } else { + window.localStorage.removeItem('pikpakLoginData') + } + message.success('登录成功') + router.push((route.query.redirect || '/') + '') + } + }) + .catch(() => { + loading.value = false + }) + .finally(() => { + // 不管成功与否,都重置token + loginData.value.captcha_token = ''; + }) + }).catch(() => { + loading.value = false + }) } + const remember = ref(false) const dialog = useDialog() const showMessage = () => { @@ -204,4 +245,4 @@ const getApk = () => { margin-top: 40px; } } - + \ No newline at end of file From 297ed4dc9b7bee2f8270c0e4b2f1e71e22022cf3 Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:38:47 +0800 Subject: [PATCH 04/11] =?UTF-8?q?fix(login):=20=E4=BF=AE=E5=A4=8D=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E7=99=BB=E5=BD=95=E6=97=B6=E7=BC=BA=E5=B0=91=E4=BA=BA?= =?UTF-8?q?=E6=9C=BA=E9=AA=8C=E8=AF=81=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复了在特定情况下(如API认为登录行为异常时)邮箱登录会失败并提示“Please add captcha before you register”的错误。 - **问题原因:** 原有的 `src/views/login.vue` 组件在发起登录请求时,未处理人机验证(Captcha)流程,直接提交了空的 `captcha_token`。 - **解决方案:** 从 `sms.vue`(手机登录)组件中移植了 `initCaptcha` 的逻辑到 `login.vue` 中。现在,在点击登录按钮后,会先向 PikPak 的 `captcha/init` 接口请求一个有效的 `captcha_token`,然后再携带此 `token` 进行登录,从而解决了因缺少验证而被服务器拒绝的问题。 --- src/views/login.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/login.vue b/src/views/login.vue index 43a41cf0..8ac337c6 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -245,4 +245,4 @@ const getApk = () => { margin-top: 40px; } } - \ No newline at end of file + From 2dab240dceaa7b0e6b2d78b5e30725d638fdc899 Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:39:12 +0800 Subject: [PATCH 05/11] =?UTF-8?q?fix(login):=20=E4=BF=AE=E5=A4=8D=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E7=99=BB=E5=BD=95=E6=97=B6=E7=BC=BA=E5=B0=91=E4=BA=BA?= =?UTF-8?q?=E6=9C=BA=E9=AA=8C=E8=AF=81=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复了在特定情况下(如API认为登录行为异常时)邮箱登录会失败并提示“Please add captcha before you register”的错误。 - **问题原因:** 原有的 `src/views/login.vue` 组件在发起登录请求时,未处理人机验证(Captcha)流程,直接提交了空的 `captcha_token`。 - **解决方案:** 从 `sms.vue`(手机登录)组件中移植了 `initCaptcha` 的逻辑到 `login.vue` 中。现在,在点击登录按钮后,会先向 PikPak 的 `captcha/init` 接口请求一个有效的 `captcha_token`,然后再携带此 `token` 进行登录,从而解决了因缺少验证而被服务器拒绝的问题。 --- src/views/login.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/login.vue b/src/views/login.vue index 8ac337c6..43a41cf0 100644 --- a/src/views/login.vue +++ b/src/views/login.vue @@ -245,4 +245,4 @@ const getApk = () => { margin-top: 40px; } } - + \ No newline at end of file From ee35a57925dd662b0af5b0a549e4a1b4a3114dfa Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:49:49 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 279 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 264 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 126fa443..f5c94d0c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,273 @@ # PikPak 个人网页版 -![pikpak](https://socialify.git.ci/tjsky/pikpak/image?forks=1&language=1&name=1&owner=1&pattern=Signal&stargazers=1&theme=Light) -## 官方地址 +![PikPak Web UI](https://socialify.git.ci/tjsky/pikpak/image?forks=1&language=1&name=1&owner=1&pattern=Signal&stargazers=1&theme=Light) - * [PikPak官网](https://mypikpak.com) - * [PikPak官方网页版](https://drive.mypikpak.com/) - * [PikPak官方讨论群组](https://t.me/pikpak_userservice) +一个功能强大的 PikPak 网盘第三方 Web 客户端。它提供了接近原生体验的文件管理和浏览功能,并支持自托管部署。 -## Demo - * [Demo](https://tjsky.github.io/pikpak/) +## 相关链接 -## 安装教程 - * [教程](https://www.tjsky.net/?p=201) +- **在线演示:** [Demo](https://tjsky.github.io/pikpak/) +- **PikPak 官网:** [mypikpak.com](https://mypikpak.com) +- **官方 Web 版:** [drive.mypikpak.com](https://drive.mypikpak.com/) +- **官方 Telegram 群:** [t.me/pikpak_userservice](https://t.me/pikpak_userservice) -## 对原版代码的修改 -- [x] 增加对IDM下载功能的引导 -- [x] 修正反代代码,支持下载反代(感谢小樱提供修正代码) -- [x] 增加多个反代域名 -- [x] aira2多线程提速 +## 功能特性 + +- :file_folder: **全面的文件管理**: 支持文件和文件夹的浏览、重命名、复制、移动、删除和回收站管理。 +- :film_frames: **在线媒体播放**: 直接在浏览器中播放视频、音频和预览图片。 +- :magnet: **强大的离线下载**: 支持添加磁力链接(Magnet)、HTTP 链接和 PikPak 秒传链接进行离线下载。 +- :arrow_down: **灵活的下载方式**: 支持文件直接下载,也可以推送到您自己的 Aria2 服务器进行下载。 +- :link: **分享与协作**: 支持创建文件分享链接,方便与他人共享资源。 +- :busts_in_silhouette: **完整的账户系统**: 支持邮箱/手机号登录、注册以及邀请码系统。 +- :gear: **高度自定义设置**: + - **Aria2 配置**: 对接您自己的 Aria2 服务,实现高效下载。 + - **反向代理设置**: 自定义 API 代理,解决跨域问题。 + - **自定义菜单**: 根据您的需求,为文件操作添加自定义的快捷方式(如调用外部播放器、发送到其他工具等)。 + - **Telegram 绑定**: 关联您的 Telegram 账号。 + +## 部署指南 + +您可以将此项目部署在任何静态网站托管平台,如 GitHub Pages, Vercel, Netlify, 或您自己的服务器。 + +部署过程主要分为 **前端项目打包** 和 **配置反向代理** 两个核心步骤。 + +### 步骤 1: 准备工作 + +确保您的本地环境已安装以下软件: + +- [Node.js](https://nodejs.org/) (建议使用 v16 或更高版本) +- [Git](https://git-scm.com/) + +### 步骤 2: 获取并打包项目 + +```bash +# 1. 克隆仓库到本地 +git clone [https://github.com/victorqr/pikpak.git](https://github.com/victorqr/pikpak.git) + +# 2. 进入项目目录 +cd pikpak + +# 3. 安装项目依赖 +npm install + +# 4. 打包构建项目 +npm run build +```` + +构建成功后,所有用于部署的静态文件都会生成在 `dist` 文件夹中。 + +### 步骤 3: 配置反向代理 (关键) + +由于浏览器跨域安全策略的限制,前端页面无法直接请求 PikPak 的 API。因此,我们需要一个反向代理来中转请求。这里我们推荐使用免费的 **Cloudflare Worker**。 + +1. **登录 Cloudflare**: 打开 [Cloudflare Dashboard](https://dash.cloudflare.com/)。 +2. **进入 Workers 和 Pages**: 在左侧菜单中选择 `Workers & Pages`。 +3. **创建 Worker**: 点击 `Create application` -\> `Create Worker`。 +4. **配置 Worker**: + - 为你的 Worker 设置一个自定义的子域名(例如 `my-pikpak-proxy`)。 + - 点击 `Deploy`。 +5. **编辑代码**: + - 部署成功后,点击 `Edit code`。 + - **清空** 编辑器中所有默认代码。 + - 将下面的 **完整 Worker 脚本** 复制并粘贴到编辑器中。 + - 点击 `Save and Deploy`。 + +现在,你的反向代理已经在 `https://<你的Worker名>.<你的子域名>.workers.dev` 上运行了。 + +\ +\\点击展开/折叠 Cloudflare Worker 脚本\\ + +```javascript +/** + * 预检请求的配置 + */ +const PREFLIGHT_INIT = { + status: 204, + headers: new Headers({ + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS", + "access-control-allow-headers": "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token, Notion-Version", + }), +}; + +/** + * 黑名单:包含不希望代理的URL关键字或扩展名 + */ +const BLOCKED_KEYWORDS = new Set([ + ".m3u8", + ".ts", + ".acc", + ".m4s", + "photocall.tv", + "googlevideo.com", + "xunleix.com", +]); + +/** + * 白名单:允许代理的域名列表,支持通配符 + */ +const ALLOWED_DOMAINS = new Set([ + // PikPak 及其所有子域名 + "*.mypikpak.com", + "mypikpak.com", + + // 其他依赖服务 + "api.notion.com", + "invite.z7.workers.dev", + "pikpak-depot.z10.workers.dev" +]); + +/** + * 检查URL是否在黑名单中 + * @param {string} url 要检查的URL + * @returns {boolean} 如果在黑名单中则返回 true + */ +function isUrlBlocked(url) { + const lowercasedUrl = url.toLowerCase(); + for (const keyword of BLOCKED_KEYWORDS) { + if (lowercasedUrl.includes(keyword)) { + return true; + } + } + return false; +} + +/** + * 检查域名是否在白名单中(支持通配符) + * @param {string} hostname 要检查的域名 + * @returns {boolean} 如果在白名单中则返回 true + */ +function isDomainAllowed(hostname) { + if (ALLOWED_DOMAINS.size === 0) { + return true; // 如果白名单为空,则允许所有域名 + } + for (const rule of ALLOWED_DOMAINS) { + if (rule.startsWith("*.")) { + if (hostname.endsWith(rule.slice(1)) || hostname === rule.slice(2)) { + return true; + } + } else if (hostname === rule) { + return true; + } + } + return false; +} + +/** + * 处理请求 + * @param {FetchEvent} event + */ +async function handleRequest(event) { + const { request } = event; + + if (request.method === "OPTIONS") { + return new Response(null, PREFLIGHT_INIT); + } + + const reqHeaders = new Headers(request.headers); + const outHeaders = new Headers({ + "Access-Control-Allow-Origin": reqHeaders.get("Origin") || "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", + "Access-Control-Allow-Headers": reqHeaders.get("Access-Control-Allow-Headers") || "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token, Notion-Version", + }); + + try { + const urlString = decodeURIComponent(request.url.split('/').slice(3).join('/')); + + if (urlString.length < 3 || urlString.indexOf('.') === -1 || ["favicon.ico", "robots.txt"].includes(urlString)) { + return Response.redirect('[https://baidu.com](https://baidu.com)', 301); + } + + if (isUrlBlocked(urlString)) { + return Response.redirect('[https://baidu.com](https://baidu.com)', 301); + } + + const url = new URL(urlString.startsWith('http') ? urlString : 'http://' + urlString); + + if (!isDomainAllowed(url.hostname)) { + return new Response(`Domain ${url.hostname} is not allowed.`, { status: 403 }); + } + + const newReqHeaders = new Headers(reqHeaders); + newReqHeaders.delete('content-length'); + + const fp = { + method: request.method, + headers: newReqHeaders, + body: ["POST", "PUT", "PATCH", "DELETE"].includes(request.method) ? request.body : null, + }; + + const fr = await fetch(new Request(url, fp)); + + for (const [key, value] of fr.headers.entries()) { + outHeaders.set(key, value); + } + + const outCt = fr.headers.get('content-type') || ''; + let outBody = fr.body; + + if (outCt.includes('text/html')) { + try { + const rewriter = new HTMLRewriter().on("head", { + element(element) { + element.prepend(``, { html: true }); + }, + }); + return rewriter.transform(fr); + } catch (e) { + // 如果HTMLRewriter失败,则直接返回原始响应 + } + } + + return new Response(outBody, { + status: fr.status, + statusText: fr.statusText, + headers: outHeaders, + }); + + } catch (err) { + return new Response(JSON.stringify({ + code: -1, + msg: err.stack || String(err), + }), { + status: 500, + headers: { "content-type": "application/json" } + }); + } +} + +addEventListener('fetch', event => { + event.passThroughOnException(); + event.respondWith(handleRequest(event)); +}); +``` + +\ + +### 步骤 4: 修改前端配置并重新打包 + +1. 打开项目代码中的 `src/config/index.ts` 文件。 + +2. 将其中的 `proxy` 数组修改为您刚刚创建的 Worker 地址。 + + ```typescript + export const proxy = [ + 'https://<你的Worker名>.<你的子域名>.workers.dev' + // 你也可以添加多个备用地址 + ] + ``` + +3. **保存文件后,重新执行打包命令**: + + ```bash + npm run build + ``` + +### 步骤 5: 部署 + +现在,将 `dist` 文件夹中的所有内容上传到您的静态网站托管平台即可。 ## 致谢 + 本项目 CDN 加速及安全防护由 [Tencent EdgeOne](https://edgeone.ai/zh?from=github) 赞助 -![image](https://github.com/user-attachments/assets/9b486747-77c5-4d28-9ce2-83591de5ee0f) + From b964e23cf7ac58934ecfb01b34a92c36b6deeee5 Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:58:46 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 237 +++++++----------------------------------------------- 1 file changed, 27 insertions(+), 210 deletions(-) diff --git a/README.md b/README.md index f5c94d0c..102eaff1 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,42 @@ # PikPak 个人网页版 -![PikPak Web UI](https://socialify.git.ci/tjsky/pikpak/image?forks=1&language=1&name=1&owner=1&pattern=Signal&stargazers=1&theme=Light) - 一个功能强大的 PikPak 网盘第三方 Web 客户端。它提供了接近原生体验的文件管理和浏览功能,并支持自托管部署。 ## 相关链接 -- **在线演示:** [Demo](https://tjsky.github.io/pikpak/) -- **PikPak 官网:** [mypikpak.com](https://mypikpak.com) -- **官方 Web 版:** [drive.mypikpak.com](https://drive.mypikpak.com/) -- **官方 Telegram 群:** [t.me/pikpak_userservice](https://t.me/pikpak_userservice) + - **在线演示:** [https://tjsky.github.io/pikpak/](https://tjsky.github.io/pikpak/) + - **PikPak 官网:** [https://mypikpak.com](https://mypikpak.com) + - **官方 Web 版:** [https://drive.mypikpak.com/](https://drive.mypikpak.com/) + - **官方 Telegram 群:** [https://t.me/pikpak\_userservice](https://t.me/pikpak_userservice) ## 功能特性 -- :file_folder: **全面的文件管理**: 支持文件和文件夹的浏览、重命名、复制、移动、删除和回收站管理。 -- :film_frames: **在线媒体播放**: 直接在浏览器中播放视频、音频和预览图片。 -- :magnet: **强大的离线下载**: 支持添加磁力链接(Magnet)、HTTP 链接和 PikPak 秒传链接进行离线下载。 -- :arrow_down: **灵活的下载方式**: 支持文件直接下载,也可以推送到您自己的 Aria2 服务器进行下载。 -- :link: **分享与协作**: 支持创建文件分享链接,方便与他人共享资源。 -- :busts_in_silhouette: **完整的账户系统**: 支持邮箱/手机号登录、注册以及邀请码系统。 -- :gear: **高度自定义设置**: - - **Aria2 配置**: 对接您自己的 Aria2 服务,实现高效下载。 - - **反向代理设置**: 自定义 API 代理,解决跨域问题。 - - **自定义菜单**: 根据您的需求,为文件操作添加自定义的快捷方式(如调用外部播放器、发送到其他工具等)。 - - **Telegram 绑定**: 关联您的 Telegram 账号。 + - **全面的文件管理**: 支持文件和文件夹的浏览、重命名、复制、移动、删除和回收站管理。 + - **在线媒体播放**: 直接在浏览器中播放视频、音频和预览图片。 + - **强大的离线下载**: 支持添加磁力链接(Magnet)、HTTP 链接和 PikPak 秒传链接进行离线下载。 + - **灵活的下载方式**: 支持文件直接下载,也可以推送到您自己的 Aria2 服务器进行下载。 + - **分享与协作**: 支持创建文件分享链接,方便与他人共享资源。 + - **完整的账户系统**: 支持邮箱/手机号登录、注册以及邀请码系统。 + - **高度自定义设置**: + - **Aria2 配置**: 对接您自己的 Aria2 服务,实现高效下载。 + - **反向代理设置**: 自定义 API 代理,解决跨域问题。 + - **自定义菜单**: 根据您的需求,为文件操作添加自定义的快捷方式。 + - **Telegram 绑定**: 关联您的 Telegram 账号。 ## 部署指南 -您可以将此项目部署在任何静态网站托管平台,如 GitHub Pages, Vercel, Netlify, 或您自己的服务器。 - -部署过程主要分为 **前端项目打包** 和 **配置反向代理** 两个核心步骤。 +您可以将此项目部署在任何静态网站托管平台,如 GitHub Pages, Vercel, Netlify, 或您自己的服务器。部署过程主要分为 **前端项目打包** 和 **配置反向代理** 两个核心步骤。 ### 步骤 1: 准备工作 确保您的本地环境已安装以下软件: -- [Node.js](https://nodejs.org/) (建议使用 v16 或更高版本) -- [Git](https://git-scm.com/) + - Node.js (建议使用 v16 或更高版本) + - Git ### 步骤 2: 获取并打包项目 -```bash +``` # 1. 克隆仓库到本地 git clone [https://github.com/victorqr/pikpak.git](https://github.com/victorqr/pikpak.git) @@ -52,214 +48,36 @@ npm install # 4. 打包构建项目 npm run build -```` +``` 构建成功后,所有用于部署的静态文件都会生成在 `dist` 文件夹中。 ### 步骤 3: 配置反向代理 (关键) -由于浏览器跨域安全策略的限制,前端页面无法直接请求 PikPak 的 API。因此,我们需要一个反向代理来中转请求。这里我们推荐使用免费的 **Cloudflare Worker**。 +由于浏览器跨域安全策略的限制,我们需要一个反向代理来中转 API 请求。这里我们推荐使用免费的 **Cloudflare Worker**。 -1. **登录 Cloudflare**: 打开 [Cloudflare Dashboard](https://dash.cloudflare.com/)。 +1. **登录 Cloudflare**: 打开 Cloudflare Dashboard。 2. **进入 Workers 和 Pages**: 在左侧菜单中选择 `Workers & Pages`。 -3. **创建 Worker**: 点击 `Create application` -\> `Create Worker`。 -4. **配置 Worker**: - - 为你的 Worker 设置一个自定义的子域名(例如 `my-pikpak-proxy`)。 - - 点击 `Deploy`。 -5. **编辑代码**: - - 部署成功后,点击 `Edit code`。 - - **清空** 编辑器中所有默认代码。 - - 将下面的 **完整 Worker 脚本** 复制并粘贴到编辑器中。 - - 点击 `Save and Deploy`。 +3. **创建 Worker**: 点击 `Create application` -\> `Create Worker`,然后为您的 Worker 命名并部署。 +4. **编辑代码**: 部署成功后,点击 `Edit code`。**清空** 编辑器中所有默认代码,然后将仓库中 `cf-worker/index.js` 文件的全部内容复制并粘贴到编辑器中,最后点击 `Save and Deploy`。 现在,你的反向代理已经在 `https://<你的Worker名>.<你的子域名>.workers.dev` 上运行了。 -\ -\\点击展开/折叠 Cloudflare Worker 脚本\\ - -```javascript -/** - * 预检请求的配置 - */ -const PREFLIGHT_INIT = { - status: 204, - headers: new Headers({ - "access-control-allow-origin": "*", - "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS", - "access-control-allow-headers": "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token, Notion-Version", - }), -}; - -/** - * 黑名单:包含不希望代理的URL关键字或扩展名 - */ -const BLOCKED_KEYWORDS = new Set([ - ".m3u8", - ".ts", - ".acc", - ".m4s", - "photocall.tv", - "googlevideo.com", - "xunleix.com", -]); - -/** - * 白名单:允许代理的域名列表,支持通配符 - */ -const ALLOWED_DOMAINS = new Set([ - // PikPak 及其所有子域名 - "*.mypikpak.com", - "mypikpak.com", - - // 其他依赖服务 - "api.notion.com", - "invite.z7.workers.dev", - "pikpak-depot.z10.workers.dev" -]); - -/** - * 检查URL是否在黑名单中 - * @param {string} url 要检查的URL - * @returns {boolean} 如果在黑名单中则返回 true - */ -function isUrlBlocked(url) { - const lowercasedUrl = url.toLowerCase(); - for (const keyword of BLOCKED_KEYWORDS) { - if (lowercasedUrl.includes(keyword)) { - return true; - } - } - return false; -} - -/** - * 检查域名是否在白名单中(支持通配符) - * @param {string} hostname 要检查的域名 - * @returns {boolean} 如果在白名单中则返回 true - */ -function isDomainAllowed(hostname) { - if (ALLOWED_DOMAINS.size === 0) { - return true; // 如果白名单为空,则允许所有域名 - } - for (const rule of ALLOWED_DOMAINS) { - if (rule.startsWith("*.")) { - if (hostname.endsWith(rule.slice(1)) || hostname === rule.slice(2)) { - return true; - } - } else if (hostname === rule) { - return true; - } - } - return false; -} - -/** - * 处理请求 - * @param {FetchEvent} event - */ -async function handleRequest(event) { - const { request } = event; - - if (request.method === "OPTIONS") { - return new Response(null, PREFLIGHT_INIT); - } - - const reqHeaders = new Headers(request.headers); - const outHeaders = new Headers({ - "Access-Control-Allow-Origin": reqHeaders.get("Origin") || "*", - "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", - "Access-Control-Allow-Headers": reqHeaders.get("Access-Control-Allow-Headers") || "Accept, Authorization, Cache-Control, Content-Type, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Requested-With, Token, x-access-token, Notion-Version", - }); - - try { - const urlString = decodeURIComponent(request.url.split('/').slice(3).join('/')); - - if (urlString.length < 3 || urlString.indexOf('.') === -1 || ["favicon.ico", "robots.txt"].includes(urlString)) { - return Response.redirect('[https://baidu.com](https://baidu.com)', 301); - } - - if (isUrlBlocked(urlString)) { - return Response.redirect('[https://baidu.com](https://baidu.com)', 301); - } - - const url = new URL(urlString.startsWith('http') ? urlString : 'http://' + urlString); - - if (!isDomainAllowed(url.hostname)) { - return new Response(`Domain ${url.hostname} is not allowed.`, { status: 403 }); - } - - const newReqHeaders = new Headers(reqHeaders); - newReqHeaders.delete('content-length'); - - const fp = { - method: request.method, - headers: newReqHeaders, - body: ["POST", "PUT", "PATCH", "DELETE"].includes(request.method) ? request.body : null, - }; - - const fr = await fetch(new Request(url, fp)); - - for (const [key, value] of fr.headers.entries()) { - outHeaders.set(key, value); - } - - const outCt = fr.headers.get('content-type') || ''; - let outBody = fr.body; - - if (outCt.includes('text/html')) { - try { - const rewriter = new HTMLRewriter().on("head", { - element(element) { - element.prepend(``, { html: true }); - }, - }); - return rewriter.transform(fr); - } catch (e) { - // 如果HTMLRewriter失败,则直接返回原始响应 - } - } - - return new Response(outBody, { - status: fr.status, - statusText: fr.statusText, - headers: outHeaders, - }); - - } catch (err) { - return new Response(JSON.stringify({ - code: -1, - msg: err.stack || String(err), - }), { - status: 500, - headers: { "content-type": "application/json" } - }); - } -} - -addEventListener('fetch', event => { - event.passThroughOnException(); - event.respondWith(handleRequest(event)); -}); -``` - -\ - ### 步骤 4: 修改前端配置并重新打包 1. 打开项目代码中的 `src/config/index.ts` 文件。 2. 将其中的 `proxy` 数组修改为您刚刚创建的 Worker 地址。 - ```typescript + ``` export const proxy = [ 'https://<你的Worker名>.<你的子域名>.workers.dev' - // 你也可以添加多个备用地址 ] ``` 3. **保存文件后,重新执行打包命令**: - ```bash + ``` npm run build ``` @@ -269,5 +87,4 @@ addEventListener('fetch', event => { ## 致谢 -本项目 CDN 加速及安全防护由 [Tencent EdgeOne](https://edgeone.ai/zh?from=github) 赞助 - +本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助 From a608f73716079a286c2674f92b64b9beee1b49b5 Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:01:52 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 65 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 102eaff1..cc23078f 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,41 @@ # PikPak 个人网页版 -一个功能强大的 PikPak 网盘第三方 Web 客户端。它提供了接近原生体验的文件管理和浏览功能,并支持自托管部署。 +![pikpak](https://socialify.git.ci/tjsky/pikpak/image?forks=1&language=1&name=1&owner=1&pattern=Signal&stargazers=1&theme=Light) -## 相关链接 +## 官方地址 - - **在线演示:** [https://tjsky.github.io/pikpak/](https://tjsky.github.io/pikpak/) - - **PikPak 官网:** [https://mypikpak.com](https://mypikpak.com) - - **官方 Web 版:** [https://drive.mypikpak.com/](https://drive.mypikpak.com/) - - **官方 Telegram 群:** [https://t.me/pikpak\_userservice](https://t.me/pikpak_userservice) +* [PikPak官网](https://mypikpak.com) +* [PikPak官方网页版](https://drive.mypikpak.com/) +* [PikPak官方讨论群组](https://t.me/pikpak_userservice) + +## Demo + +* [Demo](https://tjsky.github.io/pikpak/) + +## 安装教程 + +* [教程](https://www.tjsky.net/?p=201) + +## 对原版代码的修改 + +- [x] 增加对IDM下载功能的引导 +- [x] 修正反代代码,支持下载反代(感谢小樱提供修正代码) +- [x] 增加多个反代域名 +- [x] aira2多线程提速 ## 功能特性 - - **全面的文件管理**: 支持文件和文件夹的浏览、重命名、复制、移动、删除和回收站管理。 - - **在线媒体播放**: 直接在浏览器中播放视频、音频和预览图片。 - - **强大的离线下载**: 支持添加磁力链接(Magnet)、HTTP 链接和 PikPak 秒传链接进行离线下载。 - - **灵活的下载方式**: 支持文件直接下载,也可以推送到您自己的 Aria2 服务器进行下载。 - - **分享与协作**: 支持创建文件分享链接,方便与他人共享资源。 - - **完整的账户系统**: 支持邮箱/手机号登录、注册以及邀请码系统。 - - **高度自定义设置**: - - **Aria2 配置**: 对接您自己的 Aria2 服务,实现高效下载。 - - **反向代理设置**: 自定义 API 代理,解决跨域问题。 - - **自定义菜单**: 根据您的需求,为文件操作添加自定义的快捷方式。 - - **Telegram 绑定**: 关联您的 Telegram 账号。 +- 📁 **全面的文件管理**: 支持文件和文件夹的浏览、重命名、复制、移动、删除和回收站管理。 +- 🎥 **在线媒体播放**: 直接在浏览器中播放视频、音频和预览图片。 +- 🧲 **强大的离线下载**: 支持添加磁力链接(Magnet)、HTTP 链接和 PikPak 秒传链接进行离线下载。 +- ⬇️ **灵活的下载方式**: 支持文件直接下载,也可以推送到您自己的 Aria2 服务器进行下载。 +- 🔗 **分享与协作**: 支持创建文件分享链接,方便与他人共享资源。 +- 👥 **完整的账户系统**: 支持邮箱/手机号登录、注册以及邀请码系统。 +- ⚙️ **高度自定义设置**: + - **Aria2 配置**: 对接您自己的 Aria2 服务,实现高效下载。 + - **反向代理设置**: 自定义 API 代理,解决跨域问题。 + - **自定义菜单**: 根据您的需求,为文件操作添加自定义的快捷方式。 + - **Telegram 绑定**: 关联您的 Telegram 账号。 ## 部署指南 @@ -30,13 +44,12 @@ ### 步骤 1: 准备工作 确保您的本地环境已安装以下软件: - - - Node.js (建议使用 v16 或更高版本) - - Git +- [Node.js](https://nodejs.org/) (建议使用 v16 或更高版本) +- [Git](https://git-scm.com/) ### 步骤 2: 获取并打包项目 -``` +```bash # 1. 克隆仓库到本地 git clone [https://github.com/victorqr/pikpak.git](https://github.com/victorqr/pikpak.git) @@ -48,7 +61,7 @@ npm install # 4. 打包构建项目 npm run build -``` +```` 构建成功后,所有用于部署的静态文件都会生成在 `dist` 文件夹中。 @@ -56,7 +69,7 @@ npm run build 由于浏览器跨域安全策略的限制,我们需要一个反向代理来中转 API 请求。这里我们推荐使用免费的 **Cloudflare Worker**。 -1. **登录 Cloudflare**: 打开 Cloudflare Dashboard。 +1. **登录 Cloudflare**: 打开 [Cloudflare Dashboard](https://dash.cloudflare.com/)。 2. **进入 Workers 和 Pages**: 在左侧菜单中选择 `Workers & Pages`。 3. **创建 Worker**: 点击 `Create application` -\> `Create Worker`,然后为您的 Worker 命名并部署。 4. **编辑代码**: 部署成功后,点击 `Edit code`。**清空** 编辑器中所有默认代码,然后将仓库中 `cf-worker/index.js` 文件的全部内容复制并粘贴到编辑器中,最后点击 `Save and Deploy`。 @@ -69,7 +82,7 @@ npm run build 2. 将其中的 `proxy` 数组修改为您刚刚创建的 Worker 地址。 - ``` + ```typescript export const proxy = [ 'https://<你的Worker名>.<你的子域名>.workers.dev' ] @@ -77,7 +90,7 @@ npm run build 3. **保存文件后,重新执行打包命令**: - ``` + ```bash npm run build ``` @@ -87,4 +100,4 @@ npm run build ## 致谢 -本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助 +本项目 CDN 加速及安全防护由 [Tencent EdgeOne](https://edgeone.ai/zh?from=github) 赞助 From 50e333bbe11ede444e7fe1e9daf5f9b11aaf50d3 Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:14:39 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E7=BD=91=E9=A1=B5=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E7=BE=8E=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 1 + src/moe.css | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/moe.css diff --git a/src/main.ts b/src/main.ts index 2767f5a2..2b257a68 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import router from './router' import http from './utils/axios' import elementResizeDetectorMaker from 'element-resize-detector' import cnzzAnalytics from 'vue-cnzz-analytics' +import './moe.css' const app = createApp(App) app.directive('resize', { diff --git a/src/moe.css b/src/moe.css new file mode 100644 index 00000000..3114c557 --- /dev/null +++ b/src/moe.css @@ -0,0 +1,108 @@ +.n-layout { + background-image: url(https://cdn.seovx.com/d/?mom=302); + filter: brightness(100%); + backdrop-filter: blur(20px); + background-attachment: fixed; + background-size: cover; + background-repeat: no-repeat; +} + +/*页面背景*/ +.n-layout-sider { + background-color: rgba(255, 255, 255, 0.8) !important; +} + +/*侧边栏背景色*/ +.n-icon { + color: rgb(83, 114, 248) +} + +/*左侧图标颜色*/ +.n-data-table { + --merged-th-color: rgba(241, 241, 241, 0.8) !important; + --merged-td-color: rgba(241, 241, 241, 0.4) !important; + --merged-td-color-hover: rgba(158, 158, 158, 0.6) !important; +} + +/*列表页背景色*/ +.user-info-avatar { + content: url(https://s2.loli.net/2021/12/15/fKiaZgWSvnd5lQN.jpg); +} + +/*自定义头像*/ +.content { + background: rgba(255, 255, 255, 0.6); +} + +.status-bar { + background: rgba(255, 255, 255, 0.8); +} + +.task-list { + background: rgba(255, 255, 255, 0.8); +} + +.status-bar .status-bar-wrapper { + background-color: #00000000; +} + +/*右下角bar透明*/ +.n-text { + color: rgb(83, 114, 248); +} + +/*功能项文字颜色*/ +.n-progress .n-progress-graph .n-progress-graph-line .n-progress-graph-line-rail { + background-color: rgba(244, 237, 219, 0.80); +} + +/*容量进度条未用部分颜色*/ +.n-collapse { + background-color: rgba(255, 255, 255, 0.8); + border-radius: 15px; + padding: 10px; +} + +/*设置页背景颜色及尺寸*/ +.n-row { + background-color: rgba(255, 255, 255, 0.5); + border-radius: 1em; +} + +/*邀请页表头透明,列表圆角*/ +.n-statistic .n-statistic__label { + color: #3f51b5; +} + +/*邀请页表头文字颜色*/ +.n-statistic { + padding-left: 10px; + padding-top: 10px; +} + +.n-scrollbar { + border-radius: 1em; +} + +/*列表圆角*/ +.header { + background-color: rgba(255, 255, 255, 0.9); + border-radius: 1em; +} + +.n-breadcrumb { + padding-left: 20px; +} + +/*列表页表头圆角*/ +.header .action { + margin-right: 10px; +} + +.login-page .login-box { + background: rgba(255, 255, 255, 0.8); +} + +.n-card { + background-color: rgba(255, 255, 255, 0.8) !important; +} \ No newline at end of file From cfdd8128f408e798ec0a353714e38443daf6a1cb Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:22:59 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E7=BE=8E=E5=8C=96=E7=BD=91=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 2 +- src/modern-theme.css | 141 +++++++++++++++++++++++++++++++++++++++++++ src/moe.css | 108 --------------------------------- 3 files changed, 142 insertions(+), 109 deletions(-) create mode 100644 src/modern-theme.css delete mode 100644 src/moe.css diff --git a/src/main.ts b/src/main.ts index 2b257a68..a746de57 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import router from './router' import http from './utils/axios' import elementResizeDetectorMaker from 'element-resize-detector' import cnzzAnalytics from 'vue-cnzz-analytics' -import './moe.css' +import './modern-theme.css' const app = createApp(App) app.directive('resize', { diff --git a/src/modern-theme.css b/src/modern-theme.css new file mode 100644 index 00000000..c54b2fce --- /dev/null +++ b/src/modern-theme.css @@ -0,0 +1,141 @@ +/* --- Global Theme: Frosted Glass & Gradient --- */ + +/* 1. Advanced Gradient Background */ +.n-layout { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background-attachment: fixed; +} + +/* 2. Apply Frosted Glass to Components */ + +/* Sidebar */ +.n-layout-sider { + background: rgba(255, 255, 255, 0.1) !important; + backdrop-filter: blur(15px) !important; + -webkit-backdrop-filter: blur(15px) !important; + border-right: 1px solid rgba(255, 255, 255, 0.18) !important; +} + +/* Main Content Area Header */ +.header { + margin: 20px 20px 0 20px !important; + padding-left: 20px; + padding-right: 20px; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 12px; +} + +/* Data Table */ +.n-data-table { + background: transparent !important; + --merged-th-color: rgba(255, 255, 255, 0.2) !important; + --merged-td-color: rgba(255, 255, 255, 0.05) !important; + --merged-td-color-hover: rgba(255, 255, 255, 0.15) !important; + --merged-border-color: rgba(255, 255, 255, 0.1) !important; +} +.n-data-table .n-data-table-td, .n-data-table .n-data-table-th { + color: #f0f0f0; +} +.n-data-table .n-data-table-th { + color: #ffffff; + font-weight: 600; +} +.file-info .title, .file-info .n-ellipsis { + color: #fff !important; +} +.n-data-table-td.size, .n-data-table-td.modified_time, .n-data-table-td .n-time { + color: rgba(230, 230, 230, 0.7) !important; +} +.n-data-table .n-text--primary-type { + color: #a7c5ff !important; +} + +/* Cards, Modals, Popups */ +.n-card, .n-modal, .n-popover, .n-dialog { + background: rgba(30, 30, 50, 0.6) !important; + backdrop-filter: blur(15px) !important; + -webkit-backdrop-filter: blur(15px) !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + border-radius: 16px !important; + color: #fff !important; +} +.n-card-header, .n-dialog__title, .n-modal-header__title { + color: #fff !important; +} +.n-input, .n-input__textarea-el { + background-color: rgba(0,0,0,0.2) !important; + color: #fff !important; +} +.n-input .n-input__input-el, .n-input .n-input__placeholder { + color: #fff !important; +} + +/* Login Page */ +.login-page { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} +.login-page .login-box { + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.18); + box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1); +} +.login-page .logo-box__text { + text-shadow: 0 2px 10px rgba(0,0,0,0.2); +} + +/* Sidebar Menu & Icons */ +.n-menu { + background: transparent !important; +} +.n-menu .n-menu-item-content__icon { + color: #e0e0e0 !important; +} +.n-menu .n-menu-item-content-header { + color: #e0e0e0 !important; +} +.n-menu .n-menu-item--selected .n-menu-item-content-header, +.n-menu .n-menu-item--selected .n-menu-item-content__icon { + color: #ffffff !important; + font-weight: bold; +} +.n-layout-sider-scroll-container { + color: #f0f0f0; +} +.n-layout-sider-scroll-container .n-text--primary-type { + color: #d1c4e9 !important; +} +.sider-bottom { + background-color: transparent !important; +} +.sider-bottom.vip { + background-color: rgba(244, 237, 219, 0.1) !important; +} + +/* Progress Bar */ +.content-bottom { + border-top: 1px solid rgba(255, 255, 255, 0.18); + padding-top: 10px; +} + +/* General Text Color & Breadcrumb */ +body { + color: #f0f0f0; +} +.n-breadcrumb { + color: #fff !important; +} +.n-breadcrumb-item a { + color: #e0e0e0 !important; + text-decoration: none; +} +.n-breadcrumb-item a:hover { + color: #fff !important; +} +.n-breadcrumb .n-breadcrumb-item__separator { + color: rgba(255, 255, 255, 0.5) !important; +} diff --git a/src/moe.css b/src/moe.css deleted file mode 100644 index 3114c557..00000000 --- a/src/moe.css +++ /dev/null @@ -1,108 +0,0 @@ -.n-layout { - background-image: url(https://cdn.seovx.com/d/?mom=302); - filter: brightness(100%); - backdrop-filter: blur(20px); - background-attachment: fixed; - background-size: cover; - background-repeat: no-repeat; -} - -/*页面背景*/ -.n-layout-sider { - background-color: rgba(255, 255, 255, 0.8) !important; -} - -/*侧边栏背景色*/ -.n-icon { - color: rgb(83, 114, 248) -} - -/*左侧图标颜色*/ -.n-data-table { - --merged-th-color: rgba(241, 241, 241, 0.8) !important; - --merged-td-color: rgba(241, 241, 241, 0.4) !important; - --merged-td-color-hover: rgba(158, 158, 158, 0.6) !important; -} - -/*列表页背景色*/ -.user-info-avatar { - content: url(https://s2.loli.net/2021/12/15/fKiaZgWSvnd5lQN.jpg); -} - -/*自定义头像*/ -.content { - background: rgba(255, 255, 255, 0.6); -} - -.status-bar { - background: rgba(255, 255, 255, 0.8); -} - -.task-list { - background: rgba(255, 255, 255, 0.8); -} - -.status-bar .status-bar-wrapper { - background-color: #00000000; -} - -/*右下角bar透明*/ -.n-text { - color: rgb(83, 114, 248); -} - -/*功能项文字颜色*/ -.n-progress .n-progress-graph .n-progress-graph-line .n-progress-graph-line-rail { - background-color: rgba(244, 237, 219, 0.80); -} - -/*容量进度条未用部分颜色*/ -.n-collapse { - background-color: rgba(255, 255, 255, 0.8); - border-radius: 15px; - padding: 10px; -} - -/*设置页背景颜色及尺寸*/ -.n-row { - background-color: rgba(255, 255, 255, 0.5); - border-radius: 1em; -} - -/*邀请页表头透明,列表圆角*/ -.n-statistic .n-statistic__label { - color: #3f51b5; -} - -/*邀请页表头文字颜色*/ -.n-statistic { - padding-left: 10px; - padding-top: 10px; -} - -.n-scrollbar { - border-radius: 1em; -} - -/*列表圆角*/ -.header { - background-color: rgba(255, 255, 255, 0.9); - border-radius: 1em; -} - -.n-breadcrumb { - padding-left: 20px; -} - -/*列表页表头圆角*/ -.header .action { - margin-right: 10px; -} - -.login-page .login-box { - background: rgba(255, 255, 255, 0.8); -} - -.n-card { - background-color: rgba(255, 255, 255, 0.8) !important; -} \ No newline at end of file From ea4aad4fd8a83cd73a7b2cdc885d08ba9ac24271 Mon Sep 17 00:00:00 2001 From: VictorQR <42757940+VictorQR@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:56:23 +0800 Subject: [PATCH 11/11] =?UTF-8?q?=E7=BE=8E=E5=8C=96=E7=BD=91=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frosted-glass.css | 81 ++++++++++++++++++++++++ src/main.ts | 2 +- src/modern-theme.css | 141 ------------------------------------------ 3 files changed, 82 insertions(+), 142 deletions(-) create mode 100644 src/frosted-glass.css delete mode 100644 src/modern-theme.css diff --git a/src/frosted-glass.css b/src/frosted-glass.css new file mode 100644 index 00000000..12010f5d --- /dev/null +++ b/src/frosted-glass.css @@ -0,0 +1,81 @@ +/* --- Frosted Glass Theme for PikPak Web --- */ + +/* 1. Frosted Glass Components */ + +/* Sidebar */ +.n-layout-sider { + background-color: rgba(245, 245, 245, 0.6) !important; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-right: 1px solid rgba(255, 255, 255, 0.18) !important; +} + +/* Main Content Area Header */ +.header { + background-color: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 12px; +} + +/* Data Table */ +.n-data-table { + --merged-th-color: rgba(240, 240, 245, 0.7) !important; + --merged-td-color: rgba(255, 255, 255, 0.4) !important; + --merged-td-color-hover: rgba(240, 240, 245, 0.6) !important; + --merged-border-color: transparent !important; +} + +/* Cards, Modals, Popups, Dialogs */ +.n-card, .n-modal, .n-popover, .n-dialog, .n-collapse { + background-color: rgba(255, 255, 255, 0.75) !important; + backdrop-filter: blur(15px) !important; + -webkit-backdrop-filter: blur(15px) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + border-radius: 16px !important; +} + +/* Login Page Box */ +.login-page .login-box { + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.18); + box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1); +} + +/* Bottom Toolbar */ +.toolbar-wrapper { + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(15px); + -webkit-backdrop-filter: blur(15px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 16px; +} +.toolbar-item { + color: #333; +} + +/* 2. Unified Rounded Corners */ +.n-scrollbar, .n-row { + border-radius: 12px; +} +.n-row { + background-color: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(10px); +} + +/* 3. General Adjustments */ +.n-layout, .login-page { + background: initial !important; +} +.sider-bottom { + background-color: transparent !important; +} +.sider-bottom.vip { + background-color: rgba(244, 237, 219, 0.4) !important; +} +.n-menu { + background: transparent !important; +} diff --git a/src/main.ts b/src/main.ts index a746de57..8c8c14e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import router from './router' import http from './utils/axios' import elementResizeDetectorMaker from 'element-resize-detector' import cnzzAnalytics from 'vue-cnzz-analytics' -import './modern-theme.css' +import './frosted-glass.css' const app = createApp(App) app.directive('resize', { diff --git a/src/modern-theme.css b/src/modern-theme.css deleted file mode 100644 index c54b2fce..00000000 --- a/src/modern-theme.css +++ /dev/null @@ -1,141 +0,0 @@ -/* --- Global Theme: Frosted Glass & Gradient --- */ - -/* 1. Advanced Gradient Background */ -.n-layout { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - background-attachment: fixed; -} - -/* 2. Apply Frosted Glass to Components */ - -/* Sidebar */ -.n-layout-sider { - background: rgba(255, 255, 255, 0.1) !important; - backdrop-filter: blur(15px) !important; - -webkit-backdrop-filter: blur(15px) !important; - border-right: 1px solid rgba(255, 255, 255, 0.18) !important; -} - -/* Main Content Area Header */ -.header { - margin: 20px 20px 0 20px !important; - padding-left: 20px; - padding-right: 20px; - background: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.18); - border-radius: 12px; -} - -/* Data Table */ -.n-data-table { - background: transparent !important; - --merged-th-color: rgba(255, 255, 255, 0.2) !important; - --merged-td-color: rgba(255, 255, 255, 0.05) !important; - --merged-td-color-hover: rgba(255, 255, 255, 0.15) !important; - --merged-border-color: rgba(255, 255, 255, 0.1) !important; -} -.n-data-table .n-data-table-td, .n-data-table .n-data-table-th { - color: #f0f0f0; -} -.n-data-table .n-data-table-th { - color: #ffffff; - font-weight: 600; -} -.file-info .title, .file-info .n-ellipsis { - color: #fff !important; -} -.n-data-table-td.size, .n-data-table-td.modified_time, .n-data-table-td .n-time { - color: rgba(230, 230, 230, 0.7) !important; -} -.n-data-table .n-text--primary-type { - color: #a7c5ff !important; -} - -/* Cards, Modals, Popups */ -.n-card, .n-modal, .n-popover, .n-dialog { - background: rgba(30, 30, 50, 0.6) !important; - backdrop-filter: blur(15px) !important; - -webkit-backdrop-filter: blur(15px) !important; - border: 1px solid rgba(255, 255, 255, 0.1) !important; - border-radius: 16px !important; - color: #fff !important; -} -.n-card-header, .n-dialog__title, .n-modal-header__title { - color: #fff !important; -} -.n-input, .n-input__textarea-el { - background-color: rgba(0,0,0,0.2) !important; - color: #fff !important; -} -.n-input .n-input__input-el, .n-input .n-input__placeholder { - color: #fff !important; -} - -/* Login Page */ -.login-page { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -} -.login-page .login-box { - background: rgba(255, 255, 255, 0.15); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.18); - box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1); -} -.login-page .logo-box__text { - text-shadow: 0 2px 10px rgba(0,0,0,0.2); -} - -/* Sidebar Menu & Icons */ -.n-menu { - background: transparent !important; -} -.n-menu .n-menu-item-content__icon { - color: #e0e0e0 !important; -} -.n-menu .n-menu-item-content-header { - color: #e0e0e0 !important; -} -.n-menu .n-menu-item--selected .n-menu-item-content-header, -.n-menu .n-menu-item--selected .n-menu-item-content__icon { - color: #ffffff !important; - font-weight: bold; -} -.n-layout-sider-scroll-container { - color: #f0f0f0; -} -.n-layout-sider-scroll-container .n-text--primary-type { - color: #d1c4e9 !important; -} -.sider-bottom { - background-color: transparent !important; -} -.sider-bottom.vip { - background-color: rgba(244, 237, 219, 0.1) !important; -} - -/* Progress Bar */ -.content-bottom { - border-top: 1px solid rgba(255, 255, 255, 0.18); - padding-top: 10px; -} - -/* General Text Color & Breadcrumb */ -body { - color: #f0f0f0; -} -.n-breadcrumb { - color: #fff !important; -} -.n-breadcrumb-item a { - color: #e0e0e0 !important; - text-decoration: none; -} -.n-breadcrumb-item a:hover { - color: #fff !important; -} -.n-breadcrumb .n-breadcrumb-item__separator { - color: rgba(255, 255, 255, 0.5) !important; -}