New Huajishe Check ChaoXing

This commit is contained in:
e2hang
2025-10-01 10:01:52 +08:00
parent 240b884eac
commit 80be8ae3cf
1094 changed files with 61709 additions and 0 deletions

View File

@@ -0,0 +1,275 @@
import config from '@utils/config';
import util from '@utils/util';
import API from '@utils/api';
import log from '@utils/log';
let api = null;
Page({
data: {
tab: 'login', // login / courses / activities / signin
typeDefine: ["拍照签到", "普通签到", "二维码签到", "手势签到", "位置签到", "签到码签到"],
swiperList: (config.swiperList || [
"/swiper/1.png",
"/swiper/2.png",
"/swiper/3.png",
"/swiper/4.png",
]).map(item => `https://testingcf.jsdelivr.net/gh/${config.repository}@main/resource${item}`),
repository: config.repository,
},
onLoad(options) {
this.setData({
'username': util.getStorage('username'),
'password': util.getStorage('password'),
}, () => {
this.setData(Object.assign(this.data, options))
})
},
onUnload() {
util.setStorage('username', this.data.username);
util.setStorage('password', this.data.password);
},
input(e) { // 输入绑定
this.setData({
[e.currentTarget.dataset.input]: e.detail.value.trim(),
})
},
login() { // 账号登录
const username = this.data.username;
const password = this.data.password;
if (!username || !password) {
util.showInfo("帐号或密码不能为空!")
return;
}
api = new API(username, password);
api.login()
.then(res => {
util.showInfo(res.mes);
if (res.status) {
api.getUserInfo()
.then(userinfo => {
util.showInfo("登录成功")
this.setData({
'userinfo': userinfo,
'tab': 'courses',
})
this.options.username = username;
this.options.password = password;
this.onUnload(); // 缓存
this.get_courses(); // 获取课程
})
}
})
.catch(e => {
util.showInfo("网络不稳定 请稍后再试");
log.error("登录失败", e)
})
},
get_courses() { // 获取课程列表
api.getCourse()
.then(courses => {
this.setData({
'courses': courses,
})
util.showInfo("获取课程成功")
})
.catch(e => {
util.showInfo("获取课程失败,请重试")
log.error("获取课程失败", e)
})
},
get_activities(e) { // 获取签到列表
const item = e.currentTarget.dataset.item;
api.getActivity(item.courseId, item.classId)
.then(activities => {
this.setData({
'activities': activities.slice(0, 20),
'tab': 'activities',
})
util.showInfo("获取活动成功");
})
.catch(e => {
util.showInfo("获取活动失败,请重试")
log.error("获取活动失败", e)
})
},
async to_signin(e) { // 进入签到页
const item = e.currentTarget.dataset.item;
this.getUserLocation()
api = new API(this.data.username, this.data.password);
api.beforeSign(item.activeId, item.courseId, item.classId);
this.data.hasValidate = await api.hasValidate(item.activeId); // 是否有滑块验证码
this.data.token = ([0, 1].includes(item.type)) ? await api.getToken() : ''; // 超星云盘token
api.getActivityInfo(item.activeId) // 获取签到活动信息
.then(info => this.setData({
'info': Object.assign(info, item),
'info.type': info.otherId || (1 - info.ifphoto), // 是否为拍照签到
'tab': 'signin',
}))
},
getUserLocation() { // 获取用户位置
wx.getLocation({
'type': 'gcj02',
})
.then(gcj02 => {
log.info("获取用户位置", gcj02)
API.allToBaidu(gcj02.longitude, gcj02.latitude)
.then(bd09ll => this.setData({
'location.longitude': bd09ll.x,
'location.latitude': bd09ll.y,
}))
API.getAddressText(gcj02.longitude, gcj02.latitude)
.then(text => this.setData({
'location.name': text,
}))
})
.catch(e => {
util.showInfo("用户拒绝定位")
log.debug("用户拒绝定位", e)
})
},
chooseLocation() { // 选择位置
wx.chooseLocation(this.data.location) // 国测局坐标系 gcj02
.then(gcj02 => {
log.info("用户选择位置", gcj02)
API.allToBaidu(gcj02.longitude, gcj02.latitude)
.then(bd09ll => this.setData({
'location.latitude': bd09ll.y,
'location.longitude': bd09ll.x,
'location.name': gcj02.name || gcj02.address,
}))
})
.catch(e => {
util.showInfo("取消位置选择")
log.debug("取消位置选择", e)
})
},
async signin(e) { // 提交签到
const objectId = (this.data.srcList || []).length ? this.data.srcList[0] : "";
const location = this.data.location;
const info = this.data.info;
const userinfo = this.data.userinfo;
let res = "";
if (this.data.hasValidate)
this.data.validate = ""; // 课后作业:此处请自行实现 GET请求 https://cx.micono.eu.org/api/validate
if (info.type == 0 || info.type == 1) { // 图片/普通
if (objectId == 0 && info.type == 0) {
const ok = await wx.showModal({
title: '确认直接签到吗?',
content: '你还没有上传图片',
})
if (ok.confirm != true)
return;
}
res = await api.defaultSign(info.activeId, objectId, null, null, null, null, null, null, userinfo.name, this.data.validate);
}
if (info.type == 3 || info.type == 5) { // 签到码/手势
res = await api.defaultSign(info.activeId, null, null, null, null, this.data.signCode, null, null, userinfo.name, this.data.validate);
}
if (info.type == 4) { // 位置
res = await api.defaultSign(info.activeId, null, location.longitude, location.latitude, location.name, null, null, null, userinfo.name, this.data.validate);
}
if (info.type == 2) { // 二维码
const qrcode = await wx.scanCode();
log.debug("扫码结果", qrcode)
let params = {};
qrcode.result.split('?')[1].split('&').forEach(param => {
const parts = param.split('=');
const key = decodeURIComponent(parts[0]);
const value = decodeURIComponent(parts[1]);
params[key] = value;
});
const enc = params.enc;
res = await this.data.api.defaultSign(info.activeId, null, location.longitude, location.latitude, location.name, null, null, enc, userinfo.name, this.data.validate);
}
const msg = API.getResult(res);
util.showInfo(msg, 'none', true);
this.setData({
'result': msg,
})
},
handleAdd(e) { // 上传图片
const files = e.detail.files;
const token = this.data.token;
const fileList = this.data.fileList || [];
files.forEach(file => {
this.setData({
'fileList': [...fileList, {
...file,
'status': 'loading'
}],
'srcList': this.data.srcList || [],
});
const length = fileList.length;
const task = wx.uploadFile({
url: `https://pan-yz.chaoxing.com/upload?_token=${token}`,
filePath: file.url,
name: 'file',
formData: {
'puid': this.data.userinfo.uid
},
success: res => {
const data = JSON.parse(res.data);
if (data.result) {
log.debug("图片上传结果", data)
util.showInfo("图片上传成功")
this.setData({
[`fileList[${length}].status`]: 'done',
[`srcList[${length}]`]: data.objectId,
});
} else {
util.showInfo(data.msg)
this.handleRemove()
}
},
});
task.onProgressUpdate((res) => {
this.setData({
[`fileList[${length}].percent`]: Math.floor(res.progress * 0.99),
});
});
})
},
handleRemove(e) { // 删除图片
const index = e?.detail?.index || this.data.fileList.length - 1;
this.data.fileList.splice(index, 1);
this.data.srcList.splice(index, 1);
this.setData({
'fileList': this.data.fileList,
'srcList': this.data.srcList,
});
},
back() { // 返回
const tabs = ["login", "courses", "activities", "signin"];
this.setData({
'tab': tabs[(tabs.indexOf(this.data.tab) || 1) - 1],
})
},
onShareAppMessage() { // 分享
return {
'title': '学习通签到助手',
'imageUrl': '/static/image/share.png',
}
}
})

