diff --git a/README.md b/README.md
index 126fa443f..cc23078ff 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,103 @@
# PikPak 个人网页版
+

## 官方地址
- * [PikPak官网](https://mypikpak.com)
- * [PikPak官方网页版](https://drive.mypikpak.com/)
- * [PikPak官方讨论群组](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/)
+
+* [Demo](https://tjsky.github.io/pikpak/)
## 安装教程
- * [教程](https://www.tjsky.net/?p=201)
+
+* [教程](https://www.tjsky.net/?p=201)
## 对原版代码的修改
-- [x] 增加对IDM下载功能的引导
-- [x] 修正反代代码,支持下载反代(感谢小樱提供修正代码)
-- [x] 增加多个反代域名
-- [x] aira2多线程提速
+
+- [x] 增加对IDM下载功能的引导
+- [x] 修正反代代码,支持下载反代(感谢小樱提供修正代码)
+- [x] 增加多个反代域名
+- [x] aira2多线程提速
+
+## 功能特性
+
+- 📁 **全面的文件管理**: 支持文件和文件夹的浏览、重命名、复制、移动、删除和回收站管理。
+- 🎥 **在线媒体播放**: 直接在浏览器中播放视频、音频和预览图片。
+- 🧲 **强大的离线下载**: 支持添加磁力链接(Magnet)、HTTP 链接和 PikPak 秒传链接进行离线下载。
+- ⬇️ **灵活的下载方式**: 支持文件直接下载,也可以推送到您自己的 Aria2 服务器进行下载。
+- 🔗 **分享与协作**: 支持创建文件分享链接,方便与他人共享资源。
+- 👥 **完整的账户系统**: 支持邮箱/手机号登录、注册以及邀请码系统。
+- ⚙️ **高度自定义设置**:
+ - **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: 配置反向代理 (关键)
+
+由于浏览器跨域安全策略的限制,我们需要一个反向代理来中转 API 请求。这里我们推荐使用免费的 **Cloudflare Worker**。
+
+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`。
+
+现在,你的反向代理已经在 `https://<你的Worker名>.<你的子域名>.workers.dev` 上运行了。
+
+### 步骤 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) 赞助
-
diff --git a/cf-worker/index.js b/cf-worker/index.js
index f53dbcd5e..07da1184f 100644
--- a/cf-worker/index.js
+++ b/cf-worker/index.js
@@ -1,135 +1,166 @@
-addEventListener('fetch', event => {
- event.passThroughOnException()
-
- event.respondWith(handleRequest(event))
- })
-
- /**
- * Respond to the request
- * @param {Request} request
- */
- async function handleRequest(event) {
+/**
+ * 预检请求的配置
+ */
+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",
+]);
+
+/**
+ * 白名单:允许代理的域名列表,支持通配符
+ * 例如:
+ * - "mypikpak.com" // 精确匹配 mypikpak.com
+ * - "*.mypikpak.com" // 匹配所有 mypikpak.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;
-
- //请求头部、返回对象
- 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"
- });
-
+
+ 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 {
- //取域名第一个斜杠后的所有信息为代理链接
- 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 || ["favicon.ico", "robots.txt"].includes(urlString)) {
+ 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 (!isDomainAllowed(url.hostname)) {
+ return new Response(`Domain ${url.hostname} is 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);
- }
+ 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));
- outStatus = fr.status;
- outStatusText = fr.statusText;
- outBody = fr.body;
+ 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) {
- outCt = "application/json";
- outBody = JSON.stringify({
+ return new Response(JSON.stringify({
code: -1,
- msg: JSON.stringify(err.stack) || err
+ msg: err.stack || String(err),
+ }), {
+ 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));
+});
\ No newline at end of file
diff --git a/src/frosted-glass.css b/src/frosted-glass.css
new file mode 100644
index 000000000..12010f5d6
--- /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 2767f5a2e..8c8c14e49 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 './frosted-glass.css'
const app = createApp(App)
app.directive('resize', {
diff --git a/src/views/login.vue b/src/views/login.vue
index 8eb71e6d8..43a41cf0c 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