Initial commit
This commit is contained in:
188
wechat-mini-program/pages/coupon/coupon.js
Normal file
188
wechat-mini-program/pages/coupon/coupon.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
coupons: [],
|
||||
user: {}
|
||||
},
|
||||
onLoad() {
|
||||
this.fetchCoupons();
|
||||
},
|
||||
onShow() {
|
||||
this.getUserInfo();
|
||||
this.fetchCoupons();
|
||||
},
|
||||
getUserInfo() {
|
||||
const { request } = require('../../utils/request')
|
||||
const app = getApp()
|
||||
|
||||
// Fetch latest user info from backend to check if phone exists in DB
|
||||
request({ url: '/user/' }).then(user => {
|
||||
app.globalData.userInfo = user; // Sync global data
|
||||
this.setData({ user: user });
|
||||
}).catch(err => {
|
||||
console.error('Failed to fetch user info', err)
|
||||
// Fallback to global data if fetch fails
|
||||
if (app.globalData.userInfo) {
|
||||
this.setData({ user: app.globalData.userInfo });
|
||||
}
|
||||
})
|
||||
},
|
||||
goToProfile() {
|
||||
const app = getApp();
|
||||
app.globalData.profileAction = 'bind_phone';
|
||||
wx.switchTab({
|
||||
url: '/pages/profile/profile'
|
||||
})
|
||||
},
|
||||
fetchCoupons() {
|
||||
const { request } = require('../../utils/request')
|
||||
|
||||
Promise.all([
|
||||
request({ url: '/user-coupons/' }).catch(() => []),
|
||||
request({ url: '/available-coupons/' }).catch(() => [])
|
||||
]).then(([userCoupons, availableCoupons]) => {
|
||||
// 1. Process User Coupons
|
||||
const userList = Array.isArray(userCoupons) ? userCoupons : (userCoupons && userCoupons.results) || [];
|
||||
const formattedUserList = userList.map(item => {
|
||||
if (item.coupon_detail) {
|
||||
let scopeText = item.coupon_detail.scope_text || '全场通用';
|
||||
if (scopeText === '通用') scopeText = '全场通用';
|
||||
|
||||
return {
|
||||
...item.coupon_detail,
|
||||
id: item.id,
|
||||
coupon_id: item.coupon_detail.id,
|
||||
status: item.status,
|
||||
expiry: item.coupon_detail.expiry,
|
||||
displayDesc: scopeText,
|
||||
is_time_limited: item.coupon_detail.is_time_limited
|
||||
}
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
displayDesc: item.desc
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Process Available Coupons
|
||||
const userCouponIds = new Set(formattedUserList.map(item => item.coupon_id));
|
||||
|
||||
const availableList = Array.isArray(availableCoupons) ? availableCoupons : (availableCoupons && availableCoupons.results) || [];
|
||||
const formattedAvailableList = availableList
|
||||
.filter(c => !userCouponIds.has(c.id))
|
||||
.map(c => {
|
||||
let scopeText = c.scope_text || '全场通用';
|
||||
if (scopeText === '通用') scopeText = '全场通用';
|
||||
return {
|
||||
...c,
|
||||
id: 'avail_' + c.id, // Virtual ID
|
||||
coupon_id: c.id,
|
||||
status: 'can_claim', // Virtual Status
|
||||
displayDesc: scopeText
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Merge and Sort
|
||||
const allCoupons = [...formattedAvailableList, ...formattedUserList];
|
||||
|
||||
// Sort: can_claim -> assigned -> used -> expired
|
||||
const statusOrder = { 'can_claim': 0, 'assigned': 1, 'used': 2, 'expired': 3, 'revoked': 4 };
|
||||
|
||||
allCoupons.sort((a, b) => {
|
||||
const orderA = statusOrder[a.status] !== undefined ? statusOrder[a.status] : 99;
|
||||
const orderB = statusOrder[b.status] !== undefined ? statusOrder[b.status] : 99;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
this.setData({ coupons: allCoupons });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
// Mock data fallback
|
||||
this.setData({
|
||||
coupons: [
|
||||
{
|
||||
id: 1,
|
||||
amount: '50',
|
||||
unit: '元',
|
||||
title: '新人见面礼',
|
||||
desc: '无门槛使用,适用于所有课程',
|
||||
displayDesc: '全场通用',
|
||||
expiry: '2023-12-31',
|
||||
status: 'available',
|
||||
color: 'from-blue-500 to-cyan-400',
|
||||
bgStart: 'from-blue-50',
|
||||
bgEnd: 'to-cyan-50',
|
||||
shadow: 'shadow-blue-100',
|
||||
is_time_limited: true
|
||||
},
|
||||
// ... (existing mocks)
|
||||
]
|
||||
})
|
||||
})
|
||||
},
|
||||
handleCouponClick(e) {
|
||||
if (!this.data.user.phone) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先绑定手机号激活会员权益',
|
||||
confirmText: '去绑定',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.goToProfile();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const status = e.currentTarget.dataset.status;
|
||||
|
||||
if (status === 'can_claim') {
|
||||
this.handleClaim(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// If coupon is available/assigned, redirect to home page to use it
|
||||
if (status !== 'used') {
|
||||
wx.switchTab({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
handleClaim(e) {
|
||||
const id = e.currentTarget.dataset.id; // This is 'avail_ID'
|
||||
const realId = id.toString().replace('avail_', '');
|
||||
|
||||
wx.showLoading({ title: '领取中...' });
|
||||
|
||||
const { request } = require('../../utils/request')
|
||||
|
||||
request({
|
||||
url: '/user-coupons/',
|
||||
method: 'POST',
|
||||
data: {
|
||||
coupon_id: realId
|
||||
}
|
||||
}).then(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({
|
||||
title: '领取成功',
|
||||
icon: 'success'
|
||||
});
|
||||
// Refresh list
|
||||
this.fetchCoupons();
|
||||
}).catch(err => {
|
||||
wx.hideLoading();
|
||||
console.error(err);
|
||||
// Handle specific error message from backend
|
||||
const msg = (err && err.error) || (err && err.detail) || '领取失败';
|
||||
wx.showToast({
|
||||
title: msg,
|
||||
icon: 'none'
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
3
wechat-mini-program/pages/coupon/coupon.json
Normal file
3
wechat-mini-program/pages/coupon/coupon.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "优惠券"
|
||||
}
|
||||
43
wechat-mini-program/pages/coupon/coupon.wxml
Normal file
43
wechat-mini-program/pages/coupon/coupon.wxml
Normal file
@@ -0,0 +1,43 @@
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<text class="title">优惠券中心</text>
|
||||
</view>
|
||||
|
||||
<!-- VIP Banner -->
|
||||
<view class="vip-banner" wx:if="{{!user.phone}}">
|
||||
<view class="vip-info">
|
||||
<view class="vip-title">VIP会员权益</view>
|
||||
<view class="vip-desc">开通会员享受更多优惠</view>
|
||||
</view>
|
||||
<button class="vip-btn" bindtap="goToProfile">立即开通</button>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- Coupon List -->
|
||||
<view class="section-title">{{user.phone ? '可使用的优惠券' : '可领取的优惠券'}}</view>
|
||||
<view class="coupon-list" style="{{!user.phone ? 'filter: grayscale(100%); opacity: 0.6;' : ''}}">
|
||||
<block wx:for="{{coupons}}" wx:key="id">
|
||||
<view class="coupon-card {{item.status === 'used' ? 'used' : ''}}" bindtap="handleCouponClick" data-id="{{item.id}}" data-status="{{item.status}}">
|
||||
<view class="limit-tag" wx:if="{{item.is_time_limited}}">限时</view>
|
||||
<view class="coupon-left">
|
||||
<view class="amount">
|
||||
<text class="num">{{item.amount}}</text>
|
||||
<text class="unit">{{item.unit}}</text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<view class="coupon-name">{{item.title}}</view>
|
||||
<view class="coupon-desc">{{item.displayDesc}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coupon-right">
|
||||
<view class="btn-text" wx:if="{{item.status === 'can_claim'}}">立即领取</view>
|
||||
<view class="btn-text" wx:elif="{{item.status === 'assigned'}}">去使用</view>
|
||||
<view class="btn-text" wx:elif="{{item.status === 'used'}}">已使用</view>
|
||||
<view class="btn-text" wx:elif="{{item.status === 'expired'}}">已过期</view>
|
||||
<view class="btn-text" wx:else>去使用</view>
|
||||
<text class="expiry-date" wx:if="{{item.expiry}}">{{item.expiry}}到期</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
231
wechat-mini-program/pages/coupon/coupon.wxss
Normal file
231
wechat-mini-program/pages/coupon/coupon.wxss
Normal file
@@ -0,0 +1,231 @@
|
||||
.container {
|
||||
padding: 30rpx;
|
||||
background-color: var(--background-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
padding: 20rpx 0;
|
||||
text-align: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.vip-banner {
|
||||
background: linear-gradient(to bottom right, #111827, #1f2937);
|
||||
border-radius: 30rpx;
|
||||
padding: 40rpx;
|
||||
color: var(--text-white);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
.vip-title {
|
||||
color: #fcd34d;
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.vip-desc {
|
||||
color: #d1d5db;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.vip-btn {
|
||||
background: linear-gradient(to right, #fcd34d, #fbbf24);
|
||||
color: #111827;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
padding: 10rpx 30rpx;
|
||||
border-radius: 40rpx;
|
||||
margin: 0;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.coupon-list.disabled-list {
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
.coupon-card {
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Colorful style for unused coupons */
|
||||
.coupon-card:not(.used) {
|
||||
background: linear-gradient(135deg, #fff1f2 0%, #ffe4e6 100%); /* More saturated gradient */
|
||||
border: 1px solid #fecdd3;
|
||||
}
|
||||
|
||||
.coupon-card.used {
|
||||
background: #f9fafb;
|
||||
box-shadow: none;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.coupon-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Dashed line separator */
|
||||
.coupon-left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 20rpx;
|
||||
bottom: 20rpx;
|
||||
border-right: 2px dashed #e5e7eb;
|
||||
}
|
||||
|
||||
.coupon-card:not(.used) .coupon-left::after {
|
||||
border-right: 2px dashed #fca5a5; /* Reddish dashed line for unused */
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.amount {
|
||||
min-width: 120rpx;
|
||||
text-align: center;
|
||||
color: #ef4444; /* Red */
|
||||
margin-right: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.coupon-card.used .amount {
|
||||
color: #9ca3af; /* Gray */
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 56rpx;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 24rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.coupon-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.coupon-card.used .coupon-name {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.coupon-desc {
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.coupon-card:not(.used) .coupon-desc {
|
||||
color: #7f1d1d; /* Darker red for description on colorful bg */
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.coupon-right {
|
||||
width: 160rpx; /* Increased width to fit date */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.expiry-date {
|
||||
font-size: 18rpx;
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.coupon-card:not(.used) .expiry-date {
|
||||
color: #ef4444;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Active button style */
|
||||
.coupon-card:not(.used) .btn-text {
|
||||
background: linear-gradient(to right, #ef4444, #f87171);
|
||||
color: white;
|
||||
box-shadow: 0 4rpx 10rpx rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Used button style */
|
||||
.coupon-card.used .btn-text {
|
||||
background-color: #e5e7eb;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Circles for ticket effect */
|
||||
.coupon-card::before, .coupon-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background-color: var(--background-color); /* Assuming this var is set on page/container */
|
||||
border-radius: 50%;
|
||||
right: 148rpx; /* Position at the separator (adjusted for new width) */
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.coupon-card::before {
|
||||
top: -12rpx;
|
||||
}
|
||||
|
||||
.coupon-card::after {
|
||||
bottom: -12rpx;
|
||||
}
|
||||
|
||||
.limit-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(to bottom right, #f59e0b, #d97706);
|
||||
color: white;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-bottom-right-radius: 16rpx;
|
||||
z-index: 20;
|
||||
}
|
||||
148
wechat-mini-program/pages/course/course.js
Normal file
148
wechat-mini-program/pages/course/course.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const app = getApp()
|
||||
const { request } = require('../../utils/request')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
user: {},
|
||||
honors: [],
|
||||
projects: [],
|
||||
filteredProjects: [],
|
||||
currentFilter: 'all', // all, enrolled, completed
|
||||
showHonorModal: false,
|
||||
currentHonor: null
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.fetchHonors()
|
||||
this.fetchProjects()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.getUserInfo()
|
||||
},
|
||||
|
||||
getUserInfo() {
|
||||
const app = getApp()
|
||||
// Try to get from globalData first to speed up rendering
|
||||
if (app.globalData.userInfo) {
|
||||
this.setData({ user: app.globalData.userInfo })
|
||||
}
|
||||
|
||||
request({ url: '/user/' }).then(user => {
|
||||
app.globalData.userInfo = user;
|
||||
this.setData({ user: user });
|
||||
// Re-fetch data if needed based on user status change
|
||||
// this.fetchHonors();
|
||||
// this.fetchProjects();
|
||||
}).catch(err => {
|
||||
console.error('Failed to fetch user info', err)
|
||||
})
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
Promise.all([this.fetchHonors(), this.fetchProjects()]).then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
fetchHonors() {
|
||||
return request({
|
||||
url: '/user-honors/',
|
||||
method: 'GET'
|
||||
}).then(res => {
|
||||
this.setData({ honors: res || [] })
|
||||
}).catch(err => {
|
||||
console.error('Failed to fetch honors', err)
|
||||
})
|
||||
},
|
||||
|
||||
fetchProjects() {
|
||||
return request({
|
||||
url: '/user-projects/',
|
||||
method: 'GET'
|
||||
}).then(res => {
|
||||
const list = Array.isArray(res) ? res : []
|
||||
const projects = list.map(item => {
|
||||
// Format date
|
||||
if (item.enrolled_at) {
|
||||
item.enrolled_at_formatted = item.enrolled_at.split('T')[0]
|
||||
}
|
||||
|
||||
const statusMap = {
|
||||
'enrolled': '已报名',
|
||||
'studying': '在读',
|
||||
'graduated': '毕业',
|
||||
'finished': '结束',
|
||||
'completed': '已完成',
|
||||
'cancelled': '已取消'
|
||||
};
|
||||
item.status_display = statusMap[item.status] || item.status;
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
// Sort projects by status: enrolled, studying, finished, graduated
|
||||
const sortOrder = {
|
||||
'enrolled': 1,
|
||||
'studying': 2,
|
||||
'finished': 3,
|
||||
'graduated': 4
|
||||
};
|
||||
|
||||
projects.sort((a, b) => {
|
||||
const orderA = sortOrder[a.status] || 99;
|
||||
const orderB = sortOrder[b.status] || 99;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
this.setData({ projects })
|
||||
this.filterProjects()
|
||||
}).catch(err => {
|
||||
console.error('Failed to fetch projects', err)
|
||||
})
|
||||
},
|
||||
|
||||
setFilter(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.setData({ currentFilter: type })
|
||||
this.filterProjects()
|
||||
},
|
||||
|
||||
filterProjects() {
|
||||
const { projects, currentFilter } = this.data
|
||||
let filtered = []
|
||||
if (currentFilter === 'all') {
|
||||
filtered = projects
|
||||
} else if (currentFilter === 'enrolled') {
|
||||
// 进行中: 已报名, 在读
|
||||
const activeStatuses = ['enrolled', 'studying']
|
||||
filtered = projects.filter(p => activeStatuses.includes(p.status))
|
||||
} else if (currentFilter === 'completed') {
|
||||
// 已完成: 毕业, 结束, 已完成
|
||||
const completedStatuses = ['graduated', 'finished', 'completed']
|
||||
filtered = projects.filter(p => completedStatuses.includes(p.status))
|
||||
} else {
|
||||
filtered = projects.filter(p => p.status === currentFilter)
|
||||
}
|
||||
this.setData({ filteredProjects: filtered })
|
||||
},
|
||||
|
||||
showHonorDetail(e) {
|
||||
const item = e.currentTarget.dataset.item
|
||||
this.setData({
|
||||
showHonorModal: true,
|
||||
currentHonor: item
|
||||
})
|
||||
},
|
||||
|
||||
closeHonorModal() {
|
||||
this.setData({
|
||||
showHonorModal: false,
|
||||
currentHonor: null
|
||||
})
|
||||
},
|
||||
|
||||
preventBubble() {
|
||||
// Prevent tap event from bubbling to mask
|
||||
}
|
||||
})
|
||||
6
wechat-mini-program/pages/course/course.json
Normal file
6
wechat-mini-program/pages/course/course.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "项目&活动",
|
||||
"navigationBarBackgroundColor": "#ff9900",
|
||||
"navigationBarTextStyle": "white",
|
||||
"usingComponents": {}
|
||||
}
|
||||
72
wechat-mini-program/pages/course/course.wxml
Normal file
72
wechat-mini-program/pages/course/course.wxml
Normal file
@@ -0,0 +1,72 @@
|
||||
<view class="page-container">
|
||||
<block wx:if="{{user.phone}}">
|
||||
<!-- 头部荣誉区域 -->
|
||||
<view class="honor-section" wx:if="{{honors.length > 0}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">我的荣誉</text>
|
||||
<text class="honor-count">共 {{honors.length}} 项</text>
|
||||
</view>
|
||||
|
||||
<view class="honor-list">
|
||||
<view class="honor-item" wx:for="{{honors}}" wx:key="id" bindtap="showHonorDetail" data-item="{{item}}">
|
||||
<image class="honor-image" src="{{item.image}}" mode="aspectFill"></image>
|
||||
<view class="honor-info">
|
||||
<text class="honor-title">{{item.title}}</text>
|
||||
<text class="honor-date">{{item.date}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 课程赛事列表 -->
|
||||
<view class="course-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">我的项目&活动</text>
|
||||
</view>
|
||||
|
||||
<view class="filter-tabs">
|
||||
<view class="tab-item {{currentFilter === 'all' ? 'active' : ''}}" bindtap="setFilter" data-type="all">全部</view>
|
||||
<view class="tab-item {{currentFilter === 'enrolled' ? 'active' : ''}}" bindtap="setFilter" data-type="enrolled">进行中</view>
|
||||
<view class="tab-item {{currentFilter === 'completed' ? 'active' : ''}}" bindtap="setFilter" data-type="completed">已完成</view>
|
||||
</view>
|
||||
|
||||
<view class="project-list" wx:if="{{filteredProjects.length > 0}}">
|
||||
<view class="project-card" wx:for="{{filteredProjects}}" wx:key="id">
|
||||
<image class="project-image" src="{{item.project_image}}" mode="aspectFill"></image>
|
||||
<view class="project-info">
|
||||
<view class="project-header">
|
||||
<text class="project-title">{{item.project_title}}</text>
|
||||
<text class="project-type-tag {{item.project_type}}">{{item.project_type_display}}</text>
|
||||
</view>
|
||||
<view class="project-meta">
|
||||
<text class="status-text {{item.status}}">{{item.status_display}}</text>
|
||||
<text class="date-text">报名时间: {{item.enrolled_at_formatted}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-state" wx:else>
|
||||
<text class="empty-text">暂无相关记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view class="empty-state-global" wx:else style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 200rpx;">
|
||||
<image src="/assets/empty-honor.png" mode="aspectFit" style="width: 200rpx; height: 200rpx; margin-bottom: 20rpx; filter: grayscale(100%); opacity: 0.5;"></image>
|
||||
<text style="color: #999; font-size: 28rpx;">暂无数据,请先完善个人信息</text>
|
||||
</view>
|
||||
|
||||
<!-- 荣誉详情弹窗 -->
|
||||
<view class="modal-mask" wx:if="{{showHonorModal}}" bindtap="closeHonorModal">
|
||||
<view class="modal-content" catchtap="preventBubble">
|
||||
<image class="modal-image" src="{{currentHonor.image}}" mode="widthFix"></image>
|
||||
<view class="modal-info">
|
||||
<text class="modal-title">{{currentHonor.title}}</text>
|
||||
<text class="modal-date">获得时间:{{currentHonor.date}}</text>
|
||||
<text class="modal-desc" wx:if="{{currentHonor.description}}">{{currentHonor.description}}</text>
|
||||
</view>
|
||||
<view class="modal-close" bindtap="closeHonorModal">×</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
309
wechat-mini-program/pages/course/course.wxss
Normal file
309
wechat-mini-program/pages/course/course.wxss
Normal file
@@ -0,0 +1,309 @@
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background-color: var(--background-color);
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* Honor Section */
|
||||
.honor-section {
|
||||
background-color: var(--surface-color);
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border-bottom-left-radius: 30rpx;
|
||||
border-bottom-right-radius: 30rpx;
|
||||
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
position: relative;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8rpx;
|
||||
height: 32rpx;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.honor-count {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.honor-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.honor-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: 10rpx;
|
||||
margin-right: -20rpx; /* Compensate for the right margin of items */
|
||||
}
|
||||
|
||||
.honor-item {
|
||||
display: inline-block;
|
||||
width: calc(50% - 20rpx); /* 2 items per row with gap */
|
||||
margin-right: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid var(--border-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.honor-image {
|
||||
width: 100%;
|
||||
height: 160rpx;
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.honor-info {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.honor-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #374151;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.honor-date {
|
||||
font-size: 20rpx;
|
||||
color: var(--text-light);
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Course Section */
|
||||
.course-section {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
background-color: #e5e7eb;
|
||||
border-radius: 16rpx;
|
||||
padding: 6rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
padding: 12rpx 0;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background-color: var(--surface-color);
|
||||
color: var(--primary-color);
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.project-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.project-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: var(--surface-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-type-tag {
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.project-type-tag.training {
|
||||
background-color: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.project-type-tag.competition {
|
||||
background-color: var(--warning-bg);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.project-type-tag.grading {
|
||||
background-color: var(--success-bg);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-text.enrolled {
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
.status-text.studying {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.status-text.completed {
|
||||
color: #10b981; /* Keep or use success-color */
|
||||
}
|
||||
|
||||
.status-text.cancelled {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 20rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 80%;
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx;
|
||||
position: relative;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.modal-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.modal-date {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.modal-desc {
|
||||
font-size: 28rpx;
|
||||
color: #4b5563;
|
||||
line-height: 1.6;
|
||||
text-align: left;
|
||||
}
|
||||
173
wechat-mini-program/pages/detail/detail.js
Normal file
173
wechat-mini-program/pages/detail/detail.js
Normal file
@@ -0,0 +1,173 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
project: null,
|
||||
bestCoupon: null,
|
||||
finalPrice: 0,
|
||||
isEnrolled: false,
|
||||
tagStyle: {
|
||||
video: 'width: 100%;',
|
||||
img: 'width: 100%; height: auto; display: block;'
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
const { id } = options;
|
||||
this.fetchProject(id);
|
||||
this.checkEnrollment(id);
|
||||
},
|
||||
checkEnrollment(projectId) {
|
||||
const user = app.globalData.userInfo;
|
||||
if (!user) return;
|
||||
|
||||
const { request } = require('../../utils/request')
|
||||
request({
|
||||
url: `/student-projects/?student=${user.id}&project=${projectId}`
|
||||
}).then(res => {
|
||||
// Search result from list
|
||||
if (res && res.results && res.results.length > 0) {
|
||||
this.setData({ isEnrolled: true });
|
||||
} else if (Array.isArray(res) && res.length > 0) {
|
||||
this.setData({ isEnrolled: true });
|
||||
}
|
||||
}).catch(err => console.error('Check enrollment failed', err));
|
||||
},
|
||||
fetchProject(id) {
|
||||
const { request } = require('../../utils/request')
|
||||
// Fetch project details
|
||||
request({ url: `/projects/${id}/` })
|
||||
.then((data) => {
|
||||
this.setData({ project: data })
|
||||
// Fetch coupons to calculate best discount
|
||||
if (parseFloat(data.price) > 0 && data.show_price) {
|
||||
this.fetchCoupons(data.id, data.price);
|
||||
} else {
|
||||
this.setData({
|
||||
finalPrice: data.price,
|
||||
bestCoupon: null,
|
||||
bestCouponId: null
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Mock data fallback
|
||||
this.setData({
|
||||
project: {
|
||||
id: id,
|
||||
title: '示例项目',
|
||||
category: '示例分类',
|
||||
rating: 4.8,
|
||||
students: 120,
|
||||
duration: '4周',
|
||||
price: 299,
|
||||
show_price: true,
|
||||
detail: '<p>这是一个示例项目详情。</p>',
|
||||
image: 'https://images.unsplash.com/photo-1526379095098-d400fd0bf935'
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
fetchCoupons(projectId, originalPrice) {
|
||||
const { request } = require('../../utils/request')
|
||||
request({ url: '/user-coupons/' }).then(coupons => {
|
||||
// Filter applicable coupons
|
||||
const validCoupons = coupons.filter(sc => {
|
||||
// Check if coupon is applicable to this project
|
||||
const c = sc.coupon_detail;
|
||||
const isApplicable = c.applicable_project_titles.length === 0 || c.applicable_project_titles.includes(this.data.project.title);
|
||||
return isApplicable && sc.status === 'assigned';
|
||||
});
|
||||
|
||||
if (validCoupons.length > 0) {
|
||||
// Find the best coupon (highest discount)
|
||||
let maxDiscount = 0;
|
||||
let bestCoupon = null;
|
||||
|
||||
validCoupons.forEach(sc => {
|
||||
const c = sc.coupon_detail;
|
||||
const discount = parseFloat(c.amount);
|
||||
if (discount > maxDiscount) {
|
||||
maxDiscount = discount;
|
||||
bestCoupon = sc; // Keep the student_coupon object to get ID
|
||||
}
|
||||
});
|
||||
|
||||
if (bestCoupon) {
|
||||
const finalPrice = Math.max(0, originalPrice - maxDiscount);
|
||||
this.setData({
|
||||
bestCoupon: bestCoupon.coupon_detail, // For display
|
||||
bestCouponId: bestCoupon.id, // For submission (StudentCoupon ID)
|
||||
finalPrice: finalPrice.toFixed(2)
|
||||
});
|
||||
} else {
|
||||
this.setData({ finalPrice: originalPrice });
|
||||
}
|
||||
} else {
|
||||
this.setData({ finalPrice: originalPrice });
|
||||
}
|
||||
}).catch(() => {
|
||||
this.setData({ finalPrice: originalPrice });
|
||||
});
|
||||
},
|
||||
handleEnroll() {
|
||||
const { request } = require('../../utils/request')
|
||||
const user = app.globalData.userInfo;
|
||||
|
||||
if (!user) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.data.isEnrolled) {
|
||||
wx.showToast({ title: '您已报名该项目', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
student: user.id,
|
||||
project: this.data.project.id,
|
||||
coupon_id: this.data.bestCouponId || null
|
||||
};
|
||||
|
||||
wx.showLoading({ title: '报名中...' });
|
||||
|
||||
request({
|
||||
url: '/student-projects/',
|
||||
method: 'POST',
|
||||
data: payload
|
||||
}).then(res => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '报名成功', icon: 'success' });
|
||||
this.setData({ isEnrolled: true });
|
||||
}).catch(err => {
|
||||
wx.hideLoading();
|
||||
console.error('Enrollment error:', err);
|
||||
|
||||
let msg = '报名失败';
|
||||
if (err.data) {
|
||||
if (err.data.detail) {
|
||||
msg = err.data.detail;
|
||||
} else if (err.data.coupon_id) {
|
||||
msg = Array.isArray(err.data.coupon_id) ? err.data.coupon_id[0] : err.data.coupon_id;
|
||||
} else if (typeof err.data === 'string') {
|
||||
msg = err.data;
|
||||
} else {
|
||||
// Try to find first error value
|
||||
const keys = Object.keys(err.data);
|
||||
if (keys.length > 0) {
|
||||
const firstVal = err.data[keys[0]];
|
||||
msg = Array.isArray(firstVal) ? firstVal[0] : firstVal;
|
||||
}
|
||||
}
|
||||
} else if (err.detail) {
|
||||
msg = err.detail;
|
||||
}
|
||||
|
||||
wx.showToast({ title: msg, icon: 'none' });
|
||||
|
||||
if (msg && (msg.includes('重复') || msg.includes('exist'))) {
|
||||
this.setData({ isEnrolled: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
8
wechat-mini-program/pages/detail/detail.json
Normal file
8
wechat-mini-program/pages/detail/detail.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "详情",
|
||||
"navigationBarBackgroundColor": "#ff9900",
|
||||
"navigationBarTextStyle": "white",
|
||||
"usingComponents": {
|
||||
"mp-html": "/components/mp-html/dist/mp-weixin/index"
|
||||
}
|
||||
}
|
||||
48
wechat-mini-program/pages/detail/detail.wxml
Normal file
48
wechat-mini-program/pages/detail/detail.wxml
Normal file
@@ -0,0 +1,48 @@
|
||||
<view class="container" wx:if="{{project}}">
|
||||
<image class="hero-image" src="{{project.image}}" mode="aspectFill"></image>
|
||||
|
||||
<view class="content">
|
||||
<view class="header">
|
||||
<view class="badge">{{project.category}}</view>
|
||||
<view class="rating">
|
||||
{{project.address}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="title">{{project.title}}</view>
|
||||
|
||||
<view class="meta-row">
|
||||
<view class="meta-item">
|
||||
<text class="icon">👥</text> {{project.students}} 人已报名
|
||||
</view>
|
||||
<view class="meta-item">
|
||||
<text class="icon">🕒</text> {{project.duration}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="desc">
|
||||
<mp-html content="{{project.detail}}" tag-style="{{tagStyle}}" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-bar">
|
||||
<view class="price-container" wx:if="{{project.show_price}}">
|
||||
<block wx:if="{{bestCoupon}}">
|
||||
<view class="price-row">
|
||||
<view class="final-price">¥ {{finalPrice}}</view>
|
||||
<view class="original-price">¥ {{project.price}}</view>
|
||||
</view>
|
||||
<view class="coupon-tip">已抵扣{{bestCoupon.amount}}元</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="final-price" style="color: #ff9900;">¥ {{project.price}}</view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="price-container" wx:else>
|
||||
<view class="contact-tip" style="color: #666; font-size: 28rpx;">请咨询客服</view>
|
||||
</view>
|
||||
<button class="enroll-btn" bindtap="handleEnroll" disabled="{{isEnrolled}}" style="{{isEnrolled ? 'background-color: #ccc;' : ''}}">{{isEnrolled ? '已报名' : '立即报名'}}</button>
|
||||
</view>
|
||||
</view>
|
||||
172
wechat-mini-program/pages/detail/detail.wxss
Normal file
172
wechat-mini-program/pages/detail/detail.wxss
Normal file
@@ -0,0 +1,172 @@
|
||||
.container {
|
||||
background-color: var(--surface-color);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
}
|
||||
.content {
|
||||
padding: 40rpx;
|
||||
border-top-left-radius: 40rpx;
|
||||
border-top-right-radius: 40rpx;
|
||||
margin-top: -40rpx;
|
||||
background-color: var(--surface-color);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.badge {
|
||||
background-color: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.rating {
|
||||
color: var(--warning-color);
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 20rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.meta-row {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.meta-item {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 20rpx;
|
||||
border-left: 8rpx solid var(--primary-color);
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
.desc {
|
||||
font-size: 28rpx;
|
||||
color: #4b5563;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.chapter-header {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #374151;
|
||||
margin: 30rpx 0 20rpx 0;
|
||||
background-color: var(--surface-secondary);
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
.lesson-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
.lesson-info {
|
||||
flex: 1;
|
||||
}
|
||||
.lesson-title {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.lesson-time {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-light);
|
||||
}
|
||||
.play-btn {
|
||||
color: var(--primary-color);
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.lock-btn {
|
||||
color: var(--text-light);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.footer-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--surface-color);
|
||||
padding: 20rpx 40rpx;
|
||||
box-shadow: 0 -4rpx 10rpx rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 20;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.price-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.final-price {
|
||||
color: var(--price-color);
|
||||
font-weight: bold;
|
||||
font-size: 36rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
text-decoration: line-through;
|
||||
color: var(--text-light);
|
||||
font-size: 24rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.coupon-tip {
|
||||
font-size: 20rpx;
|
||||
color: var(--price-color);
|
||||
border: 1px solid var(--price-color);
|
||||
padding: 2rpx 6rpx;
|
||||
border-radius: 4rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.enroll-btn {
|
||||
background: linear-gradient(to right, var(--primary-gradient-start), var(--primary-gradient-end));
|
||||
color: var(--text-white);
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
padding: 20rpx 60rpx;
|
||||
border-radius: 50rpx;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
158
wechat-mini-program/pages/index/index.js
Normal file
158
wechat-mini-program/pages/index/index.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
projects: [],
|
||||
banners: [],
|
||||
categories: [],
|
||||
userInfo: {},
|
||||
hasUserInfo: false,
|
||||
canIUse: wx.canIUse('button.open-type.getUserInfo'),
|
||||
canIUseGetUserProfile: false,
|
||||
canIUseOpenData: wx.canIUse('open-data.type.userAvatarUrl') && wx.canIUse('open-data.type.userNickName'), // 如需尝试获取用户信息可改为false
|
||||
selectedCategory: 'all',
|
||||
showcases: [],
|
||||
categoryStats: {}
|
||||
},
|
||||
onLoad() {
|
||||
if (wx.getUserProfile) {
|
||||
this.setData({
|
||||
canIUseGetUserProfile: true
|
||||
})
|
||||
}
|
||||
this.fetchData();
|
||||
this.fetchBanners();
|
||||
this.fetchShowcases();
|
||||
this.fetchCategoryStats();
|
||||
},
|
||||
handleCategorySelect(e) {
|
||||
const type = e.currentTarget.dataset.type;
|
||||
this.setData({ selectedCategory: type });
|
||||
this.fetchData(type);
|
||||
},
|
||||
fetchBanners() {
|
||||
const { request } = require('../../utils/request')
|
||||
request({ url: '/banners/?is_active=true' })
|
||||
.then(data => {
|
||||
const list = Array.isArray(data) ? data : (data && data.results) || []
|
||||
this.setData({ banners: list })
|
||||
})
|
||||
.catch(err => console.error('Fetch banners error:', err))
|
||||
},
|
||||
handleBannerClick(e) {
|
||||
const item = e.currentTarget.dataset.item;
|
||||
if (item.project) {
|
||||
wx.navigateTo({
|
||||
url: `/pages/detail/detail?id=${item.project}`
|
||||
})
|
||||
} else if (item.link) {
|
||||
// Simple link handling (copy or webview)
|
||||
// For simplicity, if it starts with http, copy it
|
||||
if (item.link.startsWith('http')) {
|
||||
wx.setClipboardData({
|
||||
data: item.link,
|
||||
success: () => wx.showToast({ title: '链接已复制', icon: 'none' })
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
fetchShowcases() {
|
||||
const { request } = require('../../utils/request')
|
||||
request({ url: '/student-showcases/?is_active=true' })
|
||||
.then(data => {
|
||||
const list = Array.isArray(data) ? data : (data && data.results) || []
|
||||
this.setData({ showcases: list })
|
||||
})
|
||||
.catch(err => console.error('Fetch showcases error:', err))
|
||||
},
|
||||
playVideo(e) {
|
||||
let url = e.currentTarget.dataset.url;
|
||||
if (!url) return;
|
||||
|
||||
// Fix for local development: replace localhost/127.0.0.1 with actual API host
|
||||
if (url.includes('localhost') || url.includes('127.0.0.1')) {
|
||||
const app = getApp();
|
||||
// Extract host from baseUrl (e.g. http://127.0.0.1:8000/api -> http://127.0.0.1:8000)
|
||||
const apiBase = app.globalData.baseUrl.split('/api')[0];
|
||||
// Replace the localhost part
|
||||
url = url.replace(/http:\/\/localhost:\d+/, apiBase)
|
||||
.replace(/http:\/\/127.0.0.1:\d+/, apiBase);
|
||||
}
|
||||
|
||||
// Use previewMedia for all video urls as our backend uploads files directly now
|
||||
wx.previewMedia({
|
||||
sources: [{ url: url, type: 'video' }],
|
||||
success: () => console.log('Preview success'),
|
||||
fail: (err) => {
|
||||
console.error('Preview failed', err);
|
||||
// Fallback for non-previewable links
|
||||
wx.setClipboardData({
|
||||
data: url,
|
||||
success: () => wx.showToast({ title: '视频链接已复制', icon: 'none' })
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
fetchData(type = 'all') {
|
||||
const { request } = require('../../utils/request')
|
||||
let url = '/projects/';
|
||||
if (type !== 'all') {
|
||||
url += `?project_type=${type}`;
|
||||
}
|
||||
|
||||
request({ url: url })
|
||||
.then((data) => {
|
||||
const list = Array.isArray(data) ? data : (data && data.results) || []
|
||||
this.setData({ projects: list })
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
this.setData({
|
||||
projects: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Python 数据分析实战',
|
||||
category: '编程开发',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1526379095098-d400fd0bf935?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
|
||||
students: 1205,
|
||||
rating: 4.8,
|
||||
duration: '12 周'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '零基础英语口语速成',
|
||||
category: '语言学习',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1543269865-cbf427effbad?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3',
|
||||
students: 850,
|
||||
rating: 4.9,
|
||||
duration: '8 周'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
},
|
||||
fetchCategoryStats() {
|
||||
const { request } = require('../../utils/request')
|
||||
request({ url: '/dashboard/stats' })
|
||||
.then(res => {
|
||||
const stats = {};
|
||||
if (res.pie_chart_data) {
|
||||
res.pie_chart_data.forEach(item => {
|
||||
if (item.type) {
|
||||
stats[item.type] = item.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.setData({ categoryStats: stats });
|
||||
})
|
||||
.catch(err => console.error('Fetch stats error:', err))
|
||||
},
|
||||
goToDetail(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
wx.navigateTo({
|
||||
url: `/pages/detail/detail?id=${id}`,
|
||||
})
|
||||
}
|
||||
})
|
||||
6
wechat-mini-program/pages/index/index.json
Normal file
6
wechat-mini-program/pages/index/index.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "首页",
|
||||
"navigationBarBackgroundColor": "#ff9900",
|
||||
"navigationBarTextStyle": "white",
|
||||
"usingComponents": {}
|
||||
}
|
||||
95
wechat-mini-program/pages/index/index.wxml
Normal file
95
wechat-mini-program/pages/index/index.wxml
Normal file
@@ -0,0 +1,95 @@
|
||||
<view class="page-container">
|
||||
<!-- Header -->
|
||||
<view class="header">
|
||||
<view class="header-content">
|
||||
<view class="title-container">
|
||||
<view class="title-sub">致力成就</view>
|
||||
<view class="title-main">终身教育伟大事业</view>
|
||||
</view>
|
||||
<view class="bell-btn">🔔</view>
|
||||
</view>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<view class="search-bar">
|
||||
<icon type="search" size="16" color="#9ca3af"></icon>
|
||||
<input type="text" placeholder="搜索项目名称..." placeholder-class="search-placeholder"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content-body">
|
||||
<!-- Banner Swiper -->
|
||||
<view class="banner-swiper-container">
|
||||
<swiper class="banner-swiper" indicator-dots="{{true}}" autoplay="{{true}}" interval="{{5000}}" duration="{{500}}" circular="{{true}}" indicator-active-color="#ffffff">
|
||||
<block wx:for="{{banners}}" wx:key="id">
|
||||
<swiper-item>
|
||||
<view class="banner-item" bindtap="handleBannerClick" data-item="{{item}}">
|
||||
<image src="{{item.image}}" mode="aspectFill" class="banner-image"></image>
|
||||
<!-- Optional: Overlay text if needed, but usually banners are just images -->
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<!-- Categories -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">热门分类</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="categories-scroll" enable-flex>
|
||||
<view class="category-item {{selectedCategory === 'all' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="all">
|
||||
<text>全部</text>
|
||||
</view>
|
||||
<view class="category-item {{selectedCategory === 'training' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="training" wx:if="{{categoryStats.training > 0}}">
|
||||
<text>在职项目</text>
|
||||
</view>
|
||||
<view class="category-item {{selectedCategory === 'competition' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="competition" wx:if="{{categoryStats.competition > 0}}">
|
||||
<text>典礼&论坛</text>
|
||||
</view>
|
||||
<view class="category-item {{selectedCategory === 'grading' ? 'active' : ''}}" bindtap="handleCategorySelect" data-type="grading" wx:if="{{categoryStats.grading > 0}}">
|
||||
<text>校友活动</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- Projects -->
|
||||
<view class="projects-list">
|
||||
<block wx:for="{{projects}}" wx:key="id">
|
||||
<view class="project-card" bindtap="goToDetail" data-id="{{item.id}}">
|
||||
<image class="project-image" src="{{item.image}}" mode="aspectFill"></image>
|
||||
<view class="project-info">
|
||||
<view class="project-header-row">
|
||||
<view class="project-title">{{item.title}}</view>
|
||||
<view class="project-category-tag" wx:if="{{item.project_type_display || item.category}}">{{item.project_type_display || item.category}}</view>
|
||||
</view>
|
||||
<view class="project-teacher" wx:if="{{item.teacher_name}}">
|
||||
{{item.teacher_name}}
|
||||
</view>
|
||||
<view class="project-meta">
|
||||
<view class="meta-item"><text class="icon">👥</text> {{item.students}}</view>
|
||||
<view class="meta-item">{{item.address}}</view>
|
||||
<view class="meta-item"><text class="icon">🕒</text> {{item.duration}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- Student Showcase -->
|
||||
<view class="section" wx:if="{{showcases && showcases.length > 0}}" style="margin-top: 40rpx;">
|
||||
<view class="section-header">
|
||||
<text class="section-title">精彩视频</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="showcase-scroll" enable-flex>
|
||||
<view class="showcase-item" wx:for="{{showcases}}" wx:key="id" bindtap="playVideo" data-url="{{item.video_url}}">
|
||||
<image class="showcase-image" src="{{item.cover_image}}" mode="aspectFill"></image>
|
||||
<view class="showcase-info">
|
||||
<text class="showcase-title">{{item.title}}</text>
|
||||
<text class="showcase-student" wx:if="{{item.student_name}}">{{item.student_name}}</text>
|
||||
</view>
|
||||
<view class="play-icon" wx:if="{{item.video_url}}">▶</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
316
wechat-mini-program/pages/index/index.wxss
Normal file
316
wechat-mini-program/pages/index/index.wxss
Normal file
@@ -0,0 +1,316 @@
|
||||
.page-container {
|
||||
padding-bottom: 50rpx;
|
||||
background-color: var(--background-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #ffaa33 0%, #ff8800 100%);
|
||||
padding: 40rpx 40rpx 100rpx 40rpx;
|
||||
border-bottom-left-radius: 50rpx;
|
||||
border-bottom-right-radius: 50rpx;
|
||||
position: relative;
|
||||
box-shadow: 0 10rpx 20rpx rgba(255, 153, 0, 0.2);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10rpx;
|
||||
padding-top: 10rpx;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.title-sub {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 34rpx;
|
||||
letter-spacing: 4rpx;
|
||||
margin-bottom: 8rpx;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.title-main {
|
||||
color: var(--text-white);
|
||||
font-size: 60rpx;
|
||||
font-weight: normal;
|
||||
font-family: "Xingkai SC", "STXingkai", "STKaiti", "KaiTi", "楷体", "cursive";
|
||||
letter-spacing: 2rpx;
|
||||
text-shadow: 0 2rpx 6rpx rgba(0,0,0,0.15);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.bell-btn {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 16rpx;
|
||||
border-radius: 50%;
|
||||
color: var(--text-white);
|
||||
font-size: 32rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: absolute;
|
||||
bottom: -40rpx;
|
||||
left: 30rpx;
|
||||
right: 30rpx;
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 30rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
margin-left: 20rpx;
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding: 60rpx 30rpx 0 30rpx;
|
||||
}
|
||||
|
||||
.banner-swiper-container {
|
||||
border-radius: 30rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.banner-swiper {
|
||||
height: 300rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.banner-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 30rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.see-all {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.categories-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: inline-block;
|
||||
padding: 16rpx 40rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
margin-right: 20rpx;
|
||||
font-weight: 500;
|
||||
background-color: var(--surface-secondary);
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.category-item.active {
|
||||
background-color: var(--primary-light);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.projects-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background-color: var(--surface-color);
|
||||
padding: 24rpx;
|
||||
border-radius: 30rpx;
|
||||
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.project-image {
|
||||
width: 192rpx;
|
||||
height: 192rpx;
|
||||
border-radius: 20rpx;
|
||||
flex-shrink: 0;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.project-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.project-category-tag {
|
||||
font-size: 20rpx;
|
||||
color: var(--primary-color);
|
||||
background-color: var(--primary-light);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10rpx; /* Ensure space from teacher */
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 20rpx;
|
||||
color: var(--text-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
/* Project Teacher */
|
||||
.project-teacher {
|
||||
font-size: 24rpx; /* Slightly larger */
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: auto;
|
||||
margin-top: auto; /* Center vertically in remaining space */
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Student Showcase */
|
||||
.showcase-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.showcase-item {
|
||||
display: inline-block;
|
||||
width: 320rpx;
|
||||
height: 220rpx;
|
||||
margin-right: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.showcase-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.showcase-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20rpx 16rpx;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
|
||||
color: var(--text-white);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.showcase-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.showcase-student {
|
||||
font-size: 20rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
color: var(--text-white);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
131
wechat-mini-program/pages/login/login.js
Normal file
131
wechat-mini-program/pages/login/login.js
Normal file
@@ -0,0 +1,131 @@
|
||||
const app = getApp()
|
||||
const { request } = require('../../utils/request')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
hasUserInfo: false,
|
||||
hasPhone: false
|
||||
},
|
||||
onLoad() {
|
||||
this.checkStatus();
|
||||
},
|
||||
checkStatus() {
|
||||
// If the user is already here, we check if they are valid.
|
||||
// However, app.js handles the redirection for existing users.
|
||||
// If we are here, it means either app.js sent us here (New User),
|
||||
// or app.js hasn't finished checking yet,
|
||||
// or the user manually navigated here (unlikely).
|
||||
|
||||
const user = app.globalData.userInfo;
|
||||
if (user) {
|
||||
// We can't rely on is_new_user here because we don't persist it in globalData (though we could).
|
||||
// But based on new logic:
|
||||
// If user exists, app.js wouldn't redirect here.
|
||||
// If we are here, user probably needs to authorize.
|
||||
|
||||
// Update UI state
|
||||
const hasNick = user.wechat_nickname && user.wechat_nickname !== '微信用户';
|
||||
const hasPhone = !!user.phone;
|
||||
|
||||
this.setData({
|
||||
hasUserInfo: hasNick,
|
||||
hasPhone: hasPhone
|
||||
})
|
||||
|
||||
// If somehow we ended up here but profile is full, go home
|
||||
if (hasNick && hasPhone) {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
} else {
|
||||
// Wait for app.js login callback
|
||||
app.loginCallback = (user) => {
|
||||
// Re-run check
|
||||
this.checkStatus();
|
||||
// If app.js determines it's an existing user, it might redirect.
|
||||
// But if not, we update UI here.
|
||||
}
|
||||
}
|
||||
},
|
||||
getUserProfile(e) {
|
||||
wx.getUserProfile({
|
||||
desc: '用于完善会员资料',
|
||||
success: (res) => {
|
||||
const { userInfo } = res
|
||||
console.log('getUserProfile success', userInfo)
|
||||
|
||||
// Update backend
|
||||
// We need user ID from globalData
|
||||
if (app.globalData.userInfo && app.globalData.userInfo.id) {
|
||||
this.updateUserInfo(userInfo)
|
||||
} else {
|
||||
// Should not happen if app.js login succeeded
|
||||
wx.showToast({ title: '登录状态异常,请重启', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('getUserProfile failed', err)
|
||||
wx.showToast({ title: '需要授权才能继续', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
updateUserInfo(wxUserInfo) {
|
||||
request({
|
||||
url: `/students/${app.globalData.userInfo.id}/`,
|
||||
method: 'PATCH',
|
||||
data: {
|
||||
wechat_nickname: wxUserInfo.nickName,
|
||||
avatar: wxUserInfo.avatarUrl
|
||||
// name: wxUserInfo.nickName // Optional: sync name too
|
||||
}
|
||||
}).then(res => {
|
||||
console.log('Update user info success', res)
|
||||
app.globalData.userInfo = { ...app.globalData.userInfo, ...res }
|
||||
this.setData({ hasUserInfo: true })
|
||||
}).catch(err => {
|
||||
console.error('Update user info failed', err)
|
||||
wx.showToast({ title: '更新资料失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
getPhoneNumber(e) {
|
||||
console.log('getPhoneNumber', e)
|
||||
if (e.detail.errMsg === 'getPhoneNumber:ok') {
|
||||
request({
|
||||
url: '/user/phone/',
|
||||
method: 'POST',
|
||||
data: {
|
||||
code: e.detail.code
|
||||
}
|
||||
}).then(res => {
|
||||
console.log('Get phone success', res)
|
||||
if (res.phone) {
|
||||
this.setData({ hasPhone: true })
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.phone = res.phone
|
||||
}
|
||||
|
||||
wx.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// Redirect to home
|
||||
setTimeout(() => {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}, 1500)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Get phone failed', err)
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
})
|
||||
} else {
|
||||
console.log('User denied phone number')
|
||||
// If user denies, we can optionally let them in anyway if that's the requirement?
|
||||
// User said: "If openID exists, direct login".
|
||||
// This implies for NEW users, they MIGHT need to authorize phone.
|
||||
// But strictly speaking, once they authorized UserProfile (Avatar/Nick),
|
||||
// the OpenID exists.
|
||||
// But the "User Phone" part is usually critical.
|
||||
// Let's keep strict requirement for phone for NEW users for now unless asked otherwise.
|
||||
}
|
||||
}
|
||||
})
|
||||
3
wechat-mini-program/pages/login/login.json
Normal file
3
wechat-mini-program/pages/login/login.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "用户登录"
|
||||
}
|
||||
20
wechat-mini-program/pages/login/login.wxml
Normal file
20
wechat-mini-program/pages/login/login.wxml
Normal file
@@ -0,0 +1,20 @@
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<!-- Use a generic icon if specific one not found, or use text -->
|
||||
<image class="logo" src="/assets/logo.png" mode="aspectFit"></image>
|
||||
<view class="title">正在登录</view>
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<view class="tips">为了提供更好的服务,请授权以下信息</view>
|
||||
|
||||
<block wx:if="{{!hasUserInfo}}">
|
||||
<button class="btn-login" bindtap="getUserProfile">获取微信昵称</button>
|
||||
</block>
|
||||
|
||||
<block wx:if="{{hasUserInfo && !hasPhone}}">
|
||||
<button class="btn-login" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">获取手机号码</button>
|
||||
</block>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
56
wechat-mini-program/pages/login/login.wxss
Normal file
56
wechat-mini-program/pages/login/login.wxss
Normal file
@@ -0,0 +1,56 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-top: 100rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 120rpx;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 30rpx;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 80%;
|
||||
background-color: #07c160;
|
||||
color: white;
|
||||
border-radius: 44rpx;
|
||||
margin-bottom: 40rpx;
|
||||
font-size: 32rpx;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
.btn-login:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
339
wechat-mini-program/pages/profile/profile.js
Normal file
339
wechat-mini-program/pages/profile/profile.js
Normal file
@@ -0,0 +1,339 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
user: {},
|
||||
isFormOpen: false,
|
||||
isDevtools: false,
|
||||
formData: {
|
||||
name: '',
|
||||
phone: '',
|
||||
age: '',
|
||||
company_name: '',
|
||||
position: '',
|
||||
wechat_nickname: '',
|
||||
avatar: ''
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
this.setData({ isDevtools: sys.platform === 'devtools' })
|
||||
} catch (e) {}
|
||||
// 检查是否已登录
|
||||
const app = getApp();
|
||||
if (app.globalData.userInfo) {
|
||||
this.setData({ user: app.globalData.userInfo });
|
||||
this.initFormData(app.globalData.userInfo);
|
||||
} else {
|
||||
// 设置回调,等待登录完成
|
||||
app.loginCallback = (user) => {
|
||||
this.setData({ user: user });
|
||||
this.initFormData(user);
|
||||
}
|
||||
}
|
||||
this.fetchUser(); // Still fetch latest data just in case
|
||||
|
||||
if (options.action === 'bind_phone') {
|
||||
this.checkBindPhone();
|
||||
}
|
||||
this.ensureNickname()
|
||||
},
|
||||
onShow() {
|
||||
const app = getApp();
|
||||
if (app.globalData.profileAction === 'bind_phone') {
|
||||
app.globalData.profileAction = null; // Clear it
|
||||
this.checkBindPhone();
|
||||
}
|
||||
this.ensureNickname()
|
||||
this.fetchUser() // Always refresh user data on show to update stats
|
||||
},
|
||||
checkBindPhone() {
|
||||
// Only prompt if phone is truly missing
|
||||
const app = getApp()
|
||||
const hasPhone = this.data.user.phone || (app.globalData.userInfo && app.globalData.userInfo.phone);
|
||||
|
||||
if (!hasPhone) {
|
||||
this.setData({ isFormOpen: true });
|
||||
wx.showToast({
|
||||
title: '请先激活手机号',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
},
|
||||
initFormData(user) {
|
||||
this.setData({
|
||||
formData: {
|
||||
name: user.name,
|
||||
phone: user.phone,
|
||||
age: user.age,
|
||||
company_name: user.company_name,
|
||||
position: user.position,
|
||||
wechat_nickname: user.wechat_nickname,
|
||||
avatar: user.avatar
|
||||
}
|
||||
})
|
||||
},
|
||||
toggleForm() {
|
||||
this.setData({
|
||||
isFormOpen: !this.data.isFormOpen
|
||||
});
|
||||
},
|
||||
fetchUser() {
|
||||
const { request } = require('../../utils/request')
|
||||
request({ url: '/user/' })
|
||||
.then((data) => {
|
||||
this.setData({
|
||||
user: data,
|
||||
formData: {
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
age: data.age,
|
||||
company_name: data.company_name,
|
||||
position: data.position,
|
||||
wechat_nickname: data.wechat_nickname,
|
||||
avatar: data.avatar
|
||||
}
|
||||
})
|
||||
this.ensureNickname()
|
||||
})
|
||||
.catch(() => {
|
||||
this.setData({
|
||||
user: {
|
||||
id: '8839201',
|
||||
name: '学员用户',
|
||||
avatar:
|
||||
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200&auto=format&fit=crop&q=60',
|
||||
stats: {
|
||||
learning: 12,
|
||||
coupons: 3,
|
||||
hours: 28
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
handleInput(e) {
|
||||
const field = e.currentTarget.dataset.field;
|
||||
const value = e.detail.value;
|
||||
this.setData({
|
||||
[`formData.${field}`]: value
|
||||
});
|
||||
},
|
||||
onChooseAvatar(e) {
|
||||
const { avatarUrl } = e.detail
|
||||
const app = getApp()
|
||||
wx.showLoading({ title: '上传中...' })
|
||||
const header = {}
|
||||
if (app.globalData.token) {
|
||||
header['Authorization'] = `Bearer ${app.globalData.token}`
|
||||
}
|
||||
wx.uploadFile({
|
||||
url: `${app.globalData.baseUrl}/file/`,
|
||||
filePath: avatarUrl,
|
||||
name: 'file',
|
||||
header: header,
|
||||
success: (res) => {
|
||||
wx.hideLoading()
|
||||
if (res.statusCode === 200 || res.statusCode === 201) {
|
||||
const data = JSON.parse(res.data)
|
||||
// Handle wrapped response from FitJSONRenderer
|
||||
const path = data.data && data.data.path ? data.data.path : data.path;
|
||||
if (path) {
|
||||
this.setData({
|
||||
'formData.avatar': path,
|
||||
'user.avatar': path
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '上传失败:无路径', icon: 'none' })
|
||||
console.error('Upload response missing path:', data)
|
||||
}
|
||||
} else {
|
||||
wx.showToast({ title: '上传失败', icon: 'none' })
|
||||
console.error('Upload failed:', res)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.hideLoading()
|
||||
console.error(err)
|
||||
wx.showToast({ title: '上传出错', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
onNicknameChange(e) {
|
||||
this.setData({
|
||||
'formData.wechat_nickname': e.detail.value
|
||||
})
|
||||
},
|
||||
ensureNickname() {
|
||||
const has = this.data.formData && this.data.formData.wechat_nickname
|
||||
if (has) return
|
||||
// Only auto-fetch if we are in devtools (mock mode).
|
||||
// Real getUserProfile requires a tap event, so we cannot auto-call it.
|
||||
if (this.data.isDevtools) {
|
||||
this.fetchNickname()
|
||||
}
|
||||
},
|
||||
fetchNickname() {
|
||||
if (this.data.isDevtools) {
|
||||
const nick = '微信用户'
|
||||
const app = getApp()
|
||||
app.globalData.userInfo = {
|
||||
...app.globalData.userInfo,
|
||||
wechat_nickname: nick,
|
||||
name: app.globalData.userInfo && app.globalData.userInfo.name ? app.globalData.userInfo.name : nick
|
||||
}
|
||||
this.setData({
|
||||
'formData.wechat_nickname': nick,
|
||||
'formData.name': this.data.formData.name || nick,
|
||||
'user.name': this.data.user.name || nick
|
||||
})
|
||||
return
|
||||
}
|
||||
if (wx.getUserProfile) {
|
||||
wx.getUserProfile({ desc: '完善资料' }).then(res => {
|
||||
const info = res.userInfo || {}
|
||||
const nick = info.nickName || '微信用户'
|
||||
const avatar = info.avatarUrl || this.data.formData.avatar
|
||||
const app = getApp()
|
||||
app.globalData.userInfo = {
|
||||
...app.globalData.userInfo,
|
||||
wechat_nickname: nick,
|
||||
name: app.globalData.userInfo && app.globalData.userInfo.name ? app.globalData.userInfo.name : nick,
|
||||
avatar: avatar
|
||||
}
|
||||
this.setData({
|
||||
'formData.wechat_nickname': nick,
|
||||
'formData.name': this.data.formData.name || nick,
|
||||
'formData.avatar': avatar,
|
||||
'user.name': this.data.user.name || nick,
|
||||
'user.avatar': avatar
|
||||
})
|
||||
}).catch(err => {
|
||||
console.error('getUserProfile error:', err)
|
||||
})
|
||||
}
|
||||
},
|
||||
getPhoneNumber(e) {
|
||||
const detail = e && e.detail ? e.detail : {}
|
||||
if (detail.code) {
|
||||
const { request } = require('../../utils/request')
|
||||
wx.showLoading({ title: '获取中...' })
|
||||
request({
|
||||
url: '/user/phone/',
|
||||
method: 'POST',
|
||||
data: { code: detail.code }
|
||||
}).then(res => {
|
||||
wx.hideLoading()
|
||||
const app = getApp()
|
||||
app.globalData.userInfo = {
|
||||
...app.globalData.userInfo,
|
||||
phone: res.phone
|
||||
}
|
||||
this.setData({
|
||||
'user.phone': res.phone,
|
||||
'formData.phone': res.phone
|
||||
})
|
||||
wx.showToast({ title: '获取成功', icon: 'success' })
|
||||
}).catch(err => {
|
||||
wx.hideLoading()
|
||||
console.error('user/phone request error:', err)
|
||||
wx.showToast({ title: '获取失败,请检查网络', icon: 'none' })
|
||||
})
|
||||
} else {
|
||||
// DevTools 兜底:工具环境无法下发 code,直接模拟请求后端获取默认手机号
|
||||
if (this.data.isDevtools) {
|
||||
const { request } = require('../../utils/request')
|
||||
wx.showLoading({ title: '模拟获取中...' })
|
||||
request({
|
||||
url: '/user/phone/',
|
||||
method: 'POST',
|
||||
data: { code: 'mock_devtools' }
|
||||
}).then(res => {
|
||||
wx.hideLoading()
|
||||
const app = getApp()
|
||||
app.globalData.userInfo = {
|
||||
...app.globalData.userInfo,
|
||||
phone: res.phone
|
||||
}
|
||||
this.setData({
|
||||
'user.phone': res.phone,
|
||||
'formData.phone': res.phone
|
||||
})
|
||||
wx.showToast({ title: '工具模拟成功', icon: 'success' })
|
||||
}).catch(err => {
|
||||
wx.hideLoading()
|
||||
console.error('devtools mock phone error:', err)
|
||||
wx.showToast({ title: '工具模拟失败', icon: 'none' })
|
||||
})
|
||||
return
|
||||
}
|
||||
const msg = detail.errMsg || '未获取到授权码'
|
||||
console.error('getPhoneNumber error:', detail)
|
||||
wx.showToast({ title: msg.includes('deny') ? '用户拒绝授权' : msg, icon: 'none' })
|
||||
}
|
||||
},
|
||||
simulateGetPhone() {
|
||||
if (!this.data.isDevtools) return
|
||||
const { request } = require('../../utils/request')
|
||||
wx.showLoading({ title: '模拟获取中...' })
|
||||
request({
|
||||
url: '/user/phone/',
|
||||
method: 'POST',
|
||||
data: { code: 'mock_devtools' }
|
||||
}).then(res => {
|
||||
wx.hideLoading()
|
||||
const app = getApp()
|
||||
app.globalData.userInfo = {
|
||||
...app.globalData.userInfo,
|
||||
phone: res.phone
|
||||
}
|
||||
this.setData({
|
||||
'user.phone': res.phone,
|
||||
'formData.phone': res.phone
|
||||
})
|
||||
wx.showToast({ title: '工具模拟成功', icon: 'success' })
|
||||
}).catch(err => {
|
||||
wx.hideLoading()
|
||||
console.error('simulateGetPhone error:', err)
|
||||
wx.showToast({ title: '工具模拟失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
handleSubmit() {
|
||||
this.submitData();
|
||||
},
|
||||
submitData() {
|
||||
const { request } = require('../../utils/request')
|
||||
console.log(this.data.formData);
|
||||
|
||||
request({ url: '/user/', method: 'POST', data: this.data.formData })
|
||||
.then((data) => {
|
||||
console.log(data)
|
||||
// Update user data to reflect changes immediately
|
||||
const app = getApp()
|
||||
app.globalData.userInfo = {
|
||||
...app.globalData.userInfo,
|
||||
...data
|
||||
}
|
||||
this.setData({
|
||||
user: {
|
||||
...this.data.user,
|
||||
...data,
|
||||
stats: this.data.user.stats // preserve stats
|
||||
},
|
||||
isFormOpen: false
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
})
|
||||
},
|
||||
navigateToCoupons() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/coupon/coupon'
|
||||
})
|
||||
}
|
||||
})
|
||||
5
wechat-mini-program/pages/profile/profile.json
Normal file
5
wechat-mini-program/pages/profile/profile.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "个人中心",
|
||||
"navigationBarBackgroundColor": "#ff9900",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
107
wechat-mini-program/pages/profile/profile.wxml
Normal file
107
wechat-mini-program/pages/profile/profile.wxml
Normal file
@@ -0,0 +1,107 @@
|
||||
<view class="container">
|
||||
<view class="profile-header">
|
||||
<view class="user-info">
|
||||
<button class="avatar-btn" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar" style="padding:0;border:none;background:transparent;">
|
||||
<image class="avatar" src="{{user.avatar}}" mode="aspectFill"></image>
|
||||
</button>
|
||||
<view class="user-details">
|
||||
<view class="username">{{user.name}}</view>
|
||||
<!-- <view class="userid">ID: {{user.id}}</view> -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stat-item">
|
||||
<view class="stat-val">{{user.stats.learning}}</view>
|
||||
<view class="stat-label">在学课程</view>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-val">{{user.stats.coupons}}</view>
|
||||
<view class="stat-label">可用优惠券</view>
|
||||
</view>
|
||||
<!-- <view class="stat-item">
|
||||
<view class="stat-val">{{user.stats.hours}}h</view>
|
||||
<view class="stat-label">学习时长</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Menu List -->
|
||||
<view class="menu-list">
|
||||
<view class="menu-item" bindtap="navigateToCoupons">
|
||||
<view class="menu-left">
|
||||
<image class="menu-icon" src="/assets/icons/coupon-icon.png" mode="aspectFit"></image>
|
||||
<text class="menu-text">我的优惠券</text>
|
||||
</view>
|
||||
<view class="menu-right">
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-section">
|
||||
<view class="form-title-bar" bindtap="toggleForm">
|
||||
<view class="title-left">
|
||||
<image class="menu-icon" src="/assets/icons/profile-icon.png" mode="aspectFit"></image>
|
||||
<text class="form-title">绑定个人信息</text>
|
||||
</view>
|
||||
<text class="toggle-icon">{{isFormOpen ? '▲' : '▼'}}</text>
|
||||
</view>
|
||||
|
||||
<view class="form-content" wx:if="{{isFormOpen}}">
|
||||
<view class="form-group">
|
||||
<text class="label">头像</text>
|
||||
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
|
||||
<image class="avatar-upload" src="{{formData.avatar || user.avatar || '/assets/icons/default-avatar.png'}}" mode="aspectFill"></image>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="label">微信昵称</text>
|
||||
<view class="input-wrap">
|
||||
<input type="nickname" placeholder="请输入微信昵称" bind:change="onNicknameChange" bindinput="handleInput" data-field="wechat_nickname" value="{{formData.wechat_nickname}}"/>
|
||||
<button size="mini" type="default" bindtap="fetchNickname" wx:if="{{!formData.wechat_nickname}}" style="font-size:24rpx;margin-left:10rpx;padding:0 20rpx;line-height:60rpx;height:60rpx;">获取微信昵称</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="label">学员姓名</text>
|
||||
<view class="input-wrap">
|
||||
<input type="text" placeholder="请输入真实姓名" bindinput="handleInput" data-field="name" value="{{formData.name}}"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="label">联系电话</text>
|
||||
<view class="input-wrap" style="display:flex;align-items:center;">
|
||||
<input type="number" placeholder="请输入手机号码" disabled="true" bindinput="handleInput" data-field="phone" value="{{formData.phone}}" style="flex:1;"/>
|
||||
<button size="mini" type="primary" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber" style="font-size:24rpx;margin-left:10rpx;padding:0 20rpx;line-height:60rpx;height:60rpx;">获取号码</button>
|
||||
<button wx:if="{{isDevtools}}" size="mini" type="default" bindtap="simulateGetPhone" style="font-size:24rpx;margin-left:10rpx;padding:0 20rpx;line-height:60rpx;height:60rpx;">模拟获取号码</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="label">年龄</text>
|
||||
<view class="input-wrap">
|
||||
<input type="number" placeholder="0" bindinput="handleInput" data-field="age" value="{{formData.age}}"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="label">公司名称</text>
|
||||
<view class="input-wrap">
|
||||
<input type="text" placeholder="请输入公司名称" bindinput="handleInput" data-field="company_name" value="{{formData.company_name}}"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="label">职位</text>
|
||||
<view class="input-wrap">
|
||||
<input type="text" placeholder="请输入职位" bindinput="handleInput" data-field="position" value="{{formData.position}}"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="submit-btn" bindtap="handleSubmit">保存信息</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
206
wechat-mini-program/pages/profile/profile.wxss
Normal file
206
wechat-mini-program/pages/profile/profile.wxss
Normal file
@@ -0,0 +1,206 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
.profile-header {
|
||||
background-color: var(--surface-color);
|
||||
padding: 40rpx 40rpx 100rpx 40rpx;
|
||||
position: relative;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30rpx;
|
||||
justify-content: flex-start; /* Ensure items start from left */
|
||||
}
|
||||
.avatar-btn {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
margin: 0; /* Ensure no auto margin */
|
||||
width: 128rpx !important;
|
||||
height: 128rpx !important;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0; /* Prevent shrinking */
|
||||
}
|
||||
.avatar-btn::after {
|
||||
border: none;
|
||||
}
|
||||
.avatar {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid var(--surface-color);
|
||||
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.1);
|
||||
background-color: #eee;
|
||||
display: block;
|
||||
}
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start; /* Ensure text aligns left */
|
||||
flex: 1; /* Take remaining space */
|
||||
text-align: left; /* Force text align left */
|
||||
}
|
||||
.username {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.userid {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.stats-card {
|
||||
position: absolute;
|
||||
bottom: -40rpx;
|
||||
left: 40rpx;
|
||||
right: 40rpx;
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 10rpx 30rpx rgba(0,0,0,0.05);
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-val {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Menu List */
|
||||
.menu-list {
|
||||
margin: 0 30rpx 30rpx 30rpx;
|
||||
background-color: var(--surface-color);
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
.menu-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 40rpx;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.menu-item:active {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
.menu-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.menu-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
.menu-text {
|
||||
font-size: 30rpx;
|
||||
color: var(--text-main);
|
||||
font-weight: 500;
|
||||
}
|
||||
.menu-arrow {
|
||||
color: var(--text-light);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 40rpx;
|
||||
background-color: var(--surface-color);
|
||||
margin: 0 30rpx 40rpx 30rpx;
|
||||
border-radius: 30rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
.form-title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.toggle-icon {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-light);
|
||||
padding: 10rpx;
|
||||
}
|
||||
.form-content {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.blue-line {
|
||||
width: 8rpx;
|
||||
height: 32rpx;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
.form-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
.label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 10rpx;
|
||||
display: block;
|
||||
}
|
||||
.input-wrap {
|
||||
background-color: var(--background-color);
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 2rpx solid var(--border-color);
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
}
|
||||
.half {
|
||||
flex: 1;
|
||||
}
|
||||
.submit-btn {
|
||||
background: linear-gradient(to right, var(--primary-gradient-start), var(--primary-gradient-end));
|
||||
color: var(--text-white);
|
||||
border-radius: 20rpx;
|
||||
margin-top: 40rpx;
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
/* Avatar Upload */
|
||||
.avatar-wrapper {
|
||||
padding: 0;
|
||||
width: 120rpx !important;
|
||||
height: 120rpx !important;
|
||||
border-radius: 20rpx;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.avatar-wrapper::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.avatar-upload {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #eee;
|
||||
}
|
||||
Reference in New Issue
Block a user