update:新增页面&登录注册

This commit is contained in:
ltx 2026-03-31 23:27:35 +08:00
parent 738fe18086
commit e757da69d8
18 changed files with 1299 additions and 313 deletions

View File

@ -15,7 +15,7 @@ page {
.home-scroll {
height: 100%;
box-sizing: border-box;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
/* padding-bottom: calc( env(safe-area-inset-bottom)); */
}
.banner {

View File

@ -6,29 +6,26 @@ function authStatusText(status) {
1: '已认证',
2: '审核中'
};
return map[status] || `状态${status}`;
return map[status] || '未认证';
}
function maskPhone(phone) {
if (!phone || phone.length < 7) return phone || '';
return phone.substring(0, 3) + '****' + phone.substring(7);
}
Page({
data: {
loggedIn: false,
profile: null,
authStatusText: '-',
showAuthForm: false,
authForm: {
name: '',
mobile: '',
idCardNumber: ''
}
profile: {},
maskedPhone: '',
authStatusText: '未认证'
},
onShow() {
const accessToken = wx.getStorageSync('accessToken') || '';
if (!accessToken) {
this.setData({ loggedIn: false, profile: null, authStatusText: '-' });
this.setData({ loggedIn: false, profile: {}, maskedPhone: '', authStatusText: '未认证' });
return;
}
this.setData({ loggedIn: true });
@ -44,9 +41,11 @@ Page({
.fetchMyProfile()
.then((profile) => {
const p = profile || {};
const st = p && typeof p.authStatus === 'number' ? p.authStatus : 0;
const st = typeof p.authStatus === 'number' ? p.authStatus : 0;
const phone = p.mobile || p.phone || '';
this.setData({
profile: p,
maskedPhone: maskPhone(phone),
authStatusText: authStatusText(st)
});
})
@ -69,38 +68,32 @@ Page({
wx.navigateTo({ url: '/subpackages/mine/records/records' });
},
toggleAuthForm() {
this.setData({ showAuthForm: !this.data.showAuthForm });
goToAuth() {
// 跳转到实名认证页面(可按需调整)
wx.showToast({ title: '身份认证功能开发中', icon: 'none' });
},
onAuthInput(e) {
const key = e.currentTarget.dataset.key;
this.setData({
authForm: {
...this.data.authForm,
[key]: e.detail.value
goToOrders() {
wx.showToast({ title: '我的订单功能开发中', icon: 'none' });
},
goToTeam() {
wx.showToast({ title: '我的团队功能开发中', icon: 'none' });
},
onChooseAvatar() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
this.setData({
'profile.avatar': tempFilePath
});
// TODO: 上传头像到服务器
}
});
},
submitAuth() {
const { name, mobile, idCardNumber } = this.data.authForm;
if (!name) return wx.showToast({ title: '请输入姓名', icon: 'none' });
if (!mobile) return wx.showToast({ title: '请输入手机号', icon: 'none' });
if (!idCardNumber) return wx.showToast({ title: '请输入身份证号', icon: 'none' });
wx.showLoading({ title: '提交中...' });
auth
.submitMyAuth({ name, mobile, idCardNumber })
.then(() => {
wx.showToast({ title: '提交成功', icon: 'success' });
this.setData({ showAuthForm: false });
return this.loadProfile();
})
.catch(() => {
wx.showToast({ title: '提交失败', icon: 'none' });
})
.finally(() => wx.hideLoading());
}
});

4
pages/mine/mine.json Normal file
View File

@ -0,0 +1,4 @@
{
"navigationStyle": "custom",
"enablePullDownRefresh": true
}

View File

@ -1,50 +1,83 @@
<view class="container">
<view wx:if="{{!loggedIn}}" class="center">
<button class="login-btn" type="primary" bindtap="goToLogin">登录</button>
<view class="hint">登录后可查看我的信息与实名认证状态</view>
</view>
<view wx:else>
<view class="card profile-card">
<view class="row">
<image class="avatar" wx:if="{{profile && profile.avatar}}" src="{{profile.avatar}}" mode="aspectFill" />
<view class="avatar placeholder" wx:else>头像</view>
<view class="profile-main">
<view class="nick">{{profile && profile.nickName ? profile.nickName : '用户'}}</view>
<view class="sub">积分:{{profile && profile.points != null ? profile.points : '-'}}</view>
<view class="sub">证书:{{profile && profile.certificateCount != null ? profile.certificateCount : '-'}}</view>
<view class="sub">实名认证:{{authStatusText}}</view>
</view>
</view>
</view>
<view class="actions">
<button size="mini" type="default" bindtap="goToPoints">积分</button>
<button size="mini" type="default" bindtap="goToCert">证书</button>
<button size="mini" type="default" bindtap="goToRecords">参赛记录</button>
<button size="mini" type="primary" bindtap="toggleAuthForm">
{{showAuthForm ? '收起实名认证' : '实名认证'}}
</button>
</view>
<view class="card" wx:if="{{showAuthForm}}">
<view class="section-title">实名认证提交</view>
<view class="form-item">
<text class="label">姓名</text>
<input class="input" placeholder="请输入姓名" value="{{authForm.name}}" bindinput="onAuthInput" data-key="name" />
</view>
<view class="form-item">
<text class="label">手机号</text>
<input class="input" placeholder="请输入手机号" value="{{authForm.mobile}}" bindinput="onAuthInput" data-key="mobile" />
</view>
<view class="form-item">
<text class="label">身份证号</text>
<input class="input" placeholder="请输入身份证号" value="{{authForm.idCardNumber}}" bindinput="onAuthInput" data-key="idCardNumber" />
</view>
<button class="submit-btn" type="primary" bindtap="submitAuth">提交审核</button>
</view>
<!-- 未登录 -->
<view wx:if="{{!loggedIn}}" class="not-logged">
<view class="not-logged-inner">
<text class="not-logged-tip">登录后可查看我的信息与参赛记录</text>
<button class="go-login-btn" bindtap="goToLogin">去登录</button>
</view>
</view>
<!-- 已登录 -->
<view wx:else class="mine-page">
<!-- 蓝色头部区域 -->
<view class="header">
<view class="header-bg"></view>
<view class="status-bar"></view>
<view class="nav-bar">
<text class="nav-title">我的</text>
</view>
<!-- 头像 + 手机号 -->
<view class="profile-section">
<view class="avatar-wrap" bindtap="onChooseAvatar">
<image
class="avatar"
src="{{profile.avatar || '/assets/tab_mine.png'}}"
mode="aspectFill"
/>
<view class="camera-icon">
<text class="camera-text">📷</text>
</view>
</view>
<text class="phone-text">{{maskedPhone}}</text>
</view>
</view>
<!-- 内容区 -->
<view class="content">
<!-- 积分 & 证书双卡 -->
<view class="stat-row">
<view class="stat-card" bindtap="goToPoints">
<view class="stat-left">
<text class="stat-num">{{profile.points || 0}}</text>
<text class="stat-label">我的积分</text>
</view>
<image class="stat-icon" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_miniapp/point.png" mode="aspectFit" />
</view>
<view class="stat-card" bindtap="goToCert">
<view class="stat-left">
<text class="stat-num">{{profile.certificateCount || 0}}</text>
<text class="stat-label">我的证书</text>
</view>
<image class="stat-icon" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_miniapp/book.png" mode="aspectFit" />
</view>
</view>
<!-- 身份认证条 -->
<view class="auth-bar" bindtap="goToAuth">
<view class="auth-left">
<text class="auth-title">身份认证</text>
<text class="auth-desc">点击进行身份认证</text>
</view>
<view class="auth-badge">
<text class="auth-badge-icon">🛡</text>
<text class="auth-badge-text">{{authStatusText}}</text>
</view>
</view>
<!-- 功能入口 -->
<view class="func-card">
<view class="func-item" bindtap="goToOrders">
<image class="func-icon" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_miniapp/order.png" mode="aspectFit" />
<text class="func-text">我的订单</text>
</view>
<view class="func-item" bindtap="goToRecords">
<image class="func-icon" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_miniapp/history.png" mode="aspectFit" />
<text class="func-text">参赛记录</text>
</view>
<view class="func-item" bindtap="goToTeam">
<image class="func-icon" src="https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_miniapp/team.png" mode="aspectFit" />
<text class="func-text">我的团队</text>
</view>
</view>
</view>
</view>

View File

@ -1,153 +1,252 @@
.container {
padding: 16rpx;
/* ====== 未登录 ====== */
.not-logged {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
}
.center {
padding: 40rpx 0;
.not-logged-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 18rpx;
gap: 32rpx;
}
.login-btn {
.not-logged-tip {
font-size: 28rpx;
color: #999;
}
.go-login-btn {
width: 400rpx !important;
height: 88rpx;
line-height: 88rpx;
background: #1a56db;
color: #fff;
font-size: 30rpx;
font-weight: 600;
border-radius: 12rpx;
border: none;
}
.go-login-btn::after {
border: none;
}
/* ====== 已登录页面 ====== */
.mine-page {
min-height: 100vh;
background: #f0f2f5;
}
/* ====== 蓝色头部 ====== */
.header {
position: relative;
width: 100%;
max-width: 420rpx;
overflow: visible;
}
.hint {
color: #888;
font-size: 24rpx;
text-align: center;
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 520rpx;
background: linear-gradient(180deg, #1a3a8a 0%, #1a56db 100%);
border-radius: 0 0 40rpx 40rpx;
}
.card {
background: #ffffff;
border-radius: 18rpx;
padding: 16rpx;
box-shadow: 0 8rpx 22rpx rgba(0, 0, 0, 0.04);
.status-bar {
height: env(safe-area-inset-top, 44px);
position: relative;
}
.profile-card {
margin-top: 8rpx;
}
.row {
.nav-bar {
position: relative;
height: 88rpx;
display: flex;
gap: 16rpx;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #ffffff;
}
/* ====== 头像区 ====== */
.profile-section {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0 40rpx;
}
.avatar-wrap {
position: relative;
width: 160rpx;
height: 160rpx;
}
.avatar {
width: 110rpx;
height: 110rpx;
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background: #f2f2f2;
border: 6rpx solid rgba(255, 255, 255, 0.4);
}
.avatar.placeholder {
.camera-icon {
position: absolute;
right: 4rpx;
bottom: 4rpx;
width: 48rpx;
height: 48rpx;
background: #1a56db;
border: 4rpx solid #fff;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #aaa;
justify-content: center;
}
.camera-text {
font-size: 24rpx;
font-weight: 700;
}
.profile-main {
flex: 1;
}
.nick {
.phone-text {
margin-top: 16rpx;
font-size: 32rpx;
font-weight: 900;
color: #222;
margin-bottom: 10rpx;
font-weight: 600;
color: #ffffff;
letter-spacing: 2rpx;
}
.sub {
color: #666;
font-size: 26rpx;
/* ====== 内容区 ====== */
.content {
position: relative;
margin-top: -20rpx;
padding: 0 28rpx;
padding-bottom: 40rpx;
}
/* ====== 积分 & 证书双卡 ====== */
.stat-row {
display: flex;
gap: 20rpx;
}
.stat-card {
flex: 1;
background: #ffffff;
border-radius: 16rpx;
padding: 28rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
}
.stat-left {
display: flex;
flex-direction: column;
}
.stat-num {
font-size: 48rpx;
font-weight: 800;
color: #1a1a1a;
line-height: 1.1;
}
.stat-label {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.actions {
margin-top: 12rpx;
.stat-icon {
width: 80rpx;
height: 80rpx;
flex-shrink: 0;
}
.section-title {
margin-top: 18rpx;
margin-bottom: 14rpx;
font-size: 28rpx;
font-weight: 900;
color: #222;
/* ====== 身份认证条 ====== */
.auth-bar {
margin-top: 20rpx;
background: linear-gradient(135deg, #1a3a8a 0%, #1a56db 100%);
border-radius: 16rpx;
padding: 28rpx 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.main-title {
margin-top: 22rpx;
}
.form-item {
.auth-left {
display: flex;
flex-direction: column;
gap: 10rpx;
margin-top: 16rpx;
}
.label {
color: #666;
font-size: 24rpx;
.auth-title {
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
}
.input {
height: 72rpx;
background: #f6f7fb;
border-radius: 14rpx;
padding: 0 16rpx;
font-size: 26rpx;
}
.submit-btn {
margin-top: 22rpx;
width: 100%;
height: 84rpx;
}
.list {
height: calc(100vh - 420rpx);
}
.empty {
text-align: center;
padding: 40rpx 0;
color: #888;
font-size: 24rpx;
}
.loading {
text-align: center;
padding: 24rpx 0;
color: #888;
}
.reg-card {
padding: 16rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 8rpx 22rpx rgba(0, 0, 0, 0.03);
margin-bottom: 14rpx;
}
.r-title {
font-size: 30rpx;
font-weight: 900;
color: #222;
margin-bottom: 10rpx;
}
.r-sub {
color: #666;
font-size: 26rpx;
.auth-desc {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.7);
margin-top: 6rpx;
}
.auth-badge {
display: flex;
align-items: center;
gap: 8rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 30rpx;
padding: 10rpx 24rpx;
}
.auth-badge-icon {
font-size: 28rpx;
}
.auth-badge-text {
font-size: 24rpx;
color: #ffffff;
white-space: nowrap;
}
/* ====== 功能入口卡 ====== */
.func-card {
margin-top: 20rpx;
background: #ffffff;
border-radius: 16rpx;
padding: 36rpx 0;
display: flex;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
}
.func-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 14rpx;
}
.func-icon {
width: 64rpx;
height: 64rpx;
}
.func-text {
font-size: 24rpx;
color: #333;
}

View File

@ -34,5 +34,6 @@
"ignore": [],
"include": []
},
"editorSetting": {}
"editorSetting": {},
"libVersion": "3.15.1"
}

View File

@ -1,8 +1,8 @@
{
"libVersion": "3.15.1",
"libVersion": "3.15.0",
"projectname": "YD_Event.miniApp",
"setting": {
"urlCheck": true,
"urlCheck": false,
"coverView": false,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,

View File

@ -1,14 +1,26 @@
const auth = require('../../../utils/auth');
const PHONE_RE = /^1[3456789]\d{9}$/;
const CODE_COUNTDOWN = 60;
Page({
data: {
loginMode: 'password', // 'code' | 'password'
mobile: '',
password: '',
logging: false
verificationCode: '',
showPassword: false,
logging: false,
agreed: false,
// 验证码倒计时
codeText: '发送验证码',
codeBtnDisabled: false,
countdown: 0
},
_timer: null,
onShow() {
const accessToken = wx.getStorageSync('accessToken') || '';
if (accessToken) {
@ -16,43 +28,165 @@ Page({
}
},
onUnload() {
this._clearTimer();
},
// ========= 事件处理 =========
/** 切换登录方式 */
onSwitchMode(e) {
const mode = e.currentTarget.dataset.mode;
if (mode === this.data.loginMode) return;
this.setData({
loginMode: mode,
verificationCode: '',
password: ''
});
},
/** 手机号输入 */
onMobileInput(e) {
this.setData({ mobile: (e.detail.value || '').trim() });
},
/** 清除手机号 */
onClearMobile() {
this.setData({ mobile: '' });
},
/** 验证码输入 */
onCodeInput(e) {
this.setData({ verificationCode: (e.detail.value || '').trim() });
},
/** 密码输入 */
onPasswordInput(e) {
this.setData({ password: e.detail.value || '' });
},
onLoginTap() {
if (this.data.logging) return;
const { mobile, password } = this.data;
/** 显示/隐藏密码 */
onTogglePassword() {
this.setData({ showPassword: !this.data.showPassword });
},
/** 勾选协议 */
onToggleAgreement() {
this.setData({ agreed: !this.data.agreed });
},
/** 忘记密码 */
onForgetPassword() {
wx.navigateTo({ url: '/subpackages/mine/register/register' });
},
// ========= 发送验证码 =========
onSendCode() {
if (this.data.codeBtnDisabled) return;
const { mobile } = this.data;
if (!PHONE_RE.test(mobile)) {
wx.showToast({ title: '请输入正确手机号', icon: 'none' });
wx.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
if (!password) {
this.setData({ codeBtnDisabled: true, codeText: '发送中...' });
auth
.sendVerificationCode(mobile)
.then(() => {
wx.showToast({ title: '验证码已发送', icon: 'success' });
this._startCountdown();
})
.catch((err) => {
wx.showToast({ title: err.message || '发送失败', icon: 'none' });
this.setData({ codeBtnDisabled: false, codeText: '发送验证码' });
});
},
// ========= 登录 =========
onLoginTap() {
if (this.data.logging) return;
if (!this.data.agreed) {
wx.showToast({ title: '请先同意用户协议与隐私政策', icon: 'none' });
return;
}
const { mobile, loginMode, password, verificationCode } = this.data;
if (!PHONE_RE.test(mobile)) {
wx.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
if (loginMode === 'password' && !password) {
wx.showToast({ title: '请输入密码', icon: 'none' });
return;
}
if (loginMode === 'code' && !verificationCode) {
wx.showToast({ title: '请输入验证码', icon: 'none' });
return;
}
this.setData({ logging: true });
wx.showLoading({ title: '登录中...' });
auth
.loginWithMobilePassword({ mobile, password })
const loginPromise =
loginMode === 'code'
? auth.loginWithMobileCode({ mobile, verificationCode })
: auth.loginWithMobilePassword({ mobile, password });
loginPromise
.then(() => {
wx.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() => {
wx.redirectTo({ url: '/pages/mine/mine' });
}, 400);
})
.catch(() => {
wx.showToast({ title: '登录失败', icon: 'none' });
.catch((err) => {
wx.showToast({ title: err.message || '登录失败', icon: 'none' });
})
.finally(() => {
this.setData({ logging: false });
wx.hideLoading();
});
},
// ========= 内部方法 =========
_startCountdown() {
let seconds = CODE_COUNTDOWN;
this.setData({
countdown: seconds,
codeText: `${seconds}s`,
codeBtnDisabled: true
});
this._timer = setInterval(() => {
seconds -= 1;
if (seconds <= 0) {
this._clearTimer();
this.setData({
countdown: 0,
codeText: '发送验证码',
codeBtnDisabled: false
});
} else {
this.setData({
countdown: seconds,
codeText: `${seconds}s`
});
}
}, 1000);
},
_clearTimer() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
}
});

View File

@ -1,4 +1,6 @@
{
"navigationBarTitleText": "登录"
"navigationBarTitleText": "登录",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}

View File

@ -1,32 +1,101 @@
<view class="container">
<view class="card">
<view class="form-item">
<text class="label">手机号</text>
<input
class="input"
type="number"
maxlength="11"
placeholder="请输入手机号"
value="{{mobile}}"
bindinput="onMobileInput"
/>
<view class="login-page">
<!-- Tab 切换 -->
<view class="tab-bar">
<view class="tab-item {{loginMode === 'password' ? 'active' : ''}}" bindtap="onSwitchMode" data-mode="password">
<text class="tab-text">账号登录</text>
<view class="tab-line" wx:if="{{loginMode === 'password'}}"></view>
</view>
<view class="form-item">
<text class="label">密码</text>
<input
class="input"
password
placeholder="请输入密码"
value="{{password}}"
bindinput="onPasswordInput"
/>
<view class="tab-item {{loginMode === 'code' ? 'active' : ''}}" bindtap="onSwitchMode" data-mode="code">
<text class="tab-text">短信登录</text>
<view class="tab-line" wx:if="{{loginMode === 'code'}}"></view>
</view>
<button class="submit-btn" type="primary" bindtap="onLoginTap" loading="{{logging}}">
登录
</button>
<navigator class="link-register" url="/subpackages/mine/register/register" hover-class="none">
没有账号?去注册
</navigator>
<view class="hint">登录后可查看我的信息与参赛记录</view>
</view>
<!-- 账号登录模式 -->
<block wx:if="{{loginMode === 'password'}}">
<view class="field">
<text class="field-label">账号</text>
<view class="field-input-box">
<input
class="field-input"
type="number"
maxlength="11"
placeholder="请输入手机号"
placeholder-class="placeholder"
value="{{mobile}}"
bindinput="onMobileInput"
/>
</view>
</view>
<view class="field">
<text class="field-label">密码</text>
<view class="field-input-box">
<input
class="field-input"
password="{{!showPassword}}"
placeholder="请输入密码"
placeholder-class="placeholder"
value="{{password}}"
bindinput="onPasswordInput"
/>
<view class="eye-btn" bindtap="onTogglePassword">
<image class="eye-icon" src="/assets/icon-eye{{showPassword ? '-open' : '-close'}}.png" mode="aspectFit" wx:if="{{false}}" />
<text class="eye-text">{{showPassword ? '🙈' : '👁'}}</text>
</view>
</view>
</view>
</block>
<!-- 短信登录模式 -->
<block wx:if="{{loginMode === 'code'}}">
<view class="field">
<text class="field-label">手机号</text>
<view class="field-input-box">
<input
class="field-input"
type="number"
maxlength="11"
placeholder="请输入手机号"
placeholder-class="placeholder"
value="{{mobile}}"
bindinput="onMobileInput"
/>
</view>
</view>
<view class="field">
<text class="field-label">验证码</text>
<view class="field-input-box">
<input
class="field-input"
type="number"
maxlength="6"
placeholder="请输入验证码"
placeholder-class="placeholder"
value="{{verificationCode}}"
bindinput="onCodeInput"
/>
<text class="send-code-link {{codeBtnDisabled ? 'disabled' : ''}}" bindtap="onSendCode">{{codeText}}</text>
</view>
</view>
</block>
<!-- 协议 + 忘记密码 -->
<view class="agreement-row">
<view class="agreement-left" bindtap="onToggleAgreement">
<view class="check-box {{agreed ? 'checked' : ''}}">
<text class="check-icon" wx:if="{{agreed}}">✓</text>
</view>
<text class="agreement-text">我同意</text>
<text class="agreement-link">用户协议</text>
<text class="agreement-text">与</text>
<text class="agreement-link">隐私政策</text>
</view>
<text class="forget-link" bindtap="onForgetPassword">忘记密码?</text>
</view>
<!-- 按钮区域 -->
<view class="btn-area">
<button class="btn-login" bindtap="onLoginTap" loading="{{logging}}">登录</button>
<navigator class="btn-register" url="/subpackages/mine/register/register" hover-class="btn-register-hover">注册</navigator>
</view>
</view>

View File

@ -1,59 +1,215 @@
.container {
/* ===== 页面 ===== */
.login-page {
min-height: 100vh;
background: #f7f8fa;
padding: 24rpx 16rpx;
}
.card {
background: #ffffff;
border-radius: 18rpx;
padding: 24rpx 20rpx;
box-shadow: 0 8rpx 22rpx rgba(0, 0, 0, 0.04);
max-width: 520rpx;
margin: 40rpx auto 0;
padding: 0 48rpx;
box-sizing: border-box;
}
.form-item {
/* ===== Tab 栏 ===== */
.tab-bar {
display: flex;
align-items: flex-end;
gap: 48rpx;
padding-top: 40rpx;
padding-bottom: 20rpx;
}
.tab-item {
display: flex;
flex-direction: column;
gap: 10rpx;
margin-top: 16rpx;
align-items: center;
position: relative;
padding-bottom: 16rpx;
}
.form-item:first-child {
margin-top: 0;
.tab-text {
font-size: 30rpx;
color: #999;
transition: all 0.25s;
}
.label {
color: #666;
font-size: 24rpx;
.tab-item.active .tab-text {
font-size: 34rpx;
color: #1a1a1a;
font-weight: 700;
}
.input {
height: 72rpx;
background: #f6f7fb;
border-radius: 14rpx;
padding: 0 16rpx;
font-size: 26rpx;
.tab-line {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: #1a56db;
border-radius: 3rpx;
}
.submit-btn {
margin-top: 28rpx;
width: 100%;
height: 84rpx;
/* ===== 表单字段 ===== */
.field {
margin-top: 40rpx;
}
.link-register {
.field-label {
display: block;
margin-top: 20rpx;
text-align: center;
color: #576b95;
font-size: 26rpx;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
}
.hint {
margin-top: 24rpx;
color: #888;
font-size: 24rpx;
text-align: center;
.field-input-box {
display: flex;
align-items: center;
height: 96rpx;
border: 2rpx solid #d9d9d9;
border-radius: 12rpx;
padding: 0 24rpx;
background: #fff;
transition: border-color 0.25s;
}
.field-input-box:focus-within {
border-color: #1a56db;
}
.field-input {
flex: 1;
height: 96rpx;
font-size: 28rpx;
color: #333;
}
.placeholder {
color: #c0c4cc;
font-size: 28rpx;
}
/* ===== 眼睛按钮 ===== */
.eye-btn {
flex-shrink: 0;
padding: 10rpx;
display: flex;
align-items: center;
justify-content: center;
}
.eye-text {
font-size: 36rpx;
color: #999;
}
/* ===== 发送验证码链接 ===== */
.send-code-link {
flex-shrink: 0;
font-size: 26rpx;
color: #1a56db;
font-weight: 500;
padding-left: 16rpx;
white-space: nowrap;
}
.send-code-link.disabled {
color: #c0c4cc;
}
/* ===== 协议行 ===== */
.agreement-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 40rpx;
}
.agreement-left {
display: flex;
align-items: center;
}
.check-box {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #d9d9d9;
border-radius: 6rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
flex-shrink: 0;
transition: all 0.25s;
background: #fff;
}
.check-box.checked {
background: #1a56db;
border-color: #1a56db;
}
.check-icon {
font-size: 22rpx;
color: #ffffff;
font-weight: 700;
}
.agreement-text {
font-size: 24rpx;
color: #666;
}
.agreement-link {
font-size: 24rpx;
color: #1a1a1a;
font-weight: 600;
}
.forget-link {
font-size: 24rpx;
color: #1a56db;
flex-shrink: 0;
}
/* ===== 按钮区域 ===== */
.btn-area {
margin-top: 60rpx;
}
.btn-login {
width: 100% !important;
height: 96rpx;
line-height: 96rpx;
background: #1a56db;
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
border-radius: 12rpx;
border: none;
letter-spacing: 4rpx;
}
.btn-login::after {
border: none;
}
.btn-register {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: #ffffff;
color: #1a56db;
font-size: 32rpx;
font-weight: 600;
border-radius: 12rpx;
border: 2rpx solid #1a56db;
margin-top: 24rpx;
letter-spacing: 4rpx;
text-align: center;
box-sizing: border-box;
}
.btn-register-hover {
background: #f0f5ff;
}

View File

@ -1,10 +1,16 @@
const auth = require('../../../utils/auth');
const { formatTime } = require('../../../utils/format');
const PAGE_SIZE = 10;
Page({
data: {
nickName: '',
points: '-',
loading: false
totalPoints: 0,
activeTab: 1, // 1-获取记录 2-消耗记录
records: [],
loading: false,
noMore: false,
pageNo: 1
},
onShow() {
@ -14,22 +20,85 @@ Page({
return;
}
this.loadProfile();
this.resetAndLoad();
},
onPullDownRefresh() {
this.loadProfile();
this.resetAndLoad().finally(() => wx.stopPullDownRefresh());
},
/** 返回 */
onBack() {
wx.navigateBack({ delta: 1 });
},
/** 加载积分总数 */
loadProfile() {
if (this.data.loading) return;
auth.fetchMyProfile().then((profile) => {
const p = profile || {};
this.setData({ totalPoints: p.points || 0 });
}).catch(() => {});
},
/** 切换Tab */
onSwitchTab(e) {
const tab = Number(e.currentTarget.dataset.tab);
if (tab === this.data.activeTab) return;
this.setData({ activeTab: tab });
this.resetAndLoad();
},
/** 重置并加载 */
resetAndLoad() {
this.setData({ records: [], pageNo: 1, noMore: false });
return this.loadRecords();
},
/** 加载更多 */
onLoadMore() {
if (this.data.loading || this.data.noMore) return;
this.loadRecords();
},
/** 加载积分记录 */
loadRecords() {
if (this.data.loading) return Promise.resolve();
this.setData({ loading: true });
auth
.fetchMyProfile()
.then((profile) => {
const p = profile || {};
const { pageNo, activeTab } = this.data;
return auth
.fetchMyPoints({ pageNo, pageSize: PAGE_SIZE })
.then((result) => {
const res = result || {};
const allRows = res.rows || [];
// 按类型筛选: type 1=获取 2=消耗
const filtered = allRows.filter((item) => item.type === activeTab);
const formatted = filtered.map((item) => ({
...item,
createdTimeStr: item.createdTime ? formatTime(item.createdTime) : '',
projectInfo: item.projectInfo || item.projectName || '',
rank: item.rank || ''
}));
const newRecords = this.data.records.concat(formatted);
const totalRows = res.total || 0;
const currentTotal = pageNo * PAGE_SIZE;
this.setData({
nickName: p.nickName || '',
points: p.points != null ? p.points : '-'
records: newRecords,
pageNo: pageNo + 1,
noMore: currentTotal >= totalRows
});
})
.catch(() => {})
.finally(() => this.setData({ loading: false }));
.catch(() => {
wx.showToast({ title: '加载失败', icon: 'none' });
})
.finally(() => {
this.setData({ loading: false });
});
}
});

View File

@ -1,4 +1,5 @@
{
"navigationBarTitleText": "我的积分"
"navigationStyle": "custom",
"enablePullDownRefresh": true
}

View File

@ -1,7 +1,81 @@
<view class="container">
<view class="card">
<view class="title">{{nickName || '用户'}}</view>
<view class="value">积分:{{points}}</view>
<view class="points-page">
<!-- 蓝色头部 -->
<view class="header">
<view class="header-bg"></view>
<view class="status-bar"></view>
<view class="nav-bar">
<view class="nav-back" bindtap="onBack">
<text class="back-arrow">←</text>
</view>
<text class="nav-title">我的积分</text>
<view class="nav-placeholder"></view>
</view>
<view class="points-total">
<text class="points-num">{{totalPoints}}</text>
<text class="points-label">我的积分</text>
</view>
</view>
<!-- Tab 切换 -->
<view class="tab-bar">
<view class="tab-item {{activeTab === 1 ? 'active' : ''}}" bindtap="onSwitchTab" data-tab="1">
<text class="tab-text">获取记录</text>
<view class="tab-line" wx:if="{{activeTab === 1}}"></view>
</view>
<view class="tab-item {{activeTab === 2 ? 'active' : ''}}" bindtap="onSwitchTab" data-tab="2">
<text class="tab-text">消耗记录</text>
<view class="tab-line" wx:if="{{activeTab === 2}}"></view>
</view>
</view>
<!-- 列表 -->
<scroll-view
class="record-list"
scroll-y
enhanced
show-scrollbar="{{false}}"
bindscrolltolower="onLoadMore"
>
<!-- 空状态 -->
<view wx:if="{{records.length === 0 && !loading}}" class="empty">
<view class="empty-icon">
<view class="empty-doc">
<view class="empty-doc-top"></view>
<view class="empty-doc-line"></view>
<view class="empty-doc-line short"></view>
<view class="empty-doc-line"></view>
</view>
<view class="empty-doc-shadow"></view>
</view>
<text class="empty-text">暂无{{activeTab === 1 ? '获取' : '消耗'}}记录</text>
</view>
<!-- 有数据时:表头 + 列表 -->
<block wx:if="{{records.length > 0}}">
<view class="table-header">
<text class="th th-event">赛事</text>
<text class="th th-project">项目-组别</text>
<text class="th th-rank">名次</text>
<text class="th th-points">积分</text>
</view>
<view wx:for="{{records}}" wx:key="id" class="record-row">
<view class="td td-event">
<text class="event-name">{{item.title || '-'}}</text>
<text class="event-time">{{item.createdTimeStr}}</text>
</view>
<text class="td td-project">{{item.projectInfo || '-'}}</text>
<text class="td td-rank">{{item.rank || '-'}}</text>
<text class="td td-points {{item.points > 0 ? 'positive' : 'negative'}}">{{item.points > 0 ? '+' : ''}}{{item.points}}</text>
</view>
</block>
<view wx:if="{{loading}}" class="loading-tip">
<text>加载中...</text>
</view>
<view wx:if="{{noMore && records.length > 0}}" class="loading-tip">
<text>没有更多了</text>
</view>
</scroll-view>
</view>

View File

@ -1,26 +1,298 @@
.container {
/* ===== 页面 ===== */
.points-page {
min-height: 100vh;
background: #f7f8fa;
padding: 24rpx 16rpx;
background: #f5f7fa;
display: flex;
flex-direction: column;
}
.card {
background: #ffffff;
border-radius: 18rpx;
padding: 20rpx 18rpx;
box-shadow: 0 8rpx 22rpx rgba(0, 0, 0, 0.03);
/* ===== 蓝色头部 ===== */
.header {
position: relative;
width: 100%;
}
.title {
font-size: 32rpx;
font-weight: 900;
color: #222;
margin-bottom: 12rpx;
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 420rpx;
background: linear-gradient(180deg, #1a3a8a 0%, #3a7bd5 100%);
border-radius: 0 0 40rpx 40rpx;
}
.value {
.status-bar {
height: env(safe-area-inset-top, 44px);
position: relative;
}
.nav-bar {
position: relative;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
font-size: 40rpx;
font-weight: 900;
color: #1aad19;
color: #ffffff;
font-weight: 700;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #ffffff;
}
.nav-placeholder {
width: 60rpx;
}
/* ===== 积分总数 ===== */
.points-total {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0 50rpx;
}
.points-num {
font-size: 80rpx;
font-weight: 800;
color: #ffffff;
line-height: 1.1;
}
.points-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.7);
margin-top: 8rpx;
}
/* ===== Tab 栏 ===== */
.tab-bar {
display: flex;
background: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
}
.tab-item {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 26rpx 0;
}
.tab-text {
font-size: 30rpx;
color: #999;
}
.tab-item.active .tab-text {
color: #1a1a1a;
font-weight: 700;
font-size: 32rpx;
}
.tab-line {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: #1a56db;
border-radius: 3rpx;
}
/* ===== 表头 ===== */
.table-header {
display: flex;
align-items: center;
padding: 24rpx 28rpx;
background: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
}
.th {
font-size: 26rpx;
color: #999;
font-weight: 500;
}
.th-event {
width: 240rpx;
flex-shrink: 0;
}
.th-project {
flex: 1;
text-align: center;
}
.th-rank {
width: 80rpx;
text-align: center;
flex-shrink: 0;
}
.th-points {
width: 80rpx;
text-align: right;
flex-shrink: 0;
}
/* ===== 记录列表 ===== */
.record-list {
flex: 1;
height: 0;
background: #ffffff;
}
.record-row {
display: flex;
align-items: center;
padding: 24rpx 28rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.td {
font-size: 26rpx;
color: #333;
}
.td-event {
width: 240rpx;
flex-shrink: 0;
display: flex;
flex-direction: column;
}
.event-name {
font-size: 26rpx;
color: #1a1a1a;
font-weight: 500;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
line-height: 1.4;
}
.event-time {
font-size: 22rpx;
color: #bbb;
margin-top: 6rpx;
}
.td-project {
flex: 1;
text-align: center;
font-size: 24rpx;
color: #666;
padding: 0 8rpx;
}
.td-rank {
width: 80rpx;
text-align: center;
flex-shrink: 0;
}
.td-points {
width: 80rpx;
text-align: right;
flex-shrink: 0;
font-weight: 700;
}
.td-points.positive {
color: #1a56db;
}
.td-points.negative {
color: #e74c3c;
}
/* ===== 空状态 ===== */
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 240rpx;
}
.empty-icon {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
}
.empty-doc {
width: 160rpx;
height: 180rpx;
background: #f5f5f5;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 30rpx;
gap: 14rpx;
position: relative;
}
.empty-doc-top {
width: 80rpx;
height: 20rpx;
background: #e8e8e8;
border-radius: 4rpx;
}
.empty-doc-line {
width: 100rpx;
height: 10rpx;
background: #e8e8e8;
border-radius: 2rpx;
}
.empty-doc-line.short {
width: 70rpx;
}
.empty-doc-shadow {
width: 140rpx;
height: 16rpx;
background: radial-gradient(ellipse, rgba(0,0,0,0.08) 0%, transparent 70%);
margin-top: 8rpx;
}
.empty-text {
font-size: 28rpx;
color: #c0c4cc;
}
/* ===== 加载提示 ===== */
.loading-tip {
text-align: center;
padding: 24rpx 0;
color: #ccc;
font-size: 24rpx;
}

View File

@ -56,6 +56,57 @@ async function loginWithMobilePassword({ mobile, password }) {
};
}
async function loginWithMobileCode({ mobile, verificationCode }) {
const loginData = await request({
path: API.loginByCode,
method: 'POST',
needAuth: false,
data: {
mobile,
verificationCode
},
contentType: 'application/json'
});
const app = getApp();
const accessToken = pickString(loginData, 'accessToken');
const refreshToken = pickString(loginData, 'refreshToken');
const userInfo =
loginData && loginData.userInfo && typeof loginData.userInfo === 'object'
? loginData.userInfo
: null;
const openid = userInfo ? pickString(userInfo, 'openid') : '';
app.setAuth({
accessToken,
refreshToken,
openid,
userProfile: userInfo
});
let myProfile = null;
try {
myProfile = await request({
path: API.myProfile,
method: 'GET',
needAuth: true
});
} catch (e) {
myProfile = null;
}
if (myProfile && typeof myProfile === 'object') {
app.setAuth({
userProfile: myProfile
});
}
return {
loginData,
myProfile
};
}
async function sendVerificationCode(phone) {
return request({
path: API.sendVerificationCode,
@ -130,12 +181,26 @@ async function fetchMyRegistrations({ pageNo = 1, pageSize = 10 } = {}) {
});
}
async function fetchMyPoints({ pageNo = 1, pageSize = 10 } = {}) {
return request({
path: API.myPoints,
method: 'GET',
needAuth: true,
data: {
PageNo: pageNo,
PageSize: pageSize
}
});
}
module.exports = {
loginWithMobilePassword,
loginWithMobileCode,
sendVerificationCode,
registerWithVerification,
fetchMyProfile,
fetchMyAuth,
submitMyAuth,
fetchMyRegistrations
fetchMyRegistrations,
fetchMyPoints
};

View File

@ -11,12 +11,17 @@ const API = {
apply: '/api/eventclient/apply',
wechatLogin: '/api/oauth/login',
loginByCode: '/api/oauth/login',
oauthRegister: '/api/oauth/register',
oauthResetPassword: '/api/oauth/reset-password',
sendVerificationCode: '/api/sms/sendverificationcode',
myProfile: '/api/my/profile',
myAuth: '/api/my/auth',
myRegistrations: '/api/my/registrations'
myRegistrations: '/api/my/registrations',
myPoints: '/api/my/points',
myOrders: '/api/my/orders',
myTeams: '/api/my/teams',
myCertificates: '/api/my/certificates'
};
module.exports = {

View File

@ -6,6 +6,14 @@ function formatDateTime(iso) {
return s;
}
function formatTime(iso) {
if (!iso || typeof iso !== 'string') return '-';
const s = iso.replace('T', ' ').replace('Z', '');
// 返回 yyyy-MM-dd HH:mm 格式
if (s.length >= 16) return s.slice(0, 16);
return s;
}
function eventStatusText(status) {
if (status === 'Enrolling') return '报名中';
if (status === 'Ongoing') return '比赛中';
@ -26,6 +34,7 @@ function eventFormatText(v) {
module.exports = {
formatDateTime,
formatTime,
eventStatusText,
eventFormatText
};