This commit is contained in:
2026-08-26 15:10:25 +08:00
parent 06debfd1fc
commit fc9613197b
202 changed files with 28919 additions and 5551 deletions
@@ -0,0 +1,634 @@
<template>
<div>
<van-action-sheet
v-model="visible"
class="activity-schedule-calendar"
:style="{ height: '78%' }"
:close-on-click-overlay="true"
title="活动日历"
closeable>
<div class="activity-schedule-calendar__body">
<div class="activity-schedule-calendar__month-bar">
<button type="button" class="activity-schedule-calendar__month-btn" @click="changeMonth(-1)">
<van-icon name="arrow-left"></van-icon>
</button>
<div class="activity-schedule-calendar__month-title">{{ currentYear }}{{ currentMonth }}</div>
<button type="button" class="activity-schedule-calendar__month-btn" @click="changeMonth(1)">
<van-icon name="arrow"></van-icon>
</button>
</div>
<div class="activity-schedule-calendar__weekdays">
<span v-for="item in weekdays" :key="item">{{ item }}</span>
</div>
<div class="activity-schedule-calendar__days">
<button
v-for="day in calendarDays"
:key="day.key"
type="button"
class="activity-schedule-calendar__day"
:class="{
'is-other-month': !day.currentMonth,
'is-selected': day.key === selectedDate,
'is-today': day.isToday
}"
@click="selectDay(day)">
<span>
<span class="activity-schedule-calendar__day-number">{{ day.day }}</span>
<small v-if="day.isToday" class="activity-schedule-calendar__today-label"></small>
</span>
<i v-if="day.status" class="activity-schedule-calendar__day-status" :class="'is-' + day.status"></i>
</button>
</div>
<div class="activity-schedule-calendar__legend">
<span><i class="is-solid"></i>已签到</span>
<span><i class="is-outline"></i>可签到</span>
<span><i class="is-muted"></i>未开始</span>
</div>
<div v-if="showMap" :id="mapContainerId" class="activity-schedule-calendar__map"></div>
<section class="activity-schedule-calendar__schedule-card">
<div class="activity-schedule-calendar__schedule-title">
{{ selectedDate ? selectedDate + ' 的活动时段' : '活动时段' }}
</div>
<div v-if="selectedTimes.length" class="activity-schedule-calendar__schedule-list">
<div v-for="item in selectedTimes" :key="item.id" class="activity-schedule-calendar__schedule-item">
<div class="activity-schedule-calendar__schedule-main">
<van-icon :name="item.isAttend === true ? 'checked' : 'clock-o'"></van-icon>
<div class="activity-schedule-calendar__schedule-copy">
<div class="activity-schedule-calendar__schedule-time">
{{ formatTime(item.courseStartTime) }} - {{ formatTime(item.courseEndTime) }}
</div>
<div v-if="row && row.courseLocation" class="activity-schedule-calendar__schedule-location">
<van-icon name="location-o"></van-icon>
<span>{{ row.courseLocation }}</span>
</div>
</div>
</div>
<span v-if="item.isAttend === true" class="activity-schedule-calendar__status is-success">已签到</span>
<button
v-else-if="canSignItem(item)"
type="button"
class="activity-schedule-calendar__sign-btn"
@click.stop="onSign(item)">签到</button>
<span v-else class="activity-schedule-calendar__status" :class="{
'is-ended': getScheduleStatus(item) === '已结束',
'is-progressing': getScheduleStatus(item) === '进行中'
}">
{{ getScheduleStatus(item) }}
</span>
</div>
</div>
<div v-else class="activity-schedule-calendar__empty">
<img src="/assets/mobile/img/activity/schedule-empty.png?v=20260821-transparent" alt="">
<div class="activity-schedule-calendar__empty-title">当天暂无签到时段</div>
<div class="activity-schedule-calendar__empty-desc">当前日期没有需要签到的活动安排</div>
</div>
</section>
</div>
</van-action-sheet>
<van-popup
v-model="signVisible"
round
closeable
:safe-area-inset-bottom="true"
:close-on-click-overlay="false"
:style="{ width: '80%', height: '66%' }"
get-container="#app"
@close="onSignClose">
<scan-code ref="scanCodeRef"></scan-code>
</van-popup>
</div>
</template>
<script>
module.exports = {
name: "ActivityScheduleCalendar",
props: {
moduleType: {
type: String,
default: "brand"
}
},
components: {
"scan-code": httpVueLoader("/components/plugins/sysScanCode/index.vue?v=" + new Date().getTime())
},
data() {
return {
visible: false,
row: null,
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
selectedDate: "",
weekdays: ["日", "一", "二", "三", "四", "五", "六"],
selectCourseTime: null,
signVisible: false,
markerLayer: null,
map: null,
circle: null,
mapContainerId: "activityScheduleMap_" + Math.random().toString(36).slice(2)
}
},
computed: {
courseTimes() {
return this.row && Array.isArray(this.row.courseTimes) ? this.row.courseTimes : []
},
scheduleDateMap() {
const map = {}
this.courseTimes.forEach(item => {
const key = this.dateKey(item)
if (!key) return
if (!map[key]) map[key] = []
map[key].push(item)
})
return map
},
selectedTimes() {
return this.scheduleDateMap[this.selectedDate] || []
},
canSign() {
return !!(this.row && this.row.isSign === true && this.row.isMobileSign === true)
},
showMap() {
return !!(this.moduleType === "brand" && this.row && this.row.isMobileSign === true && this.row.signType === 3)
},
calendarDays() {
const firstDay = new Date(this.currentYear, this.currentMonth - 1, 1)
const startDate = new Date(this.currentYear, this.currentMonth - 1, 1 - firstDay.getDay())
const todayKey = this.formatDateKey(new Date())
return Array.from({ length: 42 }, (_, index) => {
const date = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + index)
const key = this.formatDateKey(date)
return {
key,
day: date.getDate(),
currentMonth: date.getFullYear() === this.currentYear && date.getMonth() + 1 === this.currentMonth,
isToday: key === todayKey,
status: this.getDayStatus(this.scheduleDateMap[key] || [])
}
})
}
},
methods: {
onOpen(row) {
this.row = row
const initialDate = this.formatDateKey(new Date())
const date = this.$moment(initialDate)
this.currentYear = date.year()
this.currentMonth = date.month() + 1
this.selectedDate = initialDate
this.visible = true
this.$nextTick(() => {
if (this.showMap) this.initMap()
})
},
dateKey(item) {
const value = item && (item.courseDate || item.courseStartTime)
return value ? this.$moment(value).format("YYYY-MM-DD") : ""
},
formatDateKey(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, "0")
const day = String(date.getDate()).padStart(2, "0")
return `${year}-${month}-${day}`
},
formatTime(value) {
return value ? this.$moment(value).format("HH:mm") : "--"
},
canSignItem(item) {
if (!this.canSign || !item || !item.courseStartTime || !item.courseEndTime) return false
const now = this.$moment()
return !now.isBefore(this.$moment(item.courseStartTime)) && !now.isAfter(this.$moment(item.courseEndTime))
},
getScheduleStatus(item) {
if (!item || !item.courseStartTime || !item.courseEndTime) return "待开始"
const now = this.$moment()
if (now.isBefore(this.$moment(item.courseStartTime))) return "待开始"
if (now.isAfter(this.$moment(item.courseEndTime))) return "已结束"
return "进行中"
},
getDayStatus(items) {
if (items.some(item => item.isAttend === true)) return "signed"
if (items.some(item => this.canSignItem(item))) return "available"
if (items.some(item => item.courseStartTime && this.$moment().isBefore(this.$moment(item.courseStartTime)))) return "pending"
return ""
},
changeMonth(offset) {
const date = new Date(this.currentYear, this.currentMonth - 1 + offset, 1)
this.currentYear = date.getFullYear()
this.currentMonth = date.getMonth() + 1
},
selectDay(day) {
this.selectedDate = day.key
if (!day.currentMonth) {
const date = this.$moment(day.key)
this.currentYear = date.year()
this.currentMonth = date.month() + 1
}
},
onSign(courseTime) {
this.selectCourseTime = courseTime
if (this.row.signType === 1) {
this.signVisible = true
this.$nextTick(() => this.$refs.scanCodeRef.init())
} else if (this.row.signType === 2) {
this.makeCode()
} else if (this.row.signType === 3 && this.moduleType === "brand") {
this.positionSign()
} else {
this.$toast("此签到模式正在升级中")
}
},
onSignClose() {
if (this.$refs.scanCodeRef) this.$refs.scanCodeRef.closeScan()
},
makeCode() {
const baseUrl = this.moduleType === "family"
? "/platform/family/mine/passiveScan"
: "/platform/trainSignUp/mine/passiveScan"
const content = jrQrcode.getQrBase64(baseUrl + "?id=" + this.selectCourseTime.id)
vant.ImagePreview([content])
},
positionSign() {
this.getLocation(coords => {
if (!coords) return
this.$axios.post("/platform/trainSignUp/mine/positionSign", {
timeId: this.selectCourseTime.id,
courseId: this.row.id,
lat: coords.lat,
lng: coords.lng
}).then(res => {
if (res.code === 0) {
this.selectCourseTime.isAttend = true
this.$toast("签到成功")
} else {
this.$toast(res.msg)
}
})
})
},
getLocation(callback) {
if (!navigator.geolocation) {
this.$toast("当前浏览器不支持定位功能")
callback(null)
return
}
const loading = vant.Toast.loading({ message: "获取定位中...", forbidClick: false, duration: 0 })
navigator.geolocation.getCurrentPosition(position => {
loading.close()
callback({ lat: position.coords.latitude, lng: position.coords.longitude })
}, error => {
loading.close()
const messageMap = {
1: "请允许浏览器获取位置信息",
2: "无法获取当前位置",
3: "定位超时,请重试"
}
this.$toast(messageMap[error.code] || "定位失败,请稍后重试")
callback(null)
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 })
},
initMap() {
if (!this.row.courseLocationCoordinates || typeof TMap === "undefined") return
const position = JSON.parse(this.row.courseLocationCoordinates)
const center = new TMap.LatLng(position[0], position[1])
if (this.map) this.map.destroy()
this.map = new TMap.Map(this.mapContainerId, { zoom: 16, center })
this.markerLayer = new TMap.MultiMarker({
map: this.map,
geometries: [{ id: "activity", position: center }]
})
this.circle = new TMap.MultiCircle({
map: this.map,
geometries: [{ center, radius: this.row.radius || 100 }]
})
}
}
}
</script>
<style scoped>
.activity-schedule-calendar {
overflow: hidden;
border-radius: 18px 18px 0 0;
background: #f4f7fc;
}
.activity-schedule-calendar /deep/ .van-action-sheet__header {
height: 50px;
color: #1f2f43;
background: #fff;
font-size: 16px;
line-height: 50px;
}
.activity-schedule-calendar /deep/ .van-action-sheet__close {
top: 14px;
color: #7e899a;
font-size: 21px;
}
.activity-schedule-calendar__body {
height: 100%;
padding: 14px 14px 24px;
overflow-y: auto;
overflow-anchor: none;
box-sizing: border-box;
}
.activity-schedule-calendar__month-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.activity-schedule-calendar__month-btn {
display: flex;
width: 36px;
height: 36px;
padding: 0;
align-items: center;
justify-content: center;
border: 1px solid #edf1f6;
border-radius: 50%;
color: #263548;
background: #fff;
box-shadow: 0 5px 14px rgba(55, 85, 126, .08);
}
.activity-schedule-calendar__month-title {
color: #1f2f43;
font-size: 18px;
font-weight: 600;
}
.activity-schedule-calendar__weekdays,
.activity-schedule-calendar__days {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
text-align: center;
}
.activity-schedule-calendar__weekdays {
margin-bottom: 5px;
color: #58677b;
font-size: 12px;
line-height: 28px;
}
.activity-schedule-calendar__day {
position: relative;
display: flex;
width: 38px;
height: 48px;
margin: 1px auto;
padding: 0;
align-items: center;
flex-direction: column;
justify-content: center;
border: 0;
color: #263548;
background: transparent;
font-size: 14px;
line-height: 1;
box-sizing: border-box;
}
.activity-schedule-calendar__day > span {
position: absolute;
top: 4px;
left: 50%;
transform: translateX(-50%);
z-index: 1;
display: flex;
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
border-radius: 50%;
box-sizing: border-box;
}
.activity-schedule-calendar__day-number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
line-height: 14px;
}
.activity-schedule-calendar__today-label {
position: absolute;
top: 2px;
right: 0;
left: 0;
color: #1989fa;
font-size: 10px;
line-height: 10px;
text-align: center;
}
.activity-schedule-calendar__day.is-other-month > span {
color: #bdc5d0;
}
.activity-schedule-calendar__day.is-selected > span {
flex: 0 0 40px;
width: 40px;
min-width: 40px;
height: 40px;
min-height: 40px;
color: #fff;
background: #1989fa;
box-shadow: 0 5px 12px rgba(25, 137, 250, .22);
}
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
top: 2px;
color: #fff;
}
.activity-schedule-calendar__day-status {
position: absolute;
bottom: -3px;
left: 50%;
width: 6px;
height: 6px;
transform: translateX(-50%);
border-radius: 50%;
box-sizing: border-box;
}
.activity-schedule-calendar__day-status.is-signed {
background: #1989fa;
}
.activity-schedule-calendar__day-status.is-available {
border: 1px solid #1989fa;
background: #fff;
}
.activity-schedule-calendar__day-status.is-pending {
background: #d8dde5;
}
.activity-schedule-calendar__legend {
display: flex;
margin: 12px 4px 16px;
align-items: center;
justify-content: space-around;
color: #78869a;
font-size: 11px;
}
.activity-schedule-calendar__legend span {
display: inline-flex;
align-items: center;
gap: 5px;
}
.activity-schedule-calendar__legend i {
width: 9px;
height: 9px;
border-radius: 50%;
box-sizing: border-box;
}
.activity-schedule-calendar__legend .is-solid { background: #1989fa; }
.activity-schedule-calendar__legend .is-outline { border: 1px solid #1989fa; }
.activity-schedule-calendar__legend .is-muted { background: #d8dde5; }
.activity-schedule-calendar__map {
width: 100%;
height: 150px;
margin-bottom: 12px;
overflow: hidden;
border-radius: 12px;
}
.activity-schedule-calendar__schedule-card {
overflow: hidden;
border-radius: 12px;
background: #fff;
box-shadow: 0 7px 20px rgba(55, 85, 126, .07);
}
.activity-schedule-calendar__schedule-title {
padding: 12px 14px;
border-bottom: 1px solid #edf1f6;
color: #263548;
font-size: 14px;
font-weight: 600;
}
.activity-schedule-calendar__schedule-item {
display: flex;
min-height: 70px;
padding: 12px 14px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #edf1f6;
box-sizing: border-box;
}
.activity-schedule-calendar__schedule-item:last-child { border-bottom: 0; }
.activity-schedule-calendar__schedule-main {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 9px;
}
.activity-schedule-calendar__schedule-main > .van-icon {
margin-top: 2px;
color: #1989fa;
font-size: 18px;
}
.activity-schedule-calendar__schedule-time {
color: #263548;
font-size: 15px;
line-height: 22px;
}
.activity-schedule-calendar__schedule-location {
display: flex;
margin-top: 5px;
align-items: center;
gap: 4px;
color: #7d899a;
font-size: 12px;
line-height: 18px;
}
.activity-schedule-calendar__status {
flex: 0 0 auto;
padding: 4px 8px;
border-radius: 5px;
color: #8b96a7;
background: #f1f4f8;
font-size: 11px;
}
.activity-schedule-calendar__status.is-success {
color: #19a965;
background: #eaf8f1;
}
.activity-schedule-calendar__status.is-progressing {
color: #ff8a00;
background: #fff4e6;
}
.activity-schedule-calendar__status.is-ended {
color: #98a2b2;
background: #eef1f5;
}
.activity-schedule-calendar__sign-btn {
display: inline-flex;
min-width: 42px !important;
height: 24px !important;
padding: 0 8px !important;
align-items: center;
justify-content: center;
border: 1px solid #1989fa !important;
border-radius: 5px !important;
color: #fff !important;
background-color: #1989fa !important;
box-shadow: none !important;
font-size: 11px !important;
}
.activity-schedule-calendar__empty {
padding: 18px 14px 22px;
color: #9aa5b5;
text-align: center;
}
.activity-schedule-calendar__empty img {
display: block;
width: 142px;
height: 94px;
margin: 0 auto 4px;
object-fit: contain;
}
.activity-schedule-calendar__empty-title {
color: #58677b;
font-size: 13px;
line-height: 20px;
}
.activity-schedule-calendar__empty-desc {
margin-top: 2px;
color: #a2adbc;
font-size: 11px;
line-height: 18px;
}
</style>