Initial commit
This commit is contained in:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user