Files
zhgh_nau/src/main/resources/static/components/module/activity/ActivityScheduleCalendar.vue
T
c-zhouhf1 1be063f818 品牌活动整改
幸福工惠移植
2026-10-14 14:33:11 +08:00

940 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<van-popup
:value="visible"
class="activity-schedule-calendar"
position="right"
:style="{ width: '100%', height: '100%' }"
:close-on-click-overlay="false"
:close-on-popstate="false">
<van-nav-bar title="课程日历" left-text="返回" left-arrow
@click-left="closeSignPopup('calendar')"></van-nav-bar>
<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,
'is-signed': day.status === 'signed',
'is-available': day.status === 'available',
'is-pending': day.status === 'pending'
}"
@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-signed"></i>已签到</span>
<span><i class="is-available"></i>可签到</span>
<span><i class="is-pending"></i>未开始</span>
</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"
:disabled="formLoading"
@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-popup>
<van-popup
:value="positionConfirmVisible"
class="activity-schedule-position-popup"
position="right"
:style="{ width: '100%', height: '100%' }"
:close-on-click-overlay="false"
:close-on-popstate="false"
@opened="initMap"
@closed="onPositionPopupClosed">
<div class="activity-schedule-position">
<van-nav-bar title="定位签到" left-text="返回" left-arrow
@click-left="closeSignPopup('position-confirm')"></van-nav-bar>
<div :id="mapContainerId" class="activity-schedule-calendar__map"></div>
<div class="activity-schedule-position__details">
<div class="activity-schedule-position__location-tools">
<span class="activity-schedule-position__location-status" role="status">{{ locationMessage }}</span>
<van-button size="small" plain type="info" icon="aim" :loading="locationLoading"
:disabled="formLoading || !!closingPopupKey" @click="refreshLocation">重新定位</van-button>
</div>
<div class="activity-schedule-position__title">{{ row ? row.courseName : '' }}</div>
<div v-if="selectCourseTime" class="activity-schedule-position__text">
{{ dateKey(selectCourseTime) }} {{ formatTime(selectCourseTime.courseStartTime) }} - {{ formatTime(selectCourseTime.courseEndTime) }}
</div>
<div v-if="row && row.courseLocation" class="activity-schedule-position__text">
<van-icon name="location-o"></van-icon>{{ row.courseLocation }}
</div>
<div class="activity-schedule-position__text">签到范围:{{ row ? (row.radius || 100) : 100 }} 米</div>
</div>
<div class="activity-schedule-position__footer">
<van-button type="info" block round :loading="formLoading" :disabled="formLoading || locationLoading || !!closingPopupKey"
loading-text="签到中..." @click="onConfirmPositionSign">确认签到</van-button>
</div>
</div>
</van-popup>
<van-popup
:value="signVisible"
round
closeable
:safe-area-inset-bottom="true"
:close-on-click-overlay="false"
:close-on-popstate="false"
:style="{ width: '80%', height: '66%' }"
get-container="#app"
@click-close-icon="closeSignPopup('scan')"
@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,
signCodePreview: null,
positionConfirmVisible: false,
formLoading: false,
popupHistoryToken: "",
calendarHistoryOpen: false,
closingPopupKey: "",
markerLayer: null,
userMarkerLayer: null,
locationLoading: false,
locationMessage: "",
locationRequestId: 0,
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
// 所有签到模式的日历均入栈;上层扫码、二维码或定位弹框单独入栈,返回时逐层关闭。
if (!this.calendarHistoryOpen) {
this.calendarHistoryOpen = true
window.h5PopupHistory.open(this.popupHistoryToken, "calendar")
}
},
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) {
// 确认框关闭或定位提交期间不再切换时段,避免重复打卡和签到对象错位。
if (this.formLoading || this.positionConfirmVisible || this.signVisible || this.signCodePreview || this.closingPopupKey) return
this.selectCourseTime = courseTime
if (this.row.signType === 1) {
this.signVisible = true
window.h5PopupHistory.open(this.popupHistoryToken, "scan")
this.$nextTick(() => {
if (this.signVisible) this.$refs.scanCodeRef.init()
})
} else if (this.row.signType === 2) {
this.makeCode()
} else if (this.row.signType === 3 && this.moduleType === "brand") {
this.positionConfirmVisible = true
window.h5PopupHistory.open(this.popupHistoryToken, "position-confirm")
} else {
this.$toast("此签到模式正在升级中")
}
},
/**
* popupKey 为 calendar(日历)、scan(扫码)、sign-code(二维码)或 position-confirm(定位确认);无返回值。
* 仅请求关闭栈顶弹层,由全局历史回调同步显示状态,防止连续点击造成多次返回。
*/
closeSignPopup(popupKey) {
if (this.closingPopupKey) return
if (window.h5PopupHistory.close(this.popupHistoryToken, popupKey)) {
this.closingPopupKey = popupKey
}
},
// 预览定位不提交签到;点击确认时重新获取位置,失败时保留弹框和当前时段供重试。
onConfirmPositionSign() {
if (this.formLoading || this.locationLoading || this.closingPopupKey || !this.positionConfirmVisible) return
this.positionSign()
},
// 退场动画完成后释放地图;快速重新打开时保留新弹框正在使用的实例。
onPositionPopupClosed() {
if (!this.positionConfirmVisible) this.destroyMap()
},
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)
// 二维码预览关闭时同步回退历史;历史回调先释放引用,避免再次触发返回。
const preview = vant.ImagePreview({
images: [content],
closeOnPopstate: false,
onClose: () => {
if (this.signCodePreview === preview) this.closeSignPopup("sign-code")
}
})
this.signCodePreview = preview
window.h5PopupHistory.open(this.popupHistoryToken, "sign-code")
},
/**
* 确认后提交所选时段的点位签到,无方法参数、无返回值。
* 接口参数:timeId 为报名时段 ID,courseId 为课程 ID,lat/lng 为定位经纬度。
* 返回对象的 code 为 0 表示成功,msg 为失败提示;定位或请求结束后释放提交状态。
*/
positionSign() {
if (this.formLoading || this.locationLoading) return
const courseTime = this.selectCourseTime
const course = this.row
const requestId = ++this.locationRequestId
this.formLoading = true
this.locationMessage = "正在获取当前位置..."
this.getLocation((coords, errorMessage) => {
// 定位失败或等待定位时已退出地图弹框,不再发起签到请求。
if (!this.visible || !this.positionConfirmVisible || requestId !== this.locationRequestId || this.row !== course || this.selectCourseTime !== courseTime) {
this.formLoading = false
return
}
this.showCurrentLocation(coords, errorMessage)
if (!coords) {
this.formLoading = false
return
}
// 地图展示使用转换后的坐标,签到请求仍传手机返回的原始 WGS-84 经纬度。
this.$axios.post("/platform/trainSignUp/mine/positionSign", {
timeId: courseTime.id,
courseId: course.id,
lat: coords.lat,
lng: coords.lng
}).then(res => {
if (res.code === 0) {
this.$set(courseTime, "isAttend", true)
this.$toast("签到成功")
// 签到完成后清理本组件的弹框历史,返回时不再出现已完成的确认框或日历。
return window.h5PopupHistory.clear(this.popupHistoryToken)
} else {
this.$toast(res.msg)
}
}).finally(() => {
this.formLoading = false
})
})
},
/**
* callback 接收浏览器 WGS-84 定位对象 {lat, lng, accuracy} 和可选失败提示;无返回值。
* lat/lng 为纬度/经度,accuracy 为精度半径(米);失败时坐标为 null。
* 预览和确认签到均重新定位,不使用上一次定位缓存,是否提交由调用方决定。
*/
getLocation(callback) {
if (!navigator.geolocation) {
callback(null, "当前浏览器不支持定位功能")
return
}
navigator.geolocation.getCurrentPosition(position => {
callback({ lat: position.coords.latitude, lng: position.coords.longitude, accuracy: position.coords.accuracy })
}, error => {
const messageMap = {
1: "请允许浏览器获取位置信息",
2: "无法获取当前位置",
3: "定位超时,请重试"
}
callback(null, messageMap[error.code] || "定位失败,请稍后重试")
}, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 })
},
// 打开地图和点击重新定位只刷新预览;请求编号隔离已关闭或已重新打开的弹框。
refreshLocation() {
if (!this.map || !this.positionConfirmVisible || this.locationLoading || this.formLoading || this.closingPopupKey) return
const requestId = ++this.locationRequestId
this.locationLoading = true
this.locationMessage = "正在获取当前位置..."
this.getLocation((coords, errorMessage) => {
if (requestId !== this.locationRequestId || !this.map || !this.positionConfirmVisible) return
this.locationLoading = false
this.showCurrentLocation(coords, errorMessage)
})
},
/**
* coords 为手机原始定位对象,errorMessage 为失败提示;无返回值,不提交签到。
* 国内坐标复用项目工具转换为腾讯地图使用的 GCJ-02,境外保留原坐标。
* 定位失败时移除旧位置点,避免将过期点位误认为当前位置。
*/
showCurrentLocation(coords, errorMessage) {
if (!coords) {
if (this.userMarkerLayer) this.userMarkerLayer.setGeometries([])
this.locationMessage = errorMessage || "暂未获取到当前位置"
this.$toast(this.locationMessage)
return
}
this.locationMessage = Number.isFinite(coords.accuracy)
? "定位精度约 " + Math.ceil(coords.accuracy) + " 米"
: "已显示我的位置"
if (!this.map || !this.userMarkerLayer) return
const converted = this.$coordinateUtil.outOfChina(coords.lng, coords.lat)
? [coords.lat, coords.lng]
: this.$coordinateUtil.wgs84ToGcj02(coords.lng, coords.lat)
const userPosition = new TMap.LatLng(converted[0], converted[1])
this.userMarkerLayer.setGeometries([{ id: "current-user", styleId: "user", position: userPosition }])
const activityPosition = this.markerLayer.getGeometries()[0].position
// 同时显示签到点和我的位置,并留出边距;两点重合时仍保留可辨识的地图范围。
const latPadding = Math.max(Math.abs(activityPosition.getLat() - converted[0]) * 0.3, 0.0003)
const lngPadding = Math.max(Math.abs(activityPosition.getLng() - converted[1]) * 0.3, 0.0003)
const bounds = new TMap.LatLngBounds(
new TMap.LatLng(Math.min(activityPosition.getLat(), converted[0]) - latPadding, Math.min(activityPosition.getLng(), converted[1]) - lngPadding),
new TMap.LatLng(Math.max(activityPosition.getLat(), converted[0]) + latPadding, Math.max(activityPosition.getLng(), converted[1]) + lngPadding)
)
this.map.fitBounds(bounds)
},
// 仅在侧滑弹框完全展开后创建地图,保证容器已有可用尺寸,避免动画期间地图空白。
initMap() {
if (!this.positionConfirmVisible || !this.showMap || !this.row.courseLocationCoordinates || typeof TMap === "undefined") return
const position = JSON.parse(this.row.courseLocationCoordinates)
const center = new TMap.LatLng(position[0], position[1])
this.destroyMap()
this.map = new TMap.Map(this.mapContainerId, { zoom: 16, center })
this.markerLayer = new TMap.MultiMarker({
map: this.map,
geometries: [{ id: "activity", position: center }]
})
// 蓝点与“我的位置”标签独立于红色签到点;内嵌 SVG 不依赖外部图片请求。
const userIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="80" height="48" viewBox="0 0 80 48">'
+ '<rect x="2" y="1" width="76" height="22" rx="11" fill="#1989fa"/>'
+ '<text x="40" y="16" text-anchor="middle" font-family="sans-serif" font-size="12" fill="white">我的位置</text>'
+ '<circle cx="40" cy="36" r="11" fill="#1989fa" fill-opacity="0.2"/>'
+ '<circle cx="40" cy="36" r="6" fill="#1989fa" stroke="white" stroke-width="2"/></svg>'
this.userMarkerLayer = new TMap.MultiMarker({
map: this.map,
zIndex: 2,
styles: { user: new TMap.MarkerStyle({ width: 80, height: 48, anchor: { x: 40, y: 36 }, src: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(userIcon) }) },
geometries: []
})
this.circle = new TMap.MultiCircle({
map: this.map,
geometries: [{ center, radius: this.row.radius || 100 }]
})
this.refreshLocation()
},
// 关闭弹框或销毁组件时释放地图及覆盖物引用,避免重复打开后残留地图实例。
destroyMap() {
// 使尚未返回的定位请求失效,避免旧结果写入下一次打开的地图。
this.locationRequestId += 1
this.locationLoading = false
this.locationMessage = ""
if (this.map) this.map.destroy()
this.map = null
this.markerLayer = null
this.userMarkerLayer = null
this.circle = null
}
},
created() {
// 复用布局已加载的全局历史管理器,不在组件内注册 popstate 事件。
this.popupHistoryToken = window.h5PopupHistory.register((popupKey) => {
this.closingPopupKey = ""
if (popupKey === "calendar") {
this.calendarHistoryOpen = false
this.visible = false
} else if (popupKey === "scan") {
this.signVisible = false
} else if (popupKey === "sign-code") {
const preview = this.signCodePreview
this.signCodePreview = null
if (preview) preview.close()
} else if (popupKey === "position-confirm") {
this.positionConfirmVisible = false
this.locationRequestId += 1
this.locationLoading = false
}
})
},
beforeDestroy() {
// 页面离开时停止扫码并释放地图,再清理历史并注销回调,避免摄像头继续占用。
if (this.signVisible) this.onSignClose()
this.positionConfirmVisible = false
this.destroyMap()
window.h5PopupHistory.clear(this.popupHistoryToken).then(() => {
window.h5PopupHistory.unregister(this.popupHistoryToken)
})
}
}
</script>
<style scoped>
.activity-schedule-calendar {
display: flex;
flex-direction: column;
overflow: hidden;
background: #f4f7fc;
}
.activity-schedule-calendar > .van-nav-bar {
flex-shrink: 0;
}
.activity-schedule-calendar__body {
flex: 1;
min-height: 0;
padding: 18px 14px calc(24px + env(safe-area-inset-bottom));
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: 100%;
max-width: 44px;
height: 52px;
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: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
display: flex;
width: 34px;
height: 34px;
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-signed > span {
color: #148a52;
}
.activity-schedule-calendar__day.is-available > span {
color: #1675d1;
}
.activity-schedule-calendar__day.is-pending > span {
color: #66758a;
}
/* 选中蓝底与外层状态环独立:切换日期时仍能分辨已签到、可签到和未开始。 */
.activity-schedule-calendar__day.is-selected > span {
color: #fff;
background: #1989fa;
box-shadow: 0 0 0 2px #fff;
}
.activity-schedule-calendar__day.is-today .activity-schedule-calendar__day-number {
margin-top: 4px;
}
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
top: 2px;
color: #fff;
}
.activity-schedule-calendar__day-status {
position: absolute;
top: 50%;
left: 50%;
width: 42px;
height: 42px;
transform: translate(-50%, -50%);
border-radius: 50%;
box-sizing: border-box;
pointer-events: none;
}
.activity-schedule-calendar__day-status.is-signed,
.activity-schedule-calendar__legend .is-signed {
border: 2px solid #19a965;
background: #eaf8f1;
}
.activity-schedule-calendar__day-status.is-available,
.activity-schedule-calendar__legend .is-available {
border: 2px solid #1989fa;
background: #fff;
}
.activity-schedule-calendar__day-status.is-pending,
.activity-schedule-calendar__legend .is-pending {
border: 2px dashed #98a4b5;
}
.activity-schedule-calendar__legend {
display: flex;
margin: 16px 0 20px;
align-items: center;
justify-content: space-between;
gap: 6px;
color: #58677b;
font-size: 12px;
}
.activity-schedule-calendar__legend span {
display: inline-flex;
padding: 8px;
align-items: center;
gap: 6px;
border-radius: 18px;
background: #fff;
white-space: nowrap;
}
.activity-schedule-calendar__legend i {
flex-shrink: 0;
width: 16px;
height: 16px;
border-radius: 50%;
box-sizing: border-box;
}
/* 小屏幕每列不足 42px 时缩小圆环,保留日期间距和选中态的白色间隔。 */
@media (max-width: 350px) {
.activity-schedule-calendar__day-status {
width: 38px;
height: 38px;
}
.activity-schedule-calendar__day > span {
width: 30px;
height: 30px;
}
.activity-schedule-calendar__today-label,
.activity-schedule-calendar__day.is-selected .activity-schedule-calendar__today-label {
top: 0;
}
}
.activity-schedule-position-popup {
background: #f4f7fc;
}
.activity-schedule-position {
display: flex;
height: 100%;
min-height: 0;
flex-direction: column;
overflow-y: auto;
}
.activity-schedule-position > .van-nav-bar {
flex-shrink: 0;
}
.activity-schedule-calendar__map {
flex: 1 0 240px;
width: 100%;
min-height: 240px;
overflow: hidden;
}
.activity-schedule-position__details {
flex-shrink: 0;
padding: 16px;
background: #fff;
}
.activity-schedule-position__title {
color: #263548;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.activity-schedule-position__location-tools {
display: flex;
margin-bottom: 12px;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.activity-schedule-position__location-tools .van-button {
flex-shrink: 0;
}
.activity-schedule-position__location-status {
color: #78869a;
font-size: 12px;
line-height: 18px;
}
.activity-schedule-position__text {
margin-top: 6px;
color: #78869a;
font-size: 13px;
line-height: 20px;
overflow-wrap: anywhere;
}
.activity-schedule-position__text .van-icon {
margin-right: 4px;
vertical-align: middle;
}
.activity-schedule-position__footer {
flex-shrink: 0;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
background: #fff;
}
.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>