├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── cf-worker └── index.js ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── mitm.html └── sw.js ├── src ├── App.vue ├── assets │ ├── aria2-tip-1.png │ ├── aria2-tip-2.png │ ├── logo.png │ ├── logo1.png │ └── phone-pc2.png ├── components │ ├── DPlayer.vue │ ├── HelloWorld.vue │ ├── MessageContent.vue │ ├── Plyr.vue │ ├── Task.vue │ └── Video.vue ├── config │ └── index.ts ├── env.d.ts ├── index.d.ts ├── main.ts ├── router │ └── index.ts ├── utils │ ├── axios.ts │ ├── gcid.worker.ts │ └── index.ts └── views │ ├── invited.vue │ ├── layout │ └── index.vue │ ├── list.vue │ ├── login.vue │ ├── register.vue │ ├── setting.vue │ ├── share.vue │ ├── shareInfo.vue │ ├── sms.vue │ ├── test.vue │ ├── testtest.vue │ └── trash.vue ├── tsconfig.json └── vite.config.ts /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | name: deploy 3 | 4 | # Controls when the action will run. 5 | on: 6 | # Triggers the workflow on push or pull request events but only for the main branch 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 18 | jobs: 19 | # This workflow contains a single job called "build" 20 | build: 21 | # The type of runner that the job will run on 22 | runs-on: ubuntu-latest 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 27 | - name: Checkout 🛎️ 28 | uses: actions/checkout@v2.3.1 29 | 30 | - name: Install and Build 🔧 31 | env: 32 | CF_DOMAIN: ${{ secrets.CF_DOMAIN }} # CF-Workers反代域名,`DIRECT`直连 33 | REPO_NAME: ${{ github.event.repository.name }} 34 | BASE_PATH: ${{ secrets.BASE_PATH }} # 页面Base-path,默认"/{repo_name}"如"/pikpak" 35 | INVITE_CODE: ${{ secrets.INVITE_CODE }} # APK邀请码 36 | CF_INVITE: ${{ secrets.CF_INVITE }} # CF-Workers邀请域名 37 | CNAME_DOMAIN: ${{ secrets.CNAME_DOMAIN }} # CNAME域名 38 | run: | 39 | [ $CF_DOMAIN ] && { 40 | [ "$CF_DOMAIN" == "DIRECT" ] && sed -i "/\[/,/]/cexport const proxy = []" src/config/index.ts || \ 41 | sed -i "/\[/,/]/cexport const proxy = ['https://$CF_DOMAIN']" src/config/index.ts; 42 | } 43 | [ $BASE_PATH ] || BASE_PATH="/${REPO_NAME}" 44 | [ "$BASE_PATH" != "/pikpak" ] && sed -i "s|/pikpak|$BASE_PATH|g" vite.config.ts 45 | [ $INVITE_CODE ] && sed -i "s|apk/url/225815|apk/url/$INVITE_CODE|g" src/views/login.vue 46 | [ $CF_INVITE ] && sed -i "s/invite.z7.workers.dev/$CF_INVITE/g" src/views/register.vue src/views/sms.vue src/views/testtest.vue 47 | [ $CNAME_DOMAIN ] && echo "$CNAME_DOMAIN" > public/CNAME 48 | 49 | npm install 50 | npm run build 51 | 52 | # Runs a single command using the runners shell 53 | - name: Deploy 54 | uses: JamesIves/github-pages-deploy-action@4.1.5 55 | with: 56 | # token: ${{ secrets.ACCESS_TOKEN }} # Settings > Secret 建立的 ACCESS_TOKEN 57 | branch: gh-pages # deploy 到 gh-pages 這個分支 58 | folder: dist # build 後的資料夾 59 | # BUILD_SCRIPT: npm install && npm run build # 執行指令 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PikPak 个人网页版 2 | 3 | ## 官方地址 4 | 5 | * [PikPak](https://mypikpak.com) 6 | 7 | ## Demo 8 | * [PikPak](https://mumuchenchen.github.io/pikpak/) 9 | 10 | ## 安装部署 11 | 12 | ### 安装教程 13 | * [去年夏天版教程](https://www.tjsky.net/?p=201) 14 | ### Github Aciton 15 | 16 | ### Github Page 17 | 18 | ### Cloudflare Workers 19 | * [CF Workers实现反代](cf-worker) -------------------------------------------------------------------------------- /cf-worker/index.js: -------------------------------------------------------------------------------- 1 | addEventListener('fetch', event => { 2 | event.passThroughOnException() 3 | 4 | event.respondWith(handleRequest(event)) 5 | }) 6 | 7 | /** 8 | * Respond to the request 9 | * @param {Request} request 10 | */ 11 | async function handleRequest(event) { 12 | const { request } = event; 13 | 14 | //请求头部、返回对象 15 | let reqHeaders = new Headers(request.headers), 16 | outBody, outStatus = 200, outStatusText = 'OK', outCt = null, outHeaders = new Headers({ 17 | "Access-Control-Allow-Origin": reqHeaders.get('Origin'), 18 | "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", 19 | "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" 20 | }); 21 | 22 | try { 23 | //取域名第一个斜杠后的所有信息为代理链接 24 | let url = request.url.substr(8); 25 | url = decodeURIComponent(url.substr(url.indexOf('/') + 1)); 26 | 27 | //需要忽略的代理 28 | if (request.method == "OPTIONS" && reqHeaders.has('access-control-request-headers')) { 29 | //输出提示 30 | return new Response(null, PREFLIGHT_INIT) 31 | } 32 | else if(url.length < 3 || url.indexOf('.') == -1 || url == "favicon.ico" || url == "robots.txt") { 33 | return Response.redirect('https://baidu.com', 301) 34 | } 35 | //阻断 36 | else if (blocker.check(url)) { 37 | return Response.redirect('https://baidu.com', 301) 38 | } 39 | else { 40 | //补上前缀 http:// 41 | url = url.replace(/https:(\/)*/,'https://').replace(/http:(\/)*/, 'http://') 42 | if (url.indexOf("://") == -1) { 43 | url = "http://" + url; 44 | } 45 | //构建 fetch 参数 46 | let fp = { 47 | method: request.method, 48 | headers: {} 49 | } 50 | 51 | //保留头部其它信息 52 | let he = reqHeaders.entries(); 53 | for (let h of he) { 54 | if (!['content-length'].includes(h[0])) { 55 | fp.headers[h[0]] = h[1]; 56 | } 57 | } 58 | // 是否带 body 59 | if (["POST", "PUT", "PATCH", "DELETE"].indexOf(request.method) >= 0) { 60 | const ct = (reqHeaders.get('content-type') || "").toLowerCase(); 61 | if (ct.includes('application/json')) { 62 | let requestJSON = await request.json() 63 | console.log(typeof requestJSON) 64 | fp.body = JSON.stringify(requestJSON); 65 | } else if (ct.includes('application/text') || ct.includes('text/html')) { 66 | fp.body = await request.text(); 67 | } else if (ct.includes('form')) { 68 | fp.body = await request.formData(); 69 | } else { 70 | fp.body = await request.blob(); 71 | } 72 | } 73 | // 发起 fetch 74 | let fr = (await fetch(url, fp)); 75 | outCt = fr.headers.get('content-type'); 76 | if(outCt && (outCt.includes('application/text') || outCt.includes('text/html'))) { 77 | try { 78 | // 添加base 79 | let newFr = new HTMLRewriter() 80 | .on("head", { 81 | element(element) { 82 | element.prepend(``, { 83 | html: true 84 | }) 85 | }, 86 | }) 87 | .transform(fr) 88 | fr = newFr 89 | } catch(e) { 90 | } 91 | } 92 | for (const [key, value] of fr.headers.entries()) { 93 | outHeaders.set(key, value); 94 | } 95 | outStatus = fr.status; 96 | outStatusText = fr.statusText; 97 | outBody = fr.body; 98 | } 99 | } catch (err) { 100 | outCt = "application/json"; 101 | outBody = JSON.stringify({ 102 | code: -1, 103 | msg: JSON.stringify(err.stack) || err 104 | }); 105 | } 106 | 107 | //设置类型 108 | if (outCt && outCt != "") { 109 | outHeaders.set("content-type", outCt); 110 | } 111 | 112 | let response = new Response(outBody, { 113 | status: outStatus, 114 | statusText: outStatusText, 115 | headers: outHeaders 116 | }) 117 | 118 | return response; 119 | 120 | // return new Response('OK', { status: 200 }) 121 | } 122 | 123 | /** 124 | * 阻断器 125 | */ 126 | const blocker = { 127 | keys: [".m3u8", ".ts", ".acc", ".m4s", "photocall.tv", "googlevideo.com"], 128 | check: function (url) { 129 | url = url.toLowerCase(); 130 | let len = blocker.keys.filter(x => url.includes(x)).length; 131 | return len != 0; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PikPak 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pikpak", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vue-tsc --noEmit && vite build", 7 | "serve": "vite preview" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.23.0", 11 | "clipboard": "^2.0.8", 12 | "element-resize-detector": "^1.2.3", 13 | "plyr": "^3.6.9", 14 | "qs": "^6.10.1", 15 | "streamsaver": "^2.0.5", 16 | "vue": "^3.2.16", 17 | "vue-gtag": "^2.0.1", 18 | "vue-router": "^4.0.12" 19 | }, 20 | "devDependencies": { 21 | "@types/ali-oss": "^6.16.1", 22 | "@types/crypto-js": "^4.0.2", 23 | "@types/dplayer": "^1.25.2", 24 | "@types/element-resize-detector": "^1.1.3", 25 | "@types/qs": "^6.9.7", 26 | "@types/streamsaver": "^2.0.1", 27 | "@types/video.js": "^7.3.27", 28 | "@vicons/tabler": "^0.11.0", 29 | "@vitejs/plugin-vue": "^1.9.3", 30 | "ali-oss": "^6.16.0", 31 | "comlink": "^4.3.1", 32 | "crypto-js": "^4.1.1", 33 | "naive-ui": "^2.19.11", 34 | "sass": "^1.43.4", 35 | "typescript": "^4.4.3", 36 | "video.js": "^7.15.4", 37 | "vite": "^2.6.4", 38 | "vue-cnzz-analytics": "^2.2.0", 39 | "vue-tsc": "^0.3.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaokaixuan/pikpak/67daf006d7a569d6b7aade2538f9384475890eed/public/favicon.ico -------------------------------------------------------------------------------- /public/mitm.html: -------------------------------------------------------------------------------- 1 | 14 | 167 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | /* global self ReadableStream Response */ 2 | 3 | self.addEventListener('install', () => { 4 | self.skipWaiting() 5 | }) 6 | 7 | self.addEventListener('activate', event => { 8 | event.waitUntil(self.clients.claim()) 9 | }) 10 | 11 | const map = new Map() 12 | 13 | // This should be called once per download 14 | // Each event has a dataChannel that the data will be piped through 15 | self.onmessage = event => { 16 | // We send a heartbeat every x second to keep the 17 | // service worker alive if a transferable stream is not sent 18 | if (event.data === 'ping') { 19 | return 20 | } 21 | 22 | const data = event.data 23 | const downloadUrl = data.url || self.registration.scope + Math.random() + '/' + (typeof data === 'string' ? data : data.filename) 24 | const port = event.ports[0] 25 | const metadata = new Array(3) // [stream, data, port] 26 | 27 | metadata[1] = data 28 | metadata[2] = port 29 | 30 | // Note to self: 31 | // old streamsaver v1.2.0 might still use `readableStream`... 32 | // but v2.0.0 will always transfer the stream through MessageChannel #94 33 | if (event.data.readableStream) { 34 | metadata[0] = event.data.readableStream 35 | } else if (event.data.transferringReadable) { 36 | port.onmessage = evt => { 37 | port.onmessage = null 38 | metadata[0] = evt.data.readableStream 39 | } 40 | } else { 41 | metadata[0] = createStream(port) 42 | } 43 | 44 | map.set(downloadUrl, metadata) 45 | port.postMessage({ download: downloadUrl }) 46 | } 47 | 48 | function createStream (port) { 49 | // ReadableStream is only supported by chrome 52 50 | return new ReadableStream({ 51 | start (controller) { 52 | // When we receive data on the messageChannel, we write 53 | port.onmessage = ({ data }) => { 54 | if (data === 'end') { 55 | return controller.close() 56 | } 57 | 58 | if (data === 'abort') { 59 | controller.error('Aborted the download') 60 | return 61 | } 62 | 63 | controller.enqueue(data) 64 | } 65 | }, 66 | cancel () { 67 | console.log('user aborted') 68 | } 69 | }) 70 | } 71 | 72 | self.onfetch = event => { 73 | const url = event.request.url 74 | 75 | // this only works for Firefox 76 | if (url.endsWith('/ping')) { 77 | return event.respondWith(new Response('pong')) 78 | } 79 | 80 | const hijacke = map.get(url) 81 | 82 | if (!hijacke) return null 83 | 84 | const [ stream, data, port ] = hijacke 85 | 86 | map.delete(url) 87 | 88 | // Not comfortable letting any user control all headers 89 | // so we only copy over the length & disposition 90 | const responseHeaders = new Headers({ 91 | 'Content-Type': 'application/octet-stream; charset=utf-8', 92 | 93 | // To be on the safe side, The link can be opened in a iframe. 94 | // but octet-stream should stop it. 95 | 'Content-Security-Policy': "default-src 'none'", 96 | 'X-Content-Security-Policy': "default-src 'none'", 97 | 'X-WebKit-CSP': "default-src 'none'", 98 | 'X-XSS-Protection': '1; mode=block' 99 | }) 100 | 101 | let headers = new Headers(data.headers || {}) 102 | 103 | if (headers.has('Content-Length')) { 104 | responseHeaders.set('Content-Length', headers.get('Content-Length')) 105 | } 106 | 107 | if (headers.has('Content-Disposition')) { 108 | responseHeaders.set('Content-Disposition', headers.get('Content-Disposition')) 109 | } 110 | 111 | // data, data.filename and size should not be used anymore 112 | if (data.size) { 113 | console.warn('Depricated') 114 | responseHeaders.set('Content-Length', data.size) 115 | } 116 | 117 | let fileName = typeof data === 'string' ? data : data.filename 118 | if (fileName) { 119 | console.warn('Depricated') 120 | // Make filename RFC5987 compatible 121 | fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A') 122 | responseHeaders.set('Content-Disposition', "attachment; filename*=UTF-8''" + fileName) 123 | } 124 | 125 | event.respondWith(new Response(stream, { headers: responseHeaders })) 126 | 127 | port.postMessage({ debug: 'Download started' }) 128 | } 129 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 51 | 52 | 54 | -------------------------------------------------------------------------------- /src/assets/aria2-tip-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaokaixuan/pikpak/67daf006d7a569d6b7aade2538f9384475890eed/src/assets/aria2-tip-1.png -------------------------------------------------------------------------------- /src/assets/aria2-tip-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaokaixuan/pikpak/67daf006d7a569d6b7aade2538f9384475890eed/src/assets/aria2-tip-2.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaokaixuan/pikpak/67daf006d7a569d6b7aade2538f9384475890eed/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaokaixuan/pikpak/67daf006d7a569d6b7aade2538f9384475890eed/src/assets/logo1.png -------------------------------------------------------------------------------- /src/assets/phone-pc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaokaixuan/pikpak/67daf006d7a569d6b7aade2538f9384475890eed/src/assets/phone-pc2.png -------------------------------------------------------------------------------- /src/components/DPlayer.vue: -------------------------------------------------------------------------------- 1 | 4 | 30 | 31 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | 36 | 53 | -------------------------------------------------------------------------------- /src/components/MessageContent.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/Plyr.vue: -------------------------------------------------------------------------------- 1 | 11 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/Task.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 132 | 133 | -------------------------------------------------------------------------------- /src/components/Video.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export const proxy = [ 2 | 'https://api.13pikpak.cf', 3 | 'https://api.14pikpak.cf', 4 | 'https://api.15pikpak.cf', 5 | 'https://api.16pikpak.cf', 6 | 'https://api.17pikpak.cf', 7 | 'https://api.18pikpak.cf', 8 | ] 9 | 10 | export const version = '1.0.0' -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $message: any, 3 | $downId: string[] 4 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import http from './utils/axios' 5 | import elementResizeDetectorMaker from 'element-resize-detector' 6 | import cnzzAnalytics from 'vue-cnzz-analytics' 7 | 8 | const app = createApp(App) 9 | app.directive('resize', { 10 | mounted(el, binding, vnode) { 11 | el.$$erd = elementResizeDetectorMaker({ 12 | strategy: 'scroll' 13 | }) 14 | el.$$erd.listenTo({}, el, (element:HTMLElement) => { 15 | let width = element.offsetWidth 16 | let height = element.offsetHeight 17 | el.$$time && clearTimeout(el.$$time) 18 | el.$$time = setTimeout(() => { 19 | vnode.props?.onResize(width, height) 20 | }, 300) 21 | }) 22 | }, 23 | unmounted(el) { 24 | el.$$erd && el.$$erd.uninstall(el) 25 | el.$$time && clearTimeout(el.$$time) 26 | } 27 | }) 28 | app.config.globalProperties.$http = http 29 | app.use(router) 30 | app.use(cnzzAnalytics, { 31 | router: router, 32 | siteIdList: [ 33 | 1280510106, 34 | ], 35 | }); 36 | app.mount('#app') 37 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | import Layout from '../views/layout/index.vue' 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | path: '/', 6 | name: 'home', 7 | component: Layout, 8 | redirect: '/list', 9 | beforeEnter: (to, from, next) => { 10 | const pikpakLogin = JSON.parse(window.localStorage.getItem('pikpakLogin') || '{}') 11 | if((!pikpakLogin || !pikpakLogin.access_token) && to.name !== 'setting') { 12 | next('/login') 13 | } else { 14 | next() 15 | } 16 | }, 17 | children: [ 18 | { 19 | path: 'list/:id?', 20 | name: 'list', 21 | component: () => import('../views/list.vue') 22 | }, 23 | { 24 | path: 'video', 25 | name: 'video', 26 | component: () => import('../views/list.vue') 27 | }, 28 | { 29 | path: 'image', 30 | name: 'image', 31 | component: () => import('../views/list.vue') 32 | }, 33 | { 34 | path: 'trash', 35 | name: 'trash', 36 | component: () => import('../views/trash.vue') 37 | }, 38 | { 39 | path: 'setting', 40 | name: 'setting', 41 | component: () => import('../views/setting.vue') 42 | }, 43 | { 44 | path: 'invited', 45 | name: 'invited', 46 | component: () => import('../views/invited.vue') 47 | }, 48 | { 49 | path: 'share', 50 | name: 'share', 51 | component: () => import('../views/share.vue') 52 | } 53 | ] 54 | }, 55 | { 56 | path: '/t/:id?', 57 | name: 'test', 58 | component: () => import('../views/test.vue') 59 | }, 60 | { 61 | path: '/s/:id/:password?', 62 | name: 'shareInfo', 63 | component: () => import('../views/shareInfo.vue'), 64 | }, 65 | { 66 | path: '/login', 67 | name: 'login', 68 | component: () => import('../views/login.vue'), 69 | }, 70 | { 71 | path: '/sms', 72 | name: 'sms', 73 | component: () => import('../views/sms.vue'), 74 | }, 75 | { 76 | path: '/register', 77 | name: 'register', 78 | component: () => import('../views/register.vue'), 79 | }, 80 | { 81 | path: '/testtest', 82 | name: 'testtest', 83 | component: () => import('../views/testtest.vue'), 84 | }, 85 | ] 86 | 87 | const router = createRouter({ 88 | history: createWebHashHistory(), 89 | routes 90 | }) 91 | 92 | export default router -------------------------------------------------------------------------------- /src/utils/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import router from '../router/index' 3 | 4 | const instance = axios.create({}) 5 | 6 | instance.interceptors.request.use(request => { 7 | const pikpakLogin = JSON.parse(window.localStorage.getItem('pikpakLogin') || '{}') 8 | request.headers = request.headers || {} 9 | if (pikpakLogin.access_token) { 10 | request.headers['Authorization'] = `${pikpakLogin.token_type || 'Bearer'} ${pikpakLogin.access_token}` 11 | } 12 | if(request.url?.indexOf('https://', 4) === -1) { 13 | const proxyArray = JSON.parse(window.localStorage.getItem('proxy') || '[]') 14 | if (proxyArray.length > 0) { 15 | const index = Math.floor((Math.random() * proxyArray.length)) 16 | request.url = proxyArray[index] + '/' + request.url 17 | } 18 | } 19 | return request 20 | }) 21 | let isLoginLoading = false 22 | instance.interceptors.response.use(response => { 23 | return response 24 | }, (error) => { 25 | const { response, config } = error 26 | if(response.status) { 27 | switch (response.status) { 28 | case 401: 29 | // router.push('/login') 30 | const loginData = window.localStorage.getItem('pikpakLoginData') 31 | const loginDataJson = loginData ? JSON.parse(loginData) : {} 32 | if(loginDataJson.username && loginDataJson.password && !isLoginLoading) { 33 | console.log('wait', config.url) 34 | isLoginLoading = true 35 | return instance.post('https://user.mypikpak.com/v1/auth/signin', { 36 | "captcha_token": "", 37 | "client_id": "YNxT9w7GMdWvEOKa", 38 | "client_secret": "dbw2OtmVEeuUvIptb1Coyg", 39 | ...loginDataJson 40 | }) 41 | .then((res:any) => { 42 | if(res.data && res.data.access_token) { 43 | window.localStorage.setItem('pikpakLogin', JSON.stringify(res.data)) 44 | } 45 | isLoginLoading = false 46 | return instance(config) 47 | }) 48 | .catch(() => { 49 | router.push('/login') 50 | return false 51 | }) 52 | } else if(loginDataJson.username && loginDataJson.password && isLoginLoading) { 53 | return new Promise((resolve, reject) => { 54 | const s = setInterval(() => { 55 | if(!isLoginLoading) { 56 | clearInterval(s) 57 | resolve(instance(config)) 58 | } 59 | }, 100) 60 | }) 61 | } else { 62 | router.push('/login') 63 | return Promise.reject(error) 64 | } 65 | 66 | break; 67 | // case 400: case 403: 68 | // window.$message.error(response.data.error_description || '出错了') 69 | default: 70 | window.$message.error(response?.data?.error_description || '出错了') 71 | break; 72 | } 73 | } 74 | console.log(config.url, 111) 75 | return Promise.reject(error) 76 | }) 77 | 78 | const instance2 = axios.create({}) 79 | instance2.interceptors.request.use(request => { 80 | request.headers = { 81 | Authorization: 'Bearer secret_FErDcv3kgsFNLiWUDOWYdJhNqOIKj55eteBg3vIoiLt', 82 | 'Notion-Version': '2021-08-16', 83 | 'Content-Type': 'application/json' 84 | } 85 | const proxyArray = JSON.parse(window.localStorage.getItem('proxy') || '[]') 86 | if (proxyArray.length > 0) { 87 | const index = Math.floor((Math.random() * proxyArray.length)) 88 | request.url = proxyArray[index] + '/' + request.url 89 | } 90 | return request 91 | }) 92 | 93 | export const notionHttp = instance2 94 | export default instance -------------------------------------------------------------------------------- /src/utils/gcid.worker.ts: -------------------------------------------------------------------------------- 1 | // import 'regenerator-runtime/runtime' 2 | import {expose} from 'comlink' 3 | import * as CryptoJS from 'crypto-js' 4 | 5 | function calculateBlockSize (size:number) { 6 | if (size >= 0 && size <= (128 << 20)) { 7 | return 256 << 10 8 | } 9 | if (size > (128 << 20) && size <= (256 << 20)) { 10 | return 512 << 10 11 | } 12 | if (size > (256 << 20) && size <= (512 << 20)) { 13 | return 1024 << 10 14 | } 15 | return 2048 << 10 16 | } 17 | 18 | class Gcid { 19 | gcid: string 20 | gcidSHA1: any 21 | constructor () { 22 | this.gcid = '' 23 | this.gcidSHA1 = '' 24 | } 25 | create () { 26 | this.gcid = '' 27 | this.gcidSHA1 = CryptoJS.algo.SHA1.create() 28 | } 29 | calculate (ab:any, blockSize:number) { 30 | const size = ab.byteLength 31 | const blockNum = size / blockSize 32 | for (let i = 0; i < blockNum; i++) { 33 | const wa = CryptoJS.lib.WordArray.create(ab.slice(blockSize * i, blockSize * (i + 1))) 34 | const bcidSHA1 = CryptoJS.SHA1(wa) 35 | this.gcidSHA1.update(bcidSHA1) 36 | } 37 | if (blockSize * blockNum < size) { 38 | const wa = CryptoJS.lib.WordArray.create(ab.slice(blockSize * blockNum, size)) 39 | const bcidSHA1 = CryptoJS.SHA1(wa) 40 | this.gcidSHA1.update(bcidSHA1) 41 | } 42 | } 43 | finalize () { 44 | this.gcid = this.gcidSHA1.finalize().toString().toUpperCase() 45 | // console.log('worker计算出来的gcid', this.gcid) 46 | } 47 | } 48 | 49 | // let file: any = null 50 | 51 | async function readFile (file:any, {progress}:any) { 52 | return new Promise(resolve => { 53 | const gcidTool = new Gcid(); 54 | gcidTool.create() 55 | const reader = new FileReader() // FileReader实例 56 | const blockSize = calculateBlockSize(file.size) 57 | const CHUNK_MIN_SIZE = 100 * 1024 * 1024 58 | const CHUNK_SIZE = Math.floor(CHUNK_MIN_SIZE / blockSize) * blockSize // 以blockSize为单位做最大分片(小于100M) 59 | console.log(CHUNK_SIZE, CHUNK_MIN_SIZE) 60 | const CHUNK_LEN = Math.ceil(file.size / CHUNK_SIZE) 61 | let reverse_index = 0 62 | reader.onload = async () => { 63 | gcidTool.calculate(reader.result, blockSize) 64 | reverse_index += 1 65 | console.log(reverse_index, progress) 66 | if(progress && typeof progress === 'function') { 67 | progress(reverse_index / CHUNK_LEN) 68 | } 69 | if (reverse_index >= CHUNK_LEN) { 70 | await gcidTool.finalize() 71 | const gcid = await gcidTool.gcid 72 | close() 73 | resolve(gcid) 74 | } else { 75 | seek() 76 | } 77 | } 78 | seek(); 79 | function seek() { 80 | let start = CHUNK_SIZE * reverse_index; 81 | let end = (reverse_index >= CHUNK_LEN - 1) ? file.size : CHUNK_SIZE * (reverse_index + 1); 82 | let slice = file.slice(start, end) 83 | reader.readAsArrayBuffer(slice); 84 | } 85 | }) 86 | } 87 | 88 | // 循环计算 89 | expose(readFile) 90 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const byteConvert = function(bytes:number) { 2 | if (isNaN(bytes)) { 3 | return '' 4 | } 5 | let symbols = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 6 | let exp = Math.floor(Math.log(bytes)/Math.log(2)) 7 | if (exp < 1) { 8 | exp = 0 9 | } 10 | let i = Math.floor(exp / 10); 11 | bytes = bytes / Math.pow(2, 10 * i) 12 | 13 | if (bytes.toString().length > bytes.toFixed(2).toString().length) { 14 | bytes = parseFloat(bytes.toFixed(2)) 15 | } 16 | return bytes + ' ' + symbols[i] 17 | } 18 | -------------------------------------------------------------------------------- /src/views/invited.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 187 | 188 | -------------------------------------------------------------------------------- /src/views/layout/index.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 230 | 231 | -------------------------------------------------------------------------------- /src/views/list.vue: -------------------------------------------------------------------------------- 1 | 238 | 239 | 1174 | 1175 | -------------------------------------------------------------------------------- /src/views/login.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 123 | 124 | 208 | -------------------------------------------------------------------------------- /src/views/register.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 250 | 251 | -------------------------------------------------------------------------------- /src/views/setting.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 213 | 214 | -------------------------------------------------------------------------------- /src/views/share.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 168 | 169 | -------------------------------------------------------------------------------- /src/views/shareInfo.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 290 | 291 | 343 | 344 | -------------------------------------------------------------------------------- /src/views/sms.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 220 | 221 | -------------------------------------------------------------------------------- /src/views/test.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | -------------------------------------------------------------------------------- /src/views/testtest.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 37 | -------------------------------------------------------------------------------- /src/views/trash.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 201 | 202 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"] 13 | }, 14 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 15 | } 16 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig(({mode}) => { 6 | console.log(mode) 7 | return { 8 | base: mode === 'development' ? '' : '/pikpak', 9 | plugins: [vue()], 10 | server: { 11 | proxy: { 12 | '/v1/pages': { 13 | target: 'https://api.notion.com', 14 | changeOrigin: true, 15 | } 16 | } 17 | } 18 | } 19 | }) 20 | --------------------------------------------------------------------------------