View File

@@ -0,0 +1,28 @@
{
"usingComponents": {
"t-swiper": "tdesign-miniprogram/swiper/swiper",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-switch": "tdesign-miniprogram/switch/switch",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-upload": "tdesign-miniprogram/upload/upload",
"t-notice-bar": "tdesign-miniprogram/notice-bar/notice-bar",
"t-button": "tdesign-miniprogram/button/button",
"t-radio": "tdesign-miniprogram/radio/radio",
"t-navbar": "tdesign-miniprogram/navbar/navbar",
"t-radio-group": "tdesign-miniprogram/radio-group/radio-group",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-picker": "tdesign-miniprogram/picker/picker",
"t-picker-item": "tdesign-miniprogram/picker-item/picker-item",
"t-popup": "tdesign-miniprogram/popup/popup",
"t-checkbox": "tdesign-miniprogram/checkbox/checkbox",
"t-checkbox-group": "tdesign-miniprogram/checkbox-group/checkbox-group",
"t-fab": "tdesign-miniprogram/fab/fab",
"t-input": "tdesign-miniprogram/input/input",
"t-image": "tdesign-miniprogram/image/image",
"t-search": "tdesign-miniprogram/search/search",
"t-divider": "tdesign-miniprogram/divider/divider",
"t-dropdown-menu": "tdesign-miniprogram/dropdown-menu/dropdown-menu",
"t-dropdown-item": "tdesign-miniprogram/dropdown-item/dropdown-item",
"no-data": "/components/no-data/no-data"
}
}

