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