学习通精简版(基于 Vue3 + UniApp)

-项目介绍
这是一个基于 Vue3 和 UniApp 构建的纯前端项目,通过调用学习通(XXT)原生接口实现核心功能。项目旨在提供一个更简洁、高效的学习通替代方案,尤其针对高频使用的签到场景进行了极致优化。
项目仓库地址

项目特点

  • 界面简洁:去除原生应用中非必要的功能模块,仅保留核心学习功能,界面清爽,操作更便捷。
  • 体验优化:重构交互逻辑,提升日常使用流畅度与响应速度。
  • 功能增强:内置多个实用且有趣的“神秘小功能”,提升学习效率与使用乐趣。
  • 纯前端实现:不依赖后端服务,安全可靠,易于部署与维护,数据完全本地存储。
  • 跨平台支持:基于 UniApp 框架,可编译至多端(如微信小程序、H5、App等),适配性强。
  • 隐私优先:所有用户信息(如账号、课程数据)仅保存在本地设备,绝不上传或收集,真正做到“你的数据你做主”。

核心功能模块

📱 主学习功能

  • 课程管理:清晰展示已加入课程,快速跳转学习页面。
  • 作业提交:集中查看待办作业,一键进入提交界面。
  • 资料查阅:便捷访问课件、文档等学习资源。
  • 消息通知:整合关键通知,避免信息遗漏。

🚀 高频场景优化 - 扫码签到助手

专为解决“扫码签到”这一高频、紧急场景设计,提供极速体验:

  • 📷 实时扫码签到
    调用手机摄像头,快速识别学习通二维码,一键跳转签到确认页。
  • 📱 移动端深度优化
    界面极简,启动快,扫码流程仅需 2-3 步,大幅缩短操作时间。
  • 🔐 安全与透明
    无敏感权限请求(除相机外),代码开源可审计,运行过程无后台数据传输。

使用说明

经实际测试,本项目在日常学习场景中可完全替代官方学习通应用,满足课程学习、作业处理及快速签到等核心需求。尤其在需要争分夺秒完成扫码签到的场景下,本工具能显著提升成功率与使用体验。

提示:建议在光线充足环境下使用扫码功能,以获得最佳识别速度与准确率。

代码展示

项目采用模块化设计,结构清晰,便于维护与功能扩展。

班级界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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
169
170
171
172
173
174
175
176
177
178
179
180
181
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
<template>
<div class="class-view">

<!-- 班级列表 -->
<div class="section">
<!-- 使用原生 view 替代 van-list -->
<div class="class-list">
<div v-for="item in filteredCourses" :key="item.clazzId" class="class-item" @click="toClassActive(item)">
<!-- 图标区域 -->
<div class="class-icon">
<image :src="item.imageUrl" class="round-img" mode="aspectFit"></image>
</div>

<!-- 文字内容 -->
<div class="class-content">
<div class="class-title">{{ item.courseName }}</div>
<div class="class-id">教师: {{ item.teacher}}</div>
</div>


</div>
</div>
</div>
</div>
</template>

<script setup>
import {
showToast
} from 'vant'
import {
ref,
onMounted,
computed,
} from 'vue'
import {
onLoad,
onPullDownRefresh
} from '@dcloudio/uni-app'
import CryptoJS from 'crypto-js'
const list = ref([])

function encryptByAES(message) {
const chaoxingkey = 'u2oh6Vu^HWe4_AES'
const utf8Key = CryptoJS.enc.Utf8.parse(chaoxingkey)
const utf8Iv = CryptoJS.enc.Utf8.parse(chaoxingkey)


const encrypted = CryptoJS.AES.encrypt(message, utf8Key, {
iv: utf8Iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
// 关键:避免 CryptoJS 自动派生密钥
})

// 返回原始密文的 Base64
return encrypted.ciphertext.toString(CryptoJS.enc.Base64)
}
onPullDownRefresh(async () => {
console.log('触发下拉刷新')
let coooo = uni.getStorageSync('CHAOXING_COOKIE');
await fetchData(coooo)
uni.showToast({
icon: 'success',
title: '刷新成功'
})
uni.stopPullDownRefresh() // 停止刷新动画
})






const getcookie = (username, password) => {
return new Promise((resolve, reject) => {
const eusername = encryptByAES(username);
const epassword = encryptByAES(password);
console.log('加密后的用户名:', eusername);
console.log('加密后的密码:', epassword);
uni.request({
url: 'https://passport2.chaoxing.com/fanyalogin',
method: 'POST',
data: {
'fid': '-1',
'uname': eusername,
'password': epassword,
'refer': 'http://i.mooc.chaoxing.com',
't': 'true',
'forbidotherlogin': '0',
'validate': '',
'doubleFactorLogin': '0',
'independentId': '0',
'independentNameId': '0'
},
header: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
success: (res) => {
const setCookie = res.cookies;
if (!setCookie || setCookie.length === 0) {
return reject('登录失败,未返回 Cookie');
}

const cookieStr = setCookie
.map(cookie => {
const trimmed = cookie.trim();
const end = trimmed.indexOf(';');
return end > -1 ? trimmed.substring(0, end) : trimmed;
})
.filter(Boolean)
.join('; ');
uni.setStorageSync('CHAOXING_COOKIE', cookieStr);
// ✅ 关键:用 resolve 返回结果
resolve(cookieStr);
},
fail: (err) => {
reject(err);
}
});
});
};

const fetchData = (coo) => {
uni.request({
url: 'http://mooc1-api.chaoxing.com/mycourse/backclazzdata',
header: {
'Cookie': coo
},
success: (res) => {
list.value = res.data;
console.log('✅ 请求成功', res.data);
},
fail(err) { // ✅ 加上 async
console.log('❌ 请求失败,准备重试...', err);
uni.showToast({
title: '请求失败.',
icon: 'none',
duration: 2000
});
}
});
};


function isTokenExpired(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]))
const exp = payload.exp * 1000 // 转为毫秒
return Date.now() > exp
} catch (e) {
console.error('Token 解码失败', e)
return true // 解码失败视为已过期
}
}



// 筛选并格式化课程数据
const filteredCourses = computed(() => {
if (!list.value?.channelList) return []

return list.value.channelList
.filter(item => {
// 只保留包含课程数据的项
return (
item.content?.course?.data &&
Array.isArray(item.content.course.data) &&
item.content.course.data.length > 0
)
})
.map(item => {
const content = item.content
const course = content.course.data[0] // 取第一个课程

return {
clazzId: String(content.id), // 班级 ID
courseId: String(course.id), // 课程 ID
courseName: course.name, // 课程名称
teacher: course.teacherfactor, // 授课教师
imageUrl: course.imageurl // 加入的图片 URL
}
})
})















// 当前激活的底部 tab
const activeTab = ref('class')

// 跳转到班级详情页
const toClassActive = (item) => {
uni.navigateTo({
url: `/pages/ClassActive?courseId=${item.courseId}&clazzId=${item.clazzId}`

})
}


onMounted(
async () => {

let cookie = uni.getStorageSync('CHAOXING_COOKIE');
const pAuthToken = cookie.match(/p_auth_token=([^;]+)/)?.[1]
if (!cookie) {
uni.showToast({
icon: 'none',
title: '请先在【账号管理】中添加账号,重启应用或下拉刷新即可查看',
duration: 2500
})
}
if (isTokenExpired(pAuthToken) || !cookie) {
const data = uni.getStorageSync('accountList')
if (!data?.[0]?.phone) {
return
}

const {
phone: user,
password: word
} = data[0]
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('登录超时')), 2500)
)

try {
const newcookie = await Promise.race([
getcookie(user, word),
timeout
])

uni.setStorageSync('CHAOXING_COOKIE', newcookie)
cookie = newcookie
} catch (e) {
uni.showToast({
icon: 'none',
title: '登录失败'
})
return
}

} else {
console.log('登录有效')
}
console.log('当前cookie:', cookie)
if (cookie) {
fetchData(cookie)
}
activeTab.value = 'class'
})
</script>


扫码界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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
169
170
171
172
173
174
175
176
177
178
179
180
181
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313

<script setup>
import {
ref,
computed,
onMounted
} from 'vue'
import {
onPullDownRefresh
} from '@dcloudio/uni-app'
import CryptoJS from 'crypto-js'
const text = ref('')
const mise = ref([])

// 账号列表
const accounts = ref([])

// 选中的账号 ID 列表
const selectedIds = ref([])
const msg = ref('')

