神代綺凛 commited on
Commit
afc074a
1 Parent(s): ef2c178

feat: cookie display

Browse files
.eslintrc.cjs CHANGED
@@ -23,6 +23,9 @@ module.exports = {
23
  '@typescript-eslint/ban-types': 'off',
24
  '@typescript-eslint/no-explicit-any': 'off',
25
  '@typescript-eslint/ban-ts-comment': 'off',
 
 
 
26
  '@typescript-eslint/consistent-type-imports': [
27
  'error',
28
  {
 
23
  '@typescript-eslint/ban-types': 'off',
24
  '@typescript-eslint/no-explicit-any': 'off',
25
  '@typescript-eslint/ban-ts-comment': 'off',
26
+ '@typescript-eslint/no-unused-vars': 'warn',
27
+ '@typescript-eslint/explicit-member-accessibility': 'warn',
28
+ '@typescript-eslint/member-ordering': 'warn',
29
  '@typescript-eslint/consistent-type-imports': [
30
  'error',
31
  {
.vscode/settings.json CHANGED
@@ -1,3 +1,6 @@
1
  {
2
- "typescript.tsdk": "node_modules/typescript/lib"
 
 
 
3
  }
 
1
  {
2
+ "typescript.tsdk": "node_modules/typescript/lib",
3
+ "editor.codeActionsOnSave": {
4
+ "source.fixAll.eslint": "explicit"
5
+ }
6
  }
README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ## 哔哩哔哩 cookie 获取工具
2
+
3
+ 扫码登录获取B站 cookie,用于各种开发场景
4
+
5
+ Vercel 部署:[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FTsuk1ko%2Fbilibili-qr-login)
6
+
7
+ ### TODO
8
+
9
+ - [ ] iframe / 窗口 模式
_api/qr.ts CHANGED
@@ -6,15 +6,35 @@ import type { StreamingApi } from 'hono/utils/stream';
6
  enum SSEEvent {
7
  GENERATE = 'generate',
8
  POLL = 'poll',
 
9
  }
10
 
 
 
 
 
 
 
 
 
 
11
  export const runtime = 'edge';
12
 
13
  export const app = new Hono().basePath('/api');
14
 
15
- app.get('/qr', c =>
16
- streamSSE(c, async stream => {
17
- // 指定编码
 
 
 
 
 
 
 
 
 
 
18
  c.header('Content-Type', 'text/event-stream; charset=utf-8');
19
 
20
  let streamClosed = false;
@@ -22,28 +42,45 @@ app.get('/qr', c =>
22
  streamClosed = true;
23
  });
24
 
 
 
 
25
  // 获取登录链接
26
- const qr = new LoginQr();
27
- await stream.writeSSE({ data: JSON.stringify(await qr.generate()), event: SSEEvent.GENERATE });
28
- await stream.sleep(2000);
 
 
 
 
 
 
 
 
 
29
 
30
- for (let i = 0; i < 30 && !streamClosed; i++) {
31
- console.log('poll', Date.now());
 
32
  const result = await qr.poll();
33
  await stream.writeSSE({ data: JSON.stringify(result), event: SSEEvent.POLL });
34
- if (result.code === 0) {
35
- stream.close();
36
- break;
 
37
  }
38
  await stream.sleep(2000);
39
  }
40
- }),
41
- );
 
 
 
42
 
43
  export const GET = handle(app);
44
 
45
  interface GenerateQrResp {
46
- code: string;
47
  message: string;
48
  ttl: number;
49
  data: {
@@ -53,7 +90,7 @@ interface GenerateQrResp {
53
  }
54
 
55
  interface PollQrResp {
56
- code: string;
57
  message: string;
58
  ttl: number;
59
  data: {
@@ -72,34 +109,56 @@ interface PollQrResult {
72
  }
73
 
74
  class LoginQr {
75
- private key = '';
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  public async generate() {
78
- const r = await fetch('https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header');
 
 
79
  const {
80
- data: { url, qrcode_key: key },
 
 
81
  } = (await r.json()) as GenerateQrResp;
82
  this.key = key;
83
- return { url, key };
84
  }
85
 
86
  public async poll() {
87
  const r0 = await fetch(
88
  `https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=${this.key}&source=main-fe-header`,
 
89
  );
90
- const { data } = (await r0.json()) as PollQrResp;
91
- const result: PollQrResult = {
92
- code: data.code,
93
- msg: data.message,
94
- };
95
-
96
- if (data.code !== 0) return result;
 
 
 
 
 
 
97
 
98
  const cookie = new Cookie(r0.headers.getSetCookie());
99
  const r1 = await fetch(data.url);
100
 
101
  cookie.add(r1.headers.getSetCookie());
102
- result.cookie = cookie.toString();
103
 
104
  return result;
105
  }
@@ -118,6 +177,12 @@ class Cookie {
118
  const [name, ...values] = nv.split('=');
119
  this.cookie.set(name, values.join('='));
120
  });
 
 
 
 
 
 
121
  }
122
 
123
  public toString() {
 
6
  enum SSEEvent {
7
  GENERATE = 'generate',
8
  POLL = 'poll',
9
+ END = 'end',
10
  }
11
 
12
+ enum PollQrResultCode {
13
+ SUCCESS = 0,
14
+ EXPIRED = 86038,
15
+ NOT_CONFIRMED = 86090,
16
+ NOT_SCANNED = 86101,
17
+ }
18
+
19
+ const keepPollQrResultCode = new Set([PollQrResultCode.NOT_CONFIRMED, PollQrResultCode.NOT_SCANNED]);
20
+
21
  export const runtime = 'edge';
22
 
23
  export const app = new Hono().basePath('/api');
24
 
25
+ app.get('/qr', c => {
26
+ if (process.env.NODE_ENV !== 'development') {
27
+ try {
28
+ const referer = c.req.header('Referer');
29
+ if (!referer || new URL(referer).origin !== new URL(c.req.url).origin) {
30
+ return c.text('', 403);
31
+ }
32
+ } catch {
33
+ return c.text('', 403);
34
+ }
35
+ }
36
+ return streamSSE(c, async stream => {
37
+ // 编码加上 charset
38
  c.header('Content-Type', 'text/event-stream; charset=utf-8');
39
 
40
  let streamClosed = false;
 
42
  streamClosed = true;
43
  });
44
 
45
+ // 断线重连时的 key
46
+ const lastEventID = c.req.header('Last-Event-ID');
47
+
48
  // 获取登录链接
49
+ const qr = new LoginQr(c.req.header('User-Agent'), lastEventID);
50
+ if (!lastEventID) {
51
+ const genRes = await qr.generate();
52
+ console.log('generate', Date.now());
53
+ await stream.writeSSE({ data: JSON.stringify(genRes), event: SSEEvent.GENERATE, id: genRes.key });
54
+ if (genRes.code !== 0) {
55
+ await stream.writeSSE({ data: '', event: SSEEvent.END });
56
+ await stream.close();
57
+ return;
58
+ }
59
+ await stream.sleep(2000);
60
+ }
61
 
62
+ // 轮询
63
+ for (let i = 0; i < 100 && !streamClosed; i++) {
64
+ console.log('poll', i, Date.now());
65
  const result = await qr.poll();
66
  await stream.writeSSE({ data: JSON.stringify(result), event: SSEEvent.POLL });
67
+ if (!keepPollQrResultCode.has(result.code)) {
68
+ await stream.writeSSE({ data: '', event: SSEEvent.END });
69
+ await stream.close();
70
+ return;
71
  }
72
  await stream.sleep(2000);
73
  }
74
+
75
+ await stream.writeSSE({ data: '超时终止', event: SSEEvent.END });
76
+ await stream.close();
77
+ });
78
+ });
79
 
80
  export const GET = handle(app);
81
 
82
  interface GenerateQrResp {
83
+ code: number;
84
  message: string;
85
  ttl: number;
86
  data: {
 
90
  }
91
 
92
  interface PollQrResp {
93
+ code: number;
94
  message: string;
95
  ttl: number;
96
  data: {
 
109
  }
110
 
111
  class LoginQr {
112
+ private readonly header: Record<string, string | undefined> = {};
113
+
114
+ public constructor(
115
+ userAgent?: string,
116
+ private key = '',
117
+ ) {
118
+ this.header = {
119
+ 'User-Agent': userAgent,
120
+ Origin: 'https://www.bilibili.com',
121
+ Referer: 'https://www.bilibili.com/',
122
+ };
123
+ }
124
 
125
  public async generate() {
126
+ const r = await fetch('https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header', {
127
+ headers: this.header,
128
+ });
129
  const {
130
+ code,
131
+ message,
132
+ data: { url, qrcode_key: key } = { url: '', qrcode_key: '' },
133
  } = (await r.json()) as GenerateQrResp;
134
  this.key = key;
135
+ return { code, msg: message, url, key };
136
  }
137
 
138
  public async poll() {
139
  const r0 = await fetch(
140
  `https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=${this.key}&source=main-fe-header`,
141
+ { headers: this.header },
142
  );
143
+ const { code, message, data } = (await r0.json()) as PollQrResp;
144
+ const result: PollQrResult =
145
+ code === 0
146
+ ? {
147
+ code: data.code,
148
+ msg: data.message,
149
+ }
150
+ : {
151
+ code: Number(code),
152
+ msg: message,
153
+ };
154
+
155
+ if (result.code !== 0) return result;
156
 
157
  const cookie = new Cookie(r0.headers.getSetCookie());
158
  const r1 = await fetch(data.url);
159
 
160
  cookie.add(r1.headers.getSetCookie());
161
+ result.cookie = cookie.del('i-wanna-go-back').toString();
162
 
163
  return result;
164
  }
 
177
  const [name, ...values] = nv.split('=');
178
  this.cookie.set(name, values.join('='));
179
  });
180
+ return this;
181
+ }
182
+
183
+ public del(name: string) {
184
+ this.cookie.delete(name);
185
+ return this;
186
  }
187
 
188
  public toString() {
server/utils.ts CHANGED
@@ -16,7 +16,7 @@ export const startApiServer = async () => {
16
  nodemon({
17
  script: 'server/index.ts',
18
  ext: 'ts',
19
- watch: ['_api'],
20
  execMap: {
21
  ts: 'tsx',
22
  },
 
16
  nodemon({
17
  script: 'server/index.ts',
18
  ext: 'ts',
19
+ watch: ['_api', 'server'],
20
  execMap: {
21
  ts: 'tsx',
22
  },
src/App.vue CHANGED
@@ -1,5 +1,5 @@
1
  <template>
2
- <div class="text-center">
3
  <h2>哔哩哔哩 cookie 获取工具</h2>
4
  <p>
5
  使用手机 APP 扫码登录后即可获取 cookie (<a
@@ -10,89 +10,37 @@
10
  >)
11
  </p>
12
  </div>
13
- <div class="qrcode flex">
14
- <QrCode :value="qrValue" :options="qrCodeOption" />
15
- <div v-if="qrStatus !== QrStatus.WAIT" class="qrcode__mask flex">
16
- <CheckIcon v-if="qrStatus === QrStatus.SCANNED || qrStatus === QrStatus.SUCCESS" />
17
- <RefreshBtn v-else />
18
  </div>
19
  </div>
20
- <div class="text-center">
21
- <p>{{ qrStatusText }}</p>
 
 
 
22
  </div>
23
  </template>
24
 
25
  <script setup lang="ts">
26
- import { computed, ref } from 'vue';
27
  import QrCode from '@chenfengyuan/vue-qrcode';
28
  import RefreshBtn from './components/RefreshBtn.vue';
 
29
  import CheckIcon from './assets/icons/check_circle.svg';
 
30
  import type { QRCodeRenderersOptions } from 'qrcode';
31
 
32
- enum QrStatus {
33
- WAIT,
34
- SCANNED,
35
- EXPIRED,
36
- SUCCESS,
37
- ERROR,
38
- }
39
-
40
  const qrCodeOption: QRCodeRenderersOptions = {
41
  margin: 0,
42
  };
43
 
44
- const qrValue = ref(
45
- 'https://passport.bilibili.com/h5-app/passport/login/scan?navhide=1&qrcode_key=ed99a307afd9a309098071fb9d9258a7&from=main-fe-header',
46
- );
47
- const qrErrorMsg = ref('');
48
- const qrStatus = ref(QrStatus.EXPIRED);
49
- const qrStatusText = computed(() => {
50
- if (qrErrorMsg.value) return qrErrorMsg.value;
51
-
52
- switch (qrStatus.value) {
53
- case QrStatus.WAIT:
54
- return '等待扫码';
55
- case QrStatus.SCANNED:
56
- return '已扫码,等待登录';
57
- case QrStatus.EXPIRED:
58
- return '二维码已过期,请刷新';
59
- }
60
-
61
- return '';
62
- });
63
  </script>
64
 
65
- <style lang="less">
66
- html,
67
- body {
68
- margin: 0;
69
- padding: 0;
70
- height: 100%;
71
- }
72
-
73
- body {
74
- -webkit-font-smoothing: antialiased;
75
- -moz-osx-font-smoothing: auto;
76
- }
77
-
78
- body,
79
- #app,
80
- .flex {
81
- display: flex;
82
- flex-direction: column;
83
- align-items: center;
84
- justify-content: center;
85
- }
86
-
87
- .text-center {
88
- text-align: center;
89
- }
90
-
91
- .link {
92
- color: rgb(0, 0, 238);
93
- text-decoration: none;
94
- }
95
-
96
  .qrcode {
97
  position: relative;
98
  min-width: 196px;
@@ -107,4 +55,8 @@ body,
107
  background-color: rgba(255, 255, 255, 0.85);
108
  }
109
  }
 
 
 
 
110
  </style>
 
1
  <template>
2
+ <div class="text-center no-select">
3
  <h2>哔哩哔哩 cookie 获取工具</h2>
4
  <p>
5
  使用手机 APP 扫码登录后即可获取 cookie (<a
 
10
  >)
11
  </p>
12
  </div>
13
+ <div class="qrcode flex no-select">
14
+ <QrCode v-if="state.url" :value="state.url" :options="qrCodeOption" />
15
+ <div v-if="state.status !== QrStatus.WAIT" class="qrcode__mask flex">
16
+ <CheckIcon v-if="state.status === QrStatus.SCANNED || state.status === QrStatus.SUCCESS" />
17
+ <RefreshBtn v-else @click="restart" />
18
  </div>
19
  </div>
20
+ <div class="text-center no-select">
21
+ <p>{{ getters.statusText }}</p>
22
+ </div>
23
+ <div class="cookie-box">
24
+ <CookieDisplay v-if="state.cookie" :value="state.cookie" />
25
  </div>
26
  </template>
27
 
28
  <script setup lang="ts">
 
29
  import QrCode from '@chenfengyuan/vue-qrcode';
30
  import RefreshBtn from './components/RefreshBtn.vue';
31
+ import CookieDisplay from './components/CookieDisplay.vue';
32
  import CheckIcon from './assets/icons/check_circle.svg';
33
+ import { useQrSSE, QrStatus } from './utils/qrSSE';
34
  import type { QRCodeRenderersOptions } from 'qrcode';
35
 
 
 
 
 
 
 
 
 
36
  const qrCodeOption: QRCodeRenderersOptions = {
37
  margin: 0,
38
  };
39
 
40
+ const { state, getters, restart } = useQrSSE();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  </script>
42
 
43
+ <style scoped lang="less">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  .qrcode {
45
  position: relative;
46
  min-width: 196px;
 
55
  background-color: rgba(255, 255, 255, 0.85);
56
  }
57
  }
58
+
59
+ .cookie-box {
60
+ min-height: 180px;
61
+ }
62
  </style>
src/assets/icons/content_copy.svg ADDED
src/components/CookieDisplay.vue ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="cookie">
3
+ <pre ref="pre" class="cookie__pre">{{ value }}</pre>
4
+ <div class="cookie__copy flex flex-row btn" @click="copy"><CopyIcon style="margin-right: 4px" />{{ text }}</div>
5
+ </div>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { ref } from 'vue';
10
+ import CopyIcon from '../assets/icons/content_copy.svg';
11
+ import { useTipText } from '../utils/tipText';
12
+
13
+ defineProps<{ value: string }>();
14
+
15
+ const pre = ref<HTMLElement>();
16
+ const { text, changeText } = useTipText('复制');
17
+
18
+ const copy = () => {
19
+ const selection = window.getSelection()!;
20
+ const range = window.document.createRange();
21
+ selection.removeAllRanges();
22
+ range.selectNode(pre.value!);
23
+ selection.addRange(range);
24
+ window.document.execCommand('copy');
25
+ selection.removeAllRanges();
26
+ changeText('已复制');
27
+ };
28
+ </script>
29
+
30
+ <style lang="less" scoped>
31
+ .cookie {
32
+ max-width: 596px;
33
+ border-radius: 4px;
34
+ background-color: rgba(0, 0, 0, 0.1);
35
+ overflow: hidden;
36
+
37
+ &__pre {
38
+ text-wrap: wrap;
39
+ word-break: break-all;
40
+ margin: 0;
41
+ padding: 8px;
42
+ }
43
+
44
+ &__copy {
45
+ border-top: 1px solid rgba(0, 0, 0, 0.2);
46
+ padding: 8px;
47
+ text-align: center;
48
+ cursor: pointer;
49
+ }
50
+ }
51
+ </style>
src/components/RefreshBtn.vue CHANGED
@@ -1,5 +1,5 @@
1
  <template>
2
- <div class="refresh-btn flex"><RefreshIcon /></div>
3
  </template>
4
 
5
  <script setup lang="ts">
@@ -12,6 +12,9 @@ import RefreshIcon from '../assets/icons/refresh.svg';
12
  height: 32px;
13
  background-color: #fff;
14
  border-radius: 50%;
15
- cursor: pointer;
 
 
 
16
  }
17
  </style>
 
1
  <template>
2
+ <div class="refresh-btn btn flex"><RefreshIcon /></div>
3
  </template>
4
 
5
  <script setup lang="ts">
 
12
  height: 32px;
13
  background-color: #fff;
14
  border-radius: 50%;
15
+ box-shadow:
16
+ 0 1.25px 5px 0 rgba(0, 0, 0, 0.2),
17
+ 0 0.3333px 1.5px 0 rgba(0, 0, 0, 0.04);
18
+ overflow: hidden;
19
  }
20
  </style>
src/main.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import { createApp } from 'vue';
2
  import App from './App.vue';
3
 
 
1
+ import './style.less';
2
  import { createApp } from 'vue';
3
  import App from './App.vue';
4
 
src/style.less ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html,
2
+ body {
3
+ margin: 0;
4
+ padding: 0;
5
+ height: 100%;
6
+ }
7
+
8
+ body {
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: auto;
11
+ }
12
+
13
+ body,
14
+ #app,
15
+ .flex {
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+ justify-content: center;
20
+ }
21
+
22
+ .flex-row {
23
+ flex-direction: row;
24
+ }
25
+
26
+ #app {
27
+ margin: 0 16px;
28
+ }
29
+
30
+ .text-center {
31
+ text-align: center;
32
+ }
33
+
34
+ .no-select {
35
+ user-select: none;
36
+ }
37
+
38
+ .link {
39
+ color: rgb(0, 0, 238);
40
+ text-decoration: none;
41
+ }
42
+
43
+ .btn {
44
+ position: relative;
45
+ cursor: pointer;
46
+ user-select: none;
47
+
48
+ &::after {
49
+ position: absolute;
50
+ content: '';
51
+ top: 0;
52
+ right: 0;
53
+ left: 0;
54
+ bottom: 0;
55
+ transition: background-color 0.1s;
56
+ }
57
+
58
+ &:hover::after {
59
+ background-color: rgba(0, 0, 0, 0.08);
60
+ }
61
+
62
+ &:active::after {
63
+ background-color: rgba(0, 0, 0, 0.12);
64
+ }
65
+ }
src/utils/qrSSE.ts ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { computed, reactive } from 'vue';
2
+
3
+ enum SSEEvent {
4
+ GENERATE = 'generate',
5
+ POLL = 'poll',
6
+ END = 'end',
7
+ }
8
+
9
+ interface SSEGenerateData {
10
+ code: number;
11
+ msg: string;
12
+ url: string;
13
+ key: string;
14
+ }
15
+
16
+ interface SSEPollData {
17
+ code: number;
18
+ msg: string;
19
+ cookie?: string;
20
+ }
21
+
22
+ enum PollQrResultCode {
23
+ SUCCESS = 0,
24
+ EXPIRED = 86038,
25
+ NOT_CONFIRMED = 86090,
26
+ NOT_SCANNED = 86101,
27
+ }
28
+
29
+ export enum QrStatus {
30
+ WAIT,
31
+ SCANNED,
32
+ EXPIRED,
33
+ SUCCESS,
34
+ ERROR,
35
+ }
36
+
37
+ interface QrState {
38
+ url: string;
39
+ cookie: string;
40
+ errMsg: string;
41
+ status: QrStatus;
42
+ }
43
+
44
+ const defaultState = (): QrState => ({
45
+ url: '',
46
+ cookie: '',
47
+ errMsg: '',
48
+ status: QrStatus.WAIT,
49
+ });
50
+
51
+ class QrSSE {
52
+ private es!: EventSource;
53
+
54
+ public constructor(private state: QrState) {
55
+ this.start();
56
+ }
57
+
58
+ public restart() {
59
+ Object.assign(this.state, defaultState());
60
+ this.start();
61
+ }
62
+
63
+ public stop() {
64
+ if (!this.es) return;
65
+ this.es.close();
66
+ }
67
+
68
+ private start() {
69
+ this.stop();
70
+ this.es = new EventSource('/api/qr');
71
+ this.es.addEventListener(SSEEvent.GENERATE, this.handleMessage);
72
+ this.es.addEventListener(SSEEvent.POLL, this.handleMessage);
73
+ this.es.addEventListener(SSEEvent.END, this.handleEnd);
74
+ }
75
+
76
+ private handleMessage = ({ type, data }: MessageEvent<string>) => {
77
+ const obj = JSON.parse(data);
78
+ console.log(type, obj);
79
+
80
+ switch (type) {
81
+ case SSEEvent.POLL:
82
+ this.handlePoll(obj);
83
+ break;
84
+ case SSEEvent.GENERATE:
85
+ this.handleGenerate(obj);
86
+ break;
87
+ }
88
+ };
89
+
90
+ private handleEnd = ({ data }: MessageEvent<string>) => {
91
+ if (data) this.handleError(data);
92
+ this.stop();
93
+ };
94
+
95
+ private handleError(msg: string) {
96
+ this.state.errMsg = msg;
97
+ this.state.status = QrStatus.ERROR;
98
+ return;
99
+ }
100
+
101
+ private handleGenerate({ code, msg, url }: SSEGenerateData) {
102
+ if (code !== 0) {
103
+ this.handleError(msg);
104
+ return;
105
+ }
106
+
107
+ this.state.url = url;
108
+ this.state.status = QrStatus.WAIT;
109
+ }
110
+
111
+ private handlePoll({ code, msg, cookie }: SSEPollData) {
112
+ switch (code) {
113
+ case PollQrResultCode.NOT_SCANNED:
114
+ this.state.status = QrStatus.WAIT;
115
+ break;
116
+ case PollQrResultCode.NOT_CONFIRMED:
117
+ this.state.status = QrStatus.SCANNED;
118
+ break;
119
+ case PollQrResultCode.EXPIRED:
120
+ this.state.status = QrStatus.EXPIRED;
121
+ break;
122
+ case PollQrResultCode.SUCCESS:
123
+ this.state.status = QrStatus.SUCCESS;
124
+ this.state.cookie = cookie!;
125
+ break;
126
+ default:
127
+ this.handleError(msg);
128
+ break;
129
+ }
130
+ }
131
+ }
132
+
133
+ export const useQrSSE = () => {
134
+ const state = reactive<QrState>(defaultState());
135
+ const getters = reactive({
136
+ statusText: computed(() => {
137
+ switch (state.status) {
138
+ case QrStatus.WAIT:
139
+ return '等待扫码';
140
+ case QrStatus.SCANNED:
141
+ return '已扫码,等待登录';
142
+ case QrStatus.EXPIRED:
143
+ return '二维码已过期,请刷新';
144
+ case QrStatus.SUCCESS:
145
+ return '登录成功';
146
+ default:
147
+ return state.errMsg;
148
+ }
149
+ }),
150
+ });
151
+
152
+ const qrSSE = new QrSSE(state);
153
+
154
+ return { state, getters, restart: qrSSE.restart.bind(qrSSE) };
155
+ };
src/utils/tipText.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ref } from 'vue';
2
+
3
+ export const useTipText = (initText: string) => {
4
+ const text = ref(initText);
5
+ let timer: NodeJS.Timeout | undefined;
6
+
7
+ const changeText = (tipText: string, timeout = 2000) => {
8
+ if (timer) clearTimeout(timer);
9
+ text.value = tipText;
10
+ timer = setTimeout(() => {
11
+ text.value = initText;
12
+ timer = undefined;
13
+ }, timeout);
14
+ };
15
+
16
+ return { text, changeText };
17
+ };
vercel.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "github": {
3
+ "silent": true
4
+ }
5
+ }