|
|
@@ -38,6 +38,10 @@
|
|
|
<text class="meta-icon">🕐</text>
|
|
|
<text class="meta-text">{{ formatDate(activity.startTime) }}{{ activity.endTime ? ' ~ ' + formatDate(activity.endTime) : '' }}</text>
|
|
|
</view>
|
|
|
+ <view class="meta-item" v-if="activity.endTime && activity.requireRegistration === 1">
|
|
|
+ <text class="meta-icon">⏰</text>
|
|
|
+ <text class="meta-text">报名截止: {{ formatDate(activity.endTime) }}</text>
|
|
|
+ </view>
|
|
|
<view class="meta-item" v-if="activity.location">
|
|
|
<text class="meta-icon">📍</text>
|
|
|
<text class="meta-text">{{ activity.location }}</text>
|
|
|
@@ -48,7 +52,10 @@
|
|
|
</view>
|
|
|
<view class="meta-item">
|
|
|
<text class="meta-icon">💰</text>
|
|
|
- <text class="meta-text">{{ activity.priceLabel || (activity.price > 0 ? '¥' + activity.price : '免费') }}</text>
|
|
|
+ <view class="meta-text" v-if="activity.memberPrice && activity.memberPrice > 0 && activity.memberPrice < activity.price">
|
|
|
+ <text>会员价 ¥{{ activity.memberPrice }} 普通价 ¥{{ activity.price }}</text>
|
|
|
+ </view>
|
|
|
+ <text class="meta-text" v-else>{{ activity.priceLabel || (activity.price > 0 ? '¥' + activity.price : '免费') }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
@@ -113,9 +120,11 @@
|
|
|
@click="onCheckin"
|
|
|
>
|
|
|
<text v-if="checkinLoading">签到中...</text>
|
|
|
+ <text v-else-if="checkinDone && activity.requireCheckinReview === 1">已签到待审核</text>
|
|
|
<text v-else-if="checkinDone">已签到 ✓</text>
|
|
|
<text v-else>签到</text>
|
|
|
</button>
|
|
|
+ <text v-if="activity.requireCheckinReview === 1 && !checkinDone" class="checkin-review-hint">签到需要管理员审核</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
@@ -127,6 +136,55 @@
|
|
|
<button class="penalty-btn" @click="showPenaltyModal = false">我知道了</button>
|
|
|
</view>
|
|
|
</modal>
|
|
|
+
|
|
|
+ <!-- 活动评价 -->
|
|
|
+ <view class="feedback-section" v-if="activity.status === 'ended' && checkinDone">
|
|
|
+ <view class="feedback-header">
|
|
|
+ <text class="feedback-title">活动评价</text>
|
|
|
+ </view>
|
|
|
+ <!-- Already submitted feedback -->
|
|
|
+ <view v-if="submittedFeedback" class="feedback-submitted">
|
|
|
+ <view class="feedback-ratings-display">
|
|
|
+ <view class="fr-row" v-for="(val, key) in parsedSubmittedRatings" :key="key">
|
|
|
+ <text class="fr-label">{{ key }}</text>
|
|
|
+ <text class="fr-stars">{{ getStarDisplay(val) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <text class="feedback-comment-text" v-if="submittedFeedback.comment">{{ submittedFeedback.comment }}</text>
|
|
|
+ <text class="feedback-thanks">感谢您的评价!</text>
|
|
|
+ </view>
|
|
|
+ <!-- Submit feedback button -->
|
|
|
+ <button v-else class="feedback-btn" @click="openFeedbackModal">
|
|
|
+ <text>提交活动评价</text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 评价弹窗 -->
|
|
|
+ <modal v-if="showFeedbackModal" class="feedback-modal-overlay" :show="showFeedbackModal" @close="showFeedbackModal = false">
|
|
|
+ <view class="feedback-modal">
|
|
|
+ <text class="feedback-modal-title">活动评价</text>
|
|
|
+
|
|
|
+ <view class="rating-row" v-for="(dim, dimIdx) in feedbackDimensions" :key="dimIdx">
|
|
|
+ <text class="rating-label">{{ dim }}</text>
|
|
|
+ <view class="stars">
|
|
|
+ <text v-for="star in 5" :key="star"
|
|
|
+ class="star-icon"
|
|
|
+ :class="{'star-active': star <= (feedbackRatings[dim] || 0)}"
|
|
|
+ @click="feedbackRatings[dim] = star">★</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <textarea class="feedback-textarea" v-model="feedbackComment" placeholder="说说您的体验..." maxlength="500" />
|
|
|
+
|
|
|
+ <view class="feedback-modal-actions">
|
|
|
+ <button class="feedback-cancel-btn" @click="showFeedbackModal = false">取消</button>
|
|
|
+ <button class="feedback-submit-btn" :disabled="feedbackSubmitting" @click="submitFeedback">
|
|
|
+ <text v-if="feedbackSubmitting">提交中...</text>
|
|
|
+ <text v-else>提交评价</text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </modal>
|
|
|
</scroll-view>
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
@@ -137,7 +195,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations } from '@/utils/api.js'
|
|
|
+import { getActivityDetail, activityCheckin, registerActivity, cancelActivityRegistration, getMyActivityRegistrations, submitActivityFeedback, getActivityFeedbackList, getMyActivityFeedback } from '@/utils/api.js'
|
|
|
|
|
|
export default {
|
|
|
data() {
|
|
|
@@ -154,7 +212,14 @@ export default {
|
|
|
registrationLoading: false,
|
|
|
paying: false,
|
|
|
showPenaltyModal: false,
|
|
|
- penaltyAmount: 0
|
|
|
+ penaltyAmount: 0,
|
|
|
+ showFeedbackModal: false,
|
|
|
+ feedbackDimensions: [],
|
|
|
+ feedbackRatings: {},
|
|
|
+ feedbackComment: '',
|
|
|
+ submittedFeedback: null,
|
|
|
+ feedbackSubmitting: false,
|
|
|
+ feedbackList: []
|
|
|
}
|
|
|
},
|
|
|
onLoad: function(options) {
|
|
|
@@ -196,6 +261,8 @@ export default {
|
|
|
self.loading = false
|
|
|
if (res && res.code === 200 && res.data) {
|
|
|
self.activity = res.data
|
|
|
+ self.parseFeedbackDimensions()
|
|
|
+ self.loadFeedbackStatus()
|
|
|
// Load registration status if logged in and activity requires registration
|
|
|
if (self.activity.requireRegistration === 1) {
|
|
|
var childId = uni.getStorageSync('currentChildId')
|
|
|
@@ -241,6 +308,7 @@ export default {
|
|
|
activityCheckin({ id: self.activity.id, childId: childId }).then(function(res) {
|
|
|
self.checkinLoading = false
|
|
|
if (res && res.code === 200 && res.data) {
|
|
|
+ self.loadFeedbackStatus()
|
|
|
// 检查是否有能量扣减
|
|
|
if (res.data.energyPenalty) {
|
|
|
self.penaltyAmount = res.data.energyPenalty
|
|
|
@@ -343,6 +411,89 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
+ },
|
|
|
+ // ===== 活动评价 =====
|
|
|
+ parseFeedbackDimensions: function() {
|
|
|
+ var dims = ['组织形式', '会场气氛', '主持人', '专家']
|
|
|
+ if (this.activity.feedbackDimensions) {
|
|
|
+ try {
|
|
|
+ var parsed = JSON.parse(this.activity.feedbackDimensions)
|
|
|
+ if (parsed && parsed.length > 0) {
|
|
|
+ dims = parsed
|
|
|
+ }
|
|
|
+ } catch(e) {}
|
|
|
+ }
|
|
|
+ this.feedbackDimensions = dims
|
|
|
+ this.initRatings()
|
|
|
+ },
|
|
|
+ initRatings: function() {
|
|
|
+ var r = {}
|
|
|
+ for (var i = 0; i < this.feedbackDimensions.length; i++) {
|
|
|
+ r[this.feedbackDimensions[i]] = 0
|
|
|
+ }
|
|
|
+ this.feedbackRatings = r
|
|
|
+ },
|
|
|
+ loadFeedbackStatus: function() {
|
|
|
+ var self = this
|
|
|
+ var childId = uni.getStorageSync('currentChildId')
|
|
|
+ if (!childId || !self.activity || !self.activity.id) return
|
|
|
+
|
|
|
+ getMyActivityFeedback({ activityId: self.activity.id, childId: childId }).then(function(res) {
|
|
|
+ if (res && res.code === 200 && res.data) {
|
|
|
+ self.submittedFeedback = res.data
|
|
|
+ }
|
|
|
+ }).catch(function() {})
|
|
|
+ },
|
|
|
+ openFeedbackModal: function() {
|
|
|
+ this.showFeedbackModal = true
|
|
|
+ },
|
|
|
+ submitFeedback: function() {
|
|
|
+ var self = this
|
|
|
+ // Validate at least one rating
|
|
|
+ var hasRating = false
|
|
|
+ for (var key in self.feedbackRatings) {
|
|
|
+ if (self.feedbackRatings[key] > 0) { hasRating = true; break }
|
|
|
+ }
|
|
|
+ if (!hasRating) {
|
|
|
+ uni.showToast({ title: '请至少评价一项', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ self.feedbackSubmitting = true
|
|
|
+ var childId = uni.getStorageSync('currentChildId')
|
|
|
+ submitActivityFeedback({
|
|
|
+ activityId: self.activity.id,
|
|
|
+ childId: childId,
|
|
|
+ ratings: JSON.stringify(self.feedbackRatings),
|
|
|
+ comment: self.feedbackComment
|
|
|
+ }).then(function(res) {
|
|
|
+ self.feedbackSubmitting = false
|
|
|
+ if (res && res.code === 200) {
|
|
|
+ self.showFeedbackModal = false
|
|
|
+ self.loadFeedbackStatus()
|
|
|
+ uni.showToast({ title: '评价成功', icon: 'success' })
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.feedbackSubmitting = false
|
|
|
+ uni.showToast({ title: '网络异常', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ getStarDisplay: function(val) {
|
|
|
+ var full = parseInt(val) || 0
|
|
|
+ var empty = 5 - full
|
|
|
+ return '★'.repeat(full) + '☆'.repeat(empty)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ parsedSubmittedRatings: function() {
|
|
|
+ if (!this.submittedFeedback || !this.submittedFeedback.ratings) return {}
|
|
|
+ try {
|
|
|
+ return JSON.parse(this.submittedFeedback.ratings)
|
|
|
+ } catch(e) {
|
|
|
+ return {}
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -645,4 +796,72 @@ export default {
|
|
|
padding: 16rpx 60rpx;
|
|
|
font-size: 28rpx;
|
|
|
}
|
|
|
+
|
|
|
+/* 活动评价 */
|
|
|
+.feedback-section {
|
|
|
+ padding: 30rpx;
|
|
|
+ margin: 20rpx;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+}
|
|
|
+.feedback-header { margin-bottom: 20rpx; }
|
|
|
+.feedback-title { font-size: 32rpx; font-weight: bold; color: #333; }
|
|
|
+.feedback-btn {
|
|
|
+ background: #F97316; color: #fff; border: none;
|
|
|
+ border-radius: 40rpx; padding: 20rpx 40rpx; font-size: 28rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.feedback-submitted { padding: 10rpx 0; }
|
|
|
+.feedback-ratings-display { margin-bottom: 16rpx; }
|
|
|
+.fr-row { display: flex; justify-content: space-between; padding: 8rpx 0; }
|
|
|
+.fr-label { font-size: 28rpx; color: #333; }
|
|
|
+.fr-stars { font-size: 28rpx; color: #F97316; }
|
|
|
+.feedback-comment-text { font-size: 26rpx; color: #666; margin: 10rpx 0; line-height: 1.5; }
|
|
|
+.feedback-thanks { font-size: 24rpx; color: #999; text-align: center; margin-top: 16rpx; }
|
|
|
+.feedback-modal-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0,0,0,0.5);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 999;
|
|
|
+}
|
|
|
+.feedback-modal {
|
|
|
+ background: #fff; border-radius: 20rpx; padding: 40rpx;
|
|
|
+ width: 80%; max-width: 600rpx;
|
|
|
+}
|
|
|
+.feedback-modal-title {
|
|
|
+ font-size: 34rpx; font-weight: bold; color: #333;
|
|
|
+ text-align: center; margin-bottom: 30rpx;
|
|
|
+}
|
|
|
+.rating-row {
|
|
|
+ display: flex; justify-content: space-between; align-items: center;
|
|
|
+ padding: 16rpx 0;
|
|
|
+}
|
|
|
+.rating-label { font-size: 28rpx; color: #333; min-width: 120rpx; }
|
|
|
+.stars { display: flex; }
|
|
|
+.star-icon { font-size: 40rpx; color: #ddd; padding: 0 6rpx; }
|
|
|
+.star-active { color: #F97316; }
|
|
|
+.feedback-textarea {
|
|
|
+ width: 100%; height: 160rpx; margin-top: 20rpx;
|
|
|
+ border: 1rpx solid #eee; border-radius: 12rpx;
|
|
|
+ padding: 16rpx; font-size: 26rpx; box-sizing: border-box;
|
|
|
+}
|
|
|
+.feedback-modal-actions {
|
|
|
+ display: flex; justify-content: space-between; margin-top: 30rpx;
|
|
|
+}
|
|
|
+.feedback-cancel-btn {
|
|
|
+ flex: 1; background: #f5f5f5; color: #666; border: none;
|
|
|
+ border-radius: 40rpx; padding: 20rpx; font-size: 28rpx;
|
|
|
+ text-align: center; margin-right: 20rpx;
|
|
|
+}
|
|
|
+.feedback-submit-btn {
|
|
|
+ flex: 1; background: #F97316; color: #fff; border: none;
|
|
|
+ border-radius: 40rpx; padding: 20rpx; font-size: 28rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
</style>
|