View File

@@ -0,0 +1,156 @@
<t-navbar title="学习助手" t-class-title="nav-title"></t-navbar>
<!-- 登录页 -->
<view wx:if="{{tab=='login'}}">
<view class="swiper shadow-radius">
<t-swiper duration="1000" height="300rpx" interval="3000" list="{{swiperList}}" navigation="{{ {type: 'dots-bar'} }}"></t-swiper>
</view>
<view class="container shadow-radius">
<view class="input">
<image class="input-icon" src="/static/svg/icon/username.svg" mode="aspectFill"></image>
<input class="input-value" value="{{username}}" type="number" placeholder="帐号 (手机号)" bindinput="input" data-input="username"></input>
</view>
<view class="input">
<image class="input-icon" src="/static/svg/icon/password.svg" mode="aspectFill"></image>
<input class="input-value" value="{{password}}" type="text" placeholder="密码" bindinput="input" data-input="password" bindconfirm="login"></input>
</view>
<view class="login-button">
<t-button block theme="primary" size="large" variant="outline" open-type="share">分享</t-button>
<t-button block theme="primary" size="large" bindtap="login">登录</t-button>
</view>
</view>
<view class="license">
<view>本小程序使用 AGPLv3 协议</view>
</view>
</view>
<!-- 课程列表页 -->
<view wx:if="{{tab=='courses'}}">
<view class="container shadow-radius">
<view class="course-item" wx:for="{{courses}}" wx:key="index" bindtap="get_activities" data-item="{{item}}">
<view class="course-item-image">
<t-image src="{{item.img}}" width="60" height="60" shape="round" mode="aspectFill"></t-image>
</view>
<view class="course-item-info {{item.folder?'inside':''}}">
<text class="course-item-title">{{item.courseName}}</text>
<text class="course-item-other">教师:{{item.teacherName}}</text>
<text class="course-item-other">班级:{{item.className}}</text>
<text class="course-item-other" wx:if="{{item.folder}}">文件夹:{{item.folder}}</text>
<text class="course-item-other" wx:if="{{item.isTeach}}">身份:教师/助教</text>
</view>
<t-divider></t-divider>
</view>
<view wx:if="{{!courses || courses.length==0}}">
<no-data text="暂无签到/活动"></no-data>
</view>
</view>
</view>
<!-- 签到列表页 -->
<view wx:if="{{tab=='activities'}}">
<view class="container shadow-radius">
<view class="course-item" wx:for="{{activities}}" wx:key="index" bindtap="to_signin" data-item="{{item}}">
<view class="course-item-image">
<t-image src="{{item.img}}" width="60" height="60" shape="round" mode="aspectFill"></t-image>
</view>
<view class="course-item-info {{item.folder?'inside':''}}">
<text class="course-item-title">{{typeDefine[item.type]}}</text>
<text class="course-item-other">截止时间:{{item.time}}</text>
<text class="course-item-other">签到名称:{{item.name}}</text>
</view>
<t-divider></t-divider>
</view>
<view wx:if="{{!courses || courses.length==0}}">
<no-data text="暂无签到/活动"></no-data>
</view>
</view>
</view>
<!-- 签到页 -->
<view wx:if="{{tab=='signin'}}">
<view class="container shadow-radius">
<view class="container-text">
<view class="title">
<text class="title-text">{{typeDefine[info.type]}}</text>
</view>
<view class="info-item">
<view class="info-tag">签到名</view>
<view class="info-content">{{info.name}}</view>
</view>
<view class="info-item">
<view class="info-tag">已签人数</view>
<view class="info-content">{{info.attendNum}}</view>
</view>
<view class="info-item">
<view class="info-tag">活动ID</view>
<view class="info-content">{{info.activeId}}</view>
</view>
<view class="info-item">
<view class="info-tag">结束时间</view>
<view class="info-content">{{info.endtimeStr}}</view>
</view>
<view class="info-item">
<view class="info-tag">签到结果</view>
<view class="info-content result">{{result}}</view>
</view>
</view>
</view>
<!-- 普通 -->
<view wx:if="{{info.type==1}}">
<view class="button-box">
<t-button block t-class="shadow-radius" theme="primary" size="large" bindtap="signin">一键签到</t-button>
</view>
</view>
<!-- 拍照 -->
<view wx:if="{{info.type==0}}">
<view class="image-box shadow-radius">
<t-upload grid-config="{{ {width: 110, height: 110, column: 5} }}" mediaType="{{['image']}}" source="{{uploadMode}}" max="1" files="{{fileList}}" bind:add="handleAdd" bind:remove="handleRemove"></t-upload>
</view>
<view class="button-box">
<t-button block t-class="shadow-radius" theme="primary" size="large" bindtap="signin">立即签到</t-button>
</view>
</view>
<!-- 签到码/手势 -->
<view wx:if="{{info.type==3 || info.type==5}}">
<view class="input-box shadow-radius">
<view class="signcode-info"><text space="emsp">提示签到码与手势签到的区别请参考数字9键</text></view>
<t-input type="number" label="签到码" value="{{signCode}}" placeholder="签到码/手势" bind:change="input" data-input="signCode" bind:blur="checkSignCode" suffixIcon="{{signCodeIcon}}"></t-input>
</view>
<view class="button-box">
<t-button block t-class="shadow-radius" theme="primary" size="large" bindtap="signin" disabled="{{signCodeIcon!='check-circle'}}">{{signCodeIcon!='check-circle'?'请先输入':'立即签到'}}</t-button>
</view>
</view>
<!-- 位置 -->
<view wx:if="{{info.type==4}}">
<view class="input-box shadow-radius">
<t-input type="digit" label="纬度" value="{{location.latitude}}" placeholder="点击下方地图选点" bind:change="input" data-input="location.latitude"></t-input>
<t-input type="digit" label="经度" value="{{location.longitude}}" placeholder="点击下方地图选点" bind:change="input" data-input="location.longitude"></t-input>
<t-input label="位置" value="{{location.name}}" placeholder="教师端可见,留空时不上传位置" bind:change="input" data-input="location.name"></t-input>
</view>
<view class="button-box">
<t-button block t-class="shadow-radius" theme="light" size="large" bindtap="chooseLocation">地图选点</t-button>
<t-button block t-class="shadow-radius" theme="primary" size="large" bindtap="signin">立即签到</t-button>
</view>
</view>
<!-- 二维码 -->
<view wx:if="{{info.type==2}}">
<view class="input-box shadow-radius">
<t-input type="digit" label="纬度" value="{{location.latitude}}" placeholder="点击下方地图选点" bind:change="input" data-input="location.latitude"></t-input>
<t-input type="digit" label="经度" value="{{location.longitude}}" placeholder="点击下方地图选点" bind:change="input" data-input="location.longitude"></t-input>
<t-input label="位置" value="{{location.name}}" placeholder="教师端可见,留空时不上传位置" bind:change="input" data-input="location.name"></t-input>
</view>
<view class="button-box">
<t-button block t-class="shadow-radius" theme="light" size="large" bindtap="chooseLocation">地图选点</t-button>
<t-button block t-class="shadow-radius" theme="primary" size="large" bindtap="signin">扫码签到</t-button>
</view>
</view>
</view>
<!-- 页尾 -->
<t-fab icon="rollback" bind:click="back" aria-label="返回" wx:if="{{tab!='login'}}"></t-fab>
<view style="height: 200rpx"></view>

