├── .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 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
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 |
2 |
3 |
4 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {{ msg }}
11 |
12 |
13 | Recommended IDE setup:
14 | VSCode
15 | +
16 | Volar
17 |
18 |
19 | See README.md
for more information.
20 |
21 |
22 |
23 | Vite Docs
24 |
25 | |
26 | Vue 3 Docs
27 |
28 |
29 |
30 |
31 | Edit
32 | components/HelloWorld.vue
to test hot module replacement.
33 |
34 |
35 |
36 |
53 |
--------------------------------------------------------------------------------
/src/components/MessageContent.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/Plyr.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/Task.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{filesList?.length || 0}}项保存中
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
![]()
16 |
17 |
18 |
19 |
20 | {{item.file_name}}
21 |
22 |
23 |
24 |
25 | {{ byteConvert(item.file_size) }}
26 |
27 |
28 |
29 | {{item.message}}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 确定删除?
43 |
44 |
45 |
46 |
47 |
48 |
49 |
收起
50 |
51 |
52 |
53 |
54 |
132 |
133 |
--------------------------------------------------------------------------------
/src/components/Video.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | / {{invitationInfo?.join_vip_nums}}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | / {{invitationInfo?.invited_nums}}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 复制
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 立即下载
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 加载中
58 |
59 |
60 |
61 |
62 |
63 |
187 |
188 |
--------------------------------------------------------------------------------
/src/views/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 | PikPak
17 |
18 |
19 |
20 | {{byteConvert(aboutInfo?.quota.usage)}} / {{byteConvert(aboutInfo?.quota.limit)}}
会员码
21 |
29 |
30 |
31 |
32 |
33 | 2021年12月23日~2021年12月31日¥119购体验会员VIP年卡
34 |
35 | 2021年12月23日~2021年12月31日 【1年PikPak体验会员仅售:119元!原价450元】 2022年1月1日起: 1年PikPak体验会员仅售:169元!原价450元】 -每人只能购买使用一次,官方代理商分销,感谢支持
36 |
37 |
38 |
39 |
40 |
41 |

42 |

43 |
44 | {{userInfo?.name}}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 退出登录
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 【0.33元/天】PikPak体验会员VIP年卡-可与7天免费会员码叠加-每人只能购买使用一次,感谢支持
80 |
81 |
82 |
83 | 添加
84 |
85 |
86 |
87 |
88 |
89 |
230 |
231 |
--------------------------------------------------------------------------------
/src/views/list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
31 |
32 |
33 |
34 | 加载中
35 |
36 |
37 |
38 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | 1.支持Magnet链接(magnet:?xt=urn),Magent链接只能默认保存到My Pack
101 | 2.支持秒传链接(PikPak://PikPak Tutorial.mp4|19682618|123)秒传链接默认保存到当前文件夹或第一个文件夹不能保存到根目录
102 | 3.支持新建文件夹(普通格式,不带:)
103 | 4.换行添加多个
104 |
105 |
106 |
107 |
108 | 添加
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
![]()
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | 重命名
151 |
152 |
153 |
154 |
155 |
156 |
157 | 自定义菜单
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | {{item.name}}
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | {{item}}
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 | 添加
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 | 复制
208 |
209 |
210 |
211 |
212 |
213 |
214 | 复制
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
1174 |
1175 |
--------------------------------------------------------------------------------
/src/views/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |
7 |
8 |

9 |
PikPak
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 记住登陆
21 |
22 |
23 | 登陆
24 |
25 |
26 | 忘记密码
27 |
28 | 去下载注册得5天VIP
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 手机登陆
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | APP内谷歌登录的账号请先通过忘记密码设置密码后登录
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
123 |
124 |
208 |
--------------------------------------------------------------------------------
/src/views/register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |
7 |
8 |

9 |
PikPak
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{ time >= 60 ? '发送验证码' : ('重新发送 ' + time + 's')}}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 | 注册
36 |
37 |
38 | 已有账号?点击登录
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | APP内谷歌登录的账号请先通过忘记密码设置密码后登录
48 |
49 |
50 |
51 |
52 |
53 |
54 |
250 |
251 |
--------------------------------------------------------------------------------
/src/views/setting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 绑定telegram
7 |
8 |
9 |
10 | Telegram绑定
11 | Telegram机器人地址
12 |
13 |
14 | aria2设置
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 选择文件夹时保存文件夹结构
25 | 选择文件夹时仅保存文件
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | 测试并保存
36 |
37 |
38 |
39 |
40 | 自动登录设置
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 保存
55 |
56 |
57 |
58 |
59 |
60 |
61 | 保存设置
62 | 恢复默认
63 |
64 |
65 |
66 | 官方网站
67 | 官方交流群
68 | 开源仓库
69 | 部署教程
70 | 问题反馈
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
213 |
214 |
--------------------------------------------------------------------------------
/src/views/share.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 作者
6 | 地址
7 |
8 |
9 |
10 |
11 | 请先在文件列表操作选择设置默认
12 |
13 |
14 |
15 | {{uploadFolder.name}}
16 |
17 |
18 |
19 |
20 |
21 |
22 | 加载中
23 |
24 |
25 |
26 |
27 |
28 |
168 |
169 |
--------------------------------------------------------------------------------
/src/views/shareInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
20 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 去首页
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
290 |
291 |
343 |
344 |
--------------------------------------------------------------------------------
/src/views/sms.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |
7 |
8 |

9 |
PikPak
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {{ time >= 60 ? '发送验证码' : ('重新发送 ' + time + 's')}}
20 |
21 |
22 |
25 |
26 | 登录
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | 邮箱登陆
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | APP内谷歌登录的账号请先通过忘记密码设置密码后登录
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
220 |
221 |
--------------------------------------------------------------------------------
/src/views/test.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | e.preventDefault()">
6 |
7 |
8 |
9 |
10 |
11 |
12 | 点击或者拖动文件到该区域来上传
13 |
14 | 支持链接上拼上id传到指定文件夹哦~(/t/12154sds)
15 |
16 |
17 | 请不要上传敏感数据,比如你的银行卡号和密码,信用卡号有效期和安全码
18 |
19 |
20 | 记得解决浏览器跨域问题哦
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/views/testtest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
37 |
--------------------------------------------------------------------------------
/src/views/trash.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 | 加载中
16 |
17 |
18 |
42 |
43 |
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 |
--------------------------------------------------------------------------------