function encryptByAES(message) {
const chaoxingkey = 'u2oh6Vu^HWe4_AES'
const utf8Key = CryptoJS.enc.Utf8.parse(chaoxingkey)
const utf8Iv = CryptoJS.enc.Utf8.parse(chaoxingkey)


const encrypted = CryptoJS.AES.encrypt(message, utf8Key, {
iv: utf8Iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
// 关键:避免 CryptoJS 自动派生密钥
})

// 返回原始密文的 Base64
return encrypted.ciphertext.toString(CryptoJS.enc.Base64)
}







const getcookie = (username, password) => {
return new Promise((resolve, reject) => {
const eusername = encryptByAES(username);
const epassword = encryptByAES(password);
console.log('加密后的用户名:', eusername);
console.log('加密后的密码:', epassword);
uni.request({
url: 'https://passport2.chaoxing.com/fanyalogin',
method: 'POST',
data: {
'fid': '-1',
'uname': eusername,
'password': epassword,
'refer': 'http://i.mooc.chaoxing.com',
't': 'true',
'forbidotherlogin': '0',
'validate': '',
'doubleFactorLogin': '0',
'independentId': '0',
'independentNameId': '0'
},
header: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
success: (res) => {
const setCookie = res.cookies;
if (!setCookie || setCookie.length === 0) {
return reject('登录失败,未返回 Cookie');
}

const cookieStr = setCookie
.map(cookie => {
const trimmed = cookie.trim();
const end = trimmed.indexOf(';');
return end > -1 ? trimmed.substring(0, end) : trimmed;
})
.filter(Boolean)
.join('; ');


resolve(cookieStr);
},
fail: (err) => {
reject(err);
}
});
});
};



// 当前选中的账号对象列表
const selectedAccounts = computed(() =>
accounts.value.filter(account => selectedIds.value.includes(account.id))
)

// 扫码状态与结果
const scanResult = ref('')
const query = ref({})
const isScanning = ref(false)