View File

@@ -0,0 +1,172 @@
/* 公用样式 */
.container {
margin: 30rpx;
min-height: 100rpx;
padding: 20rpx;
background: white;
}
/* 登录页 */
.license {
text-align: center;
color: gray;
position: fixed;
bottom: 30rpx;
width: 100vw;
font-size: 24rpx;
}
.swiper {
margin: 30rpx;
height: 300rpx;
}
.input {
width: 600rpx;
height: 100rpx;
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: flex-start;
align-items: center;
padding: 0px 0px 0px 32rpx;
gap: 16px;
background: #f6f6f6;
border: 0.5px solid #e4e4e4;
border-radius: 15rpx;
margin: 25rpx auto;
position: relative;
}
.input-icon {
width: 40rpx;
height: 40rpx;
}
.input-value {
width: 340rpx;
text-align: left;
font-size: 30rpx;
margin: 0rpx 50rpx;
background-color: rgb(0, 0, 0, 0);
}
.login-button {
width: 600rpx;
display: flex;
justify-content: space-between;
margin: 20rpx auto;
gap: 40rpx;
}
/* 课程列表页 */
.course-item {
min-height: 100rpx;
background: white;
padding: 30rpx;
display: flex;
position: relative;
}
.inside {
opacity: 0.5;
}
.course-item-image {
width: 60px;
height: 60px;
}
.course-item-info {
margin: 0rpx 20rpx;
max-width: 480rpx;
}
.course-item-title {
font-size: 30rpx;
display: block;
margin-bottom: 10rpx;
color: black;
}
.course-item-other {
color: #535353;
font-size: 24rpx;
display: block;
}
/* 签到页 */
.container-text {
margin: 30rpx 45rpx;
position: relative;
}
.title {
position: relative;
height: 60rpx;
line-height: 60rpx;
margin: 15rpx auto;
text-align: center;
}
.title-text {
font-size: 40rpx;
text-align: center;
}
.info-item {
float: right;
font-size: 26rpx;
margin: 1rpx 0;
}
.info-tag {
position: absolute;
color: black;
left: -10rpx;
width: 4em;
display: inline-block;
text-align: justify;
text-align-last: justify;
}
.info-content {
width: 450rpx;
height: 50rpx;
color: gray;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.image-box {
margin: 30rpx;
background: none;
align-items: left;
padding: 20rpx;
background: white;
}
.signcode-info {
color: gray;
margin: 20rpx 30rpx 10rpx;
font-size: 25rpx;
}
.input-box {
width: 660rpx;
margin: 0 auto 30rpx;
padding-right: 30rpx;
background: white;
}
.button-box {
width: 690rpx;
display: flex;
justify-content: space-between;
gap: 15rpx;
margin: 30rpx auto;
}