// 切换账号选中状态
const toggleAccount = (id) => {
const index = selectedIds.value.indexOf(id)
if (index === -1) {
selectedIds.value.push(id)
} else {
selectedIds.value.splice(index, 1)
}
}
const getQueryParams = (url) => {
// 1. 安全处理:转字符串,防止 null/undefined
const str = String(url || '')

// 2. 使用正则提取 ? 后面的查询字符串(忽略 #hash)
const match = str.match(/\?([^#]*)/)
if (!match) return {}

const queryStr = match[1]
const params = {}

queryStr.split('&').forEach((pair) => {
if (!pair) return

const [key, value] = pair.split('=')
if (!key) return

try {
const decodedKey = decodeURIComponent(key.trim())
const decodedValue = value ? decodeURIComponent(value) : ''
params[decodedKey] = decodedValue
} catch (e) {
params[key.trim()] = value || ''
}
})

return params
}
const handleScanmini = async (Ids, resu) => {
const promises = Ids.map(id => { // 注意:这里不需要 async
return new Promise(async (resolve) => { // 外层 Promise 用于 resolve 结果
try {
const query = getQueryParams(resu);
console.log(query)
const coo = await getcookie(accounts.value.find(acc => acc.id === id).phone, accounts.value
.find(acc => acc.id === id).password);
const requrl =
`https://mobilelearn.chaoxing.com/pptSign/stuSignajax?activeId=${query.id}&enc=${query.enc}`;
// ✅ 关键:在 uni.request 的回调中 resolve
uni.request({
url: requrl,
method: 'GET',
header: {
'Cookie': coo
},
success: (res) => {
const message = res.data || '无数据';

resolve({
success: true,
id,
message,
});
},
fail: (err) => {
const errorMsg = `请求失败:${err.errMsg}`;
resolve({
success: false,
id,
error: errorMsg
});
}
});
} catch (error) {
// 获取 cookie 失败
const errorMsg = `登录失败:${error.message}`;
uni.showToast({
title: `账号${id}:${errorMsg}`,
icon: 'none'
});
resolve({
success: false,
id,
error: errorMsg
});
}
});
});

// ✅ 此时 Promise.all 会等待所有 uni.request 完成
return await Promise.all(promises);
};

// 扫码操作
const handleScan = (ids) => {
if (selectedAccounts.value.length === 0) {
uni.showToast({
title: '请先选择至少一个账号',
icon: 'none'
})
return
}
isScanning.value = true

uni.scanCode({
onlyFromCamera: true,
scanType: ['qrCode'],
success: async ({
result
}) => {
const pmise = await handleScanmini(ids, result)
console.log('mise:', pmise)
mise.value = pmise
scanResult.value = true
uni.showToast({
title: '扫码完成',
icon: 'success'
})
},
fail: (err) => {
let msg = '扫码失败'
if (err.errMsg.includes('cancel')) msg = '用户取消'
else if (err.errMsg.includes('auth')) msg = '相机权限被拒绝'
uni.showToast({
title: msg,
icon: 'none'
})
},
complete: () => {
isScanning.value = false
}
})
}
const loadAccountList = () => {
try {
const data = uni.getStorageSync('accountList')
if (data) {
accounts.value = data

}
} catch (e) {
console.error('读取账号列表失败:', e)
}
}
onPullDownRefresh(async () => {
console.log('触发下拉刷新')
loadAccountList()
uni.stopPullDownRefresh() // 停止刷新动画
})

onMounted(() => {
loadAccountList()
})
</script>

<template>
<view class="container">
<!-- 页面标题 -->
<view class="header">
<text class="title">扫码账号</text>
</view>

<!-- 账号列表 -->
<view class="account-list">
<view v-for="account in accounts" :key="account.id" class="account-item">
<label class="account-label" @click="toggleAccount(account.id)">
<!-- 自定义复选框 -->
<view class="custom-checkbox" :class="{ checked: selectedIds.includes(account.id) }">
<text v-if="selectedIds.includes(account.id)" class="icon-check">✔</text>
</view>
<!-- 账号信息 -->
<text class="account-name">{{ account.name }}</text>
</label>
</view>
</view>

<!-- 已选提示 -->
<view class="selected-tip">
<text v-if="selectedAccounts.length > 0">已选择 {{ selectedAccounts.length }} 个账号</text>
<text v-else>暂未选择账号</text>
</view>

<!-- 扫码按钮 -->
<view class="action-section">
<button class="scan-btn" :disabled="selectedAccounts.length === 0" :loading="isScanning"
@click="handleScan(selectedIds)">
{{ isScanning ? '扫描中...' : '扫码获取信息' }}
</button>
</view>
<view>{{text}}</view>

<!-- 扫码结果 -->
<view v-if="scanResult" class="result-section">
<view>签到结果</view>
<view v-for="(item, index) in mise" :key="index">
<view> {{ accounts.find(acc => acc.id === item.id)?.name }}-{{ item.message }}</view>
</view>
</view>
</view>
</template>








班级活动页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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
169
170
171
172
173
174
175
176
177
178
179
180
181
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
<template>
<div class="class-view">

<!-- 班级列表 -->
<div class="section">
<!-- 使用原生 view 替代 van-list -->
<div class="class-list">
<div v-for="item in filteredCourses" :key="item.clazzId" class="class-item" @click="toClassActive(item)">
<!-- 图标区域 -->
<div class="class-icon">
<image :src="item.imageUrl" class="round-img" mode="aspectFit"></image>
</div>

<!-- 文字内容 -->
<div class="class-content">
<div class="class-title">{{ item.courseName }}</div>
<div class="class-id">教师: {{ item.teacher}}</div>
</div>


</div>
</div>
</div>
</div>
</template>

<script setup>
import {
showToast
} from 'vant'
import {
ref,
onMounted,
computed,
} from 'vue'
import {
onLoad,
onPullDownRefresh
} from '@dcloudio/uni-app'
import CryptoJS from 'crypto-js'
const list = ref([])

function encryptByAES(message) {
const chaoxingkey = 'u2oh6Vu^HWe4_AES'
const utf8Key = CryptoJS.enc.Utf8.parse(chaoxingkey)
const utf8Iv = CryptoJS.enc.Utf8.parse(chaoxingkey)


const encrypted = CryptoJS.AES.encrypt(message, utf8Key, {
iv: utf8Iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
// 关键:避免 CryptoJS 自动派生密钥
})

// 返回原始密文的 Base64
return encrypted.ciphertext.toString(CryptoJS.enc.Base64)
}
onPullDownRefresh(async () => {
console.log('触发下拉刷新')
let coooo = uni.getStorageSync('CHAOXING_COOKIE');
await fetchData(coooo)
uni.showToast({
icon: 'success',
title: '刷新成功'
})
uni.stopPullDownRefresh() // 停止刷新动画
})






const getcookie = (username, password) => {
return new Promise((resolve, reject) => {
const eusername = encryptByAES(username);
const epassword = encryptByAES(password);
console.log('加密后的用户名:', eusername);
console.log('加密后的密码:', epassword);
uni.request({
url: 'https://passport2.chaoxing.com/fanyalogin',
method: 'POST',
data: {
'fid': '-1',
'uname': eusername,
'password': epassword,
'refer': 'http://i.mooc.chaoxing.com',
't': 'true',
'forbidotherlogin': '0',
'validate': '',
'doubleFactorLogin': '0',
'independentId': '0',
'independentNameId': '0'
},
header: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
success: (res) => {
const setCookie = res.cookies;
if (!setCookie || setCookie.length === 0) {
return reject('登录失败,未返回 Cookie');
}

const cookieStr = setCookie
.map(cookie => {
const trimmed = cookie.trim();
const end = trimmed.indexOf(';');
return end > -1 ? trimmed.substring(0, end) : trimmed;
})
.filter(Boolean)
.join('; ');
uni.setStorageSync('CHAOXING_COOKIE', cookieStr);
// ✅ 关键:用 resolve 返回结果
resolve(cookieStr);
},
fail: (err) => {
reject(err);
}
});
});
};

const fetchData = (coo) => {
uni.request({
url: 'http://mooc1-api.chaoxing.com/mycourse/backclazzdata',
header: {
'Cookie': coo
},
success: (res) => {
list.value = res.data;
console.log('✅ 请求成功', res.data);
},
fail(err) { // ✅ 加上 async
console.log('❌ 请求失败,准备重试...', err);
uni.showToast({
title: '请求失败.',
icon: 'none',
duration: 2000
});
}
});
};


function isTokenExpired(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]))
const exp = payload.exp * 1000 // 转为毫秒
return Date.now() > exp
} catch (e) {
console.error('Token 解码失败', e)
return true // 解码失败视为已过期
}
}



// 筛选并格式化课程数据
const filteredCourses = computed(() => {
if (!list.value?.channelList) return []

return list.value.channelList
.filter(item => {
// 只保留包含课程数据的项
return (
item.content?.course?.data &&
Array.isArray(item.content.course.data) &&
item.content.course.data.length > 0
)
})
.map(item => {
const content = item.content
const course = content.course.data[0] // 取第一个课程

return {
clazzId: String(content.id), // 班级 ID
courseId: String(course.id), // 课程 ID
courseName: course.name, // 课程名称
teacher: course.teacherfactor, // 授课教师
imageUrl: course.imageurl // 加入的图片 URL
}
})
})















// 当前激活的底部 tab
const activeTab = ref('class')

// 跳转到班级详情页
const toClassActive = (item) => {
uni.navigateTo({
url: `/pages/ClassActive?courseId=${item.courseId}&clazzId=${item.clazzId}`

})
}


onMounted(
async () => {

let cookie = uni.getStorageSync('CHAOXING_COOKIE');
const pAuthToken = cookie.match(/p_auth_token=([^;]+)/)?.[1]
if (!cookie) {
uni.showToast({
icon: 'none',
title: '请先在【账号管理】中添加账号,重启应用或下拉刷新即可查看',
duration: 2500
})
}
if (isTokenExpired(pAuthToken) || !cookie) {
const data = uni.getStorageSync('accountList')
if (!data?.[0]?.phone) {
return
}

const {
phone: user,
password: word
} = data[0]
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('登录超时')), 2500)
)

try {
const newcookie = await Promise.race([
getcookie(user, word),
timeout
])

uni.setStorageSync('CHAOXING_COOKIE', newcookie)
cookie = newcookie
} catch (e) {
uni.showToast({
icon: 'none',
title: '登录失败'
})
return
}

} else {
console.log('登录有效')
}
console.log('当前cookie:', cookie)
if (cookie) {
fetchData(cookie)
}
activeTab.value = 'class'
})
</script>

应用界面展示

以下是应用的实际运行界面截图:

应用界面1
应用界面2
应用界面3
应用界面4