You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
8.5 KiB
Vue
221 lines
8.5 KiB
Vue
<template>
|
|
<view class="container">
|
|
<!-- 月份切换 -->
|
|
<view class="month-bar">
|
|
<view class="month-btn" @tap="prevMonth">◀</view>
|
|
<view class="month-text">{{ year }}年{{ month }}月</view>
|
|
<view class="month-btn" @tap="nextMonth">▶</view>
|
|
</view>
|
|
|
|
<!-- 日历 -->
|
|
<view class="calendar card">
|
|
<view class="week-row">
|
|
<view class="week-item" v-for="w in weeks" :key="w">{{ w }}</view>
|
|
</view>
|
|
<view class="date-grid">
|
|
<view class="date-item" v-for="(d, idx) in calendarDays" :key="idx"
|
|
:class="{ selected: selectedDate === d.date, hasPlan: d.available > 0, disabled: !d.date || d.past }"
|
|
@tap="selectDate(d)">
|
|
<view class="date-num">{{ d.day }}</view>
|
|
<view class="date-dot" v-if="d.available > 0"></view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 时间段列表 -->
|
|
<view v-if="selectedDate && periods.length > 0" class="periods">
|
|
<view class="section-title">{{ selectedDate }} 可预约时段</view>
|
|
<view class="period-card card" v-for="p in periods" :key="p.period_id">
|
|
<view class="period-header">
|
|
<view class="period-left">
|
|
<text class="period-time">{{ p.time_range }}</text>
|
|
<text class="period-room" v-if="p.room_name">{{ p.room_name }}</text>
|
|
</view>
|
|
<text class="period-remain" v-if="p.remain_count !== undefined">余 {{ p.remain_count }}</text>
|
|
</view>
|
|
<!-- 时间段模式 -->
|
|
<view v-if="!p.slots">
|
|
<view class="slot-item" :class="{ disabled: !p.enable }" @tap="selectSlot(p)">
|
|
<text>{{ p.enable ? '可选择' : '已满' }}</text>
|
|
</view>
|
|
</view>
|
|
<!-- 时间点模式 -->
|
|
<view v-else class="slot-list">
|
|
<view class="slot-item" v-for="s in p.slots" :key="s.id"
|
|
:class="{ disabled: !s.enable, selected: selectedPlanId === s.id }"
|
|
@tap="s.enable && selectTimePoint(s)">
|
|
{{ s.time }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="selectedDate && periods.length === 0 && !loading" class="empty-state">
|
|
<view class="empty-icon">📅</view>
|
|
<view>该日期暂无可用号源</view>
|
|
</view>
|
|
|
|
<!-- 底部确认按钮 -->
|
|
<view class="bottom-bar" v-if="selectedPlanId">
|
|
<view class="btn-primary" @tap="goConfirm">确认选择</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { getPlanCalendar, getAvailablePlans } from '../../services/plan'
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
applicationIds: '',
|
|
year: new Date().getFullYear(),
|
|
month: new Date().getMonth() + 1,
|
|
weeks: ['日', '一', '二', '三', '四', '五', '六'],
|
|
calendarDays: [],
|
|
calendarData: {},
|
|
selectedDate: '',
|
|
periods: [],
|
|
selectedPlanId: null,
|
|
loading: false
|
|
}
|
|
},
|
|
onLoad(options) {
|
|
this.applicationIds = options.application_ids || ''
|
|
this.generateCalendar()
|
|
this.loadCalendar()
|
|
},
|
|
methods: {
|
|
generateCalendar() {
|
|
const firstDay = new Date(this.year, this.month - 1, 1)
|
|
const lastDay = new Date(this.year, this.month, 0)
|
|
const startWeek = firstDay.getDay()
|
|
const totalDays = lastDay.getDate()
|
|
const today = new Date()
|
|
today.setHours(0,0,0,0)
|
|
|
|
const days = []
|
|
for (let i = 0; i < startWeek; i++) { days.push({ day: '', date: '', past: true, available: 0 }) }
|
|
for (let d = 1; d <= totalDays; d++) {
|
|
const dateStr = `${this.year}-${String(this.month).padStart(2,'0')}-${String(d).padStart(2,'0')}`
|
|
const dateObj = new Date(this.year, this.month - 1, d)
|
|
days.push({ day: d, date: dateStr, past: dateObj < today, available: 0 })
|
|
}
|
|
const remaining = 42 - days.length
|
|
for (let i = 0; i < remaining; i++) { days.push({ day: '', date: '', past: true, available: 0 }) }
|
|
this.calendarDays = days
|
|
},
|
|
async loadCalendar() {
|
|
this.loading = true
|
|
try {
|
|
const ids = this.applicationIds.split(',').map(Number)
|
|
const res = await getPlanCalendar({
|
|
exam_application_ids: ids,
|
|
channel_id: 3,
|
|
year: this.year,
|
|
month: this.month
|
|
})
|
|
const resources = res.data.resources || []
|
|
const dataMap = {}
|
|
resources.forEach(r => {
|
|
(r.slot_data || []).forEach(d => {
|
|
if (!dataMap[d.date]) dataMap[d.date] = 0
|
|
dataMap[d.date] += d.available_count
|
|
})
|
|
})
|
|
this.calendarData = dataMap
|
|
this.calendarDays.forEach(d => {
|
|
if (d.date) d.available = dataMap[d.date] || 0
|
|
})
|
|
} catch (e) {}
|
|
this.loading = false
|
|
},
|
|
prevMonth() {
|
|
this.month--
|
|
if (this.month < 1) { this.month = 12; this.year-- }
|
|
this.selectedDate = ''
|
|
this.periods = []
|
|
this.generateCalendar()
|
|
this.loadCalendar()
|
|
},
|
|
nextMonth() {
|
|
this.month++
|
|
if (this.month > 12) { this.month = 1; this.year++ }
|
|
this.selectedDate = ''
|
|
this.periods = []
|
|
this.generateCalendar()
|
|
this.loadCalendar()
|
|
},
|
|
selectDate(d) {
|
|
if (!d.date || d.past || d.available <= 0) return
|
|
this.selectedDate = d.date
|
|
this.selectedPlanId = null
|
|
this.loadAvailable()
|
|
},
|
|
async loadAvailable() {
|
|
this.loading = true
|
|
try {
|
|
const ids = this.applicationIds.split(',').map(Number)
|
|
const res = await getAvailablePlans({
|
|
exam_application_ids: ids,
|
|
channel_id: 3,
|
|
start_date: this.selectedDate
|
|
})
|
|
const resources = res.data.resources || []
|
|
this.periods = []
|
|
resources.forEach(r => {
|
|
if (r.periods) {
|
|
r.periods.forEach(p => {
|
|
p.room_name = r.room_name
|
|
this.periods.push(p)
|
|
})
|
|
}
|
|
})
|
|
this.periods.sort((a, b) => a.time_range.localeCompare(b.time_range))
|
|
} catch (e) { this.periods = [] }
|
|
this.loading = false
|
|
},
|
|
selectSlot(p) {
|
|
if (!p.enable || !p.plan_id) return
|
|
this.selectedPlanId = p.plan_id
|
|
},
|
|
selectTimePoint(s) {
|
|
this.selectedPlanId = s.id
|
|
},
|
|
goConfirm() {
|
|
uni.navigateTo({
|
|
url: `/pages/appointment/confirm?application_ids=${this.applicationIds}&plan_id=${this.selectedPlanId}&date=${this.selectedDate}`
|
|
})
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.month-bar { display: flex; justify-content: space-between; align-items: center; padding: 20rpx 30rpx; background: #FFF; border-radius: 16rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
|
|
.month-text { font-size: 34rpx; font-weight: 600; color: #333; }
|
|
.month-btn { padding: 16rpx 24rpx; font-size: 28rpx; color: #2B7FD2; }
|
|
.week-row { display: flex; }
|
|
.week-item { flex: 1; text-align: center; font-size: 24rpx; color: #999; padding: 16rpx 0; }
|
|
.date-grid { display: flex; flex-wrap: wrap; }
|
|
.date-item { width: calc(100% / 7); display: flex; flex-direction: column; align-items: center; padding: 12rpx 0; position: relative; }
|
|
.date-num { font-size: 30rpx; color: #333; width: 64rpx; height: 64rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
|
|
.date-item.selected .date-num { background: #2B7FD2; color: #FFF; }
|
|
.date-item.disabled .date-num { color: #DDD; }
|
|
.date-item.hasPlan .date-num { color: #2B7FD2; font-weight: 600; }
|
|
.date-dot { width: 10rpx; height: 10rpx; border-radius: 50%; background: #2B7FD2; margin-top: 4rpx; }
|
|
.date-item.selected .date-dot { background: #FFF; }
|
|
.section-title { font-size: 30rpx; font-weight: 600; color: #333; margin: 24rpx 0 16rpx; }
|
|
.period-card { padding: 24rpx 30rpx; }
|
|
.period-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16rpx; }
|
|
.period-left { display: flex; align-items: center; gap: 12rpx; }
|
|
.period-time { font-size: 30rpx; font-weight: 600; color: #333; }
|
|
.period-room { font-size: 22rpx; color: #999; background: #F5F5F5; padding: 2rpx 12rpx; border-radius: 6rpx; }
|
|
.period-remain { font-size: 24rpx; color: #4CAF50; }
|
|
.slot-list { display: flex; flex-wrap: wrap; gap: 16rpx; }
|
|
.slot-item { padding: 16rpx 32rpx; background: #E8F4FD; color: #2B7FD2; border-radius: 8rpx; font-size: 26rpx; }
|
|
.slot-item.disabled { background: #F5F5F5; color: #CCC; }
|
|
.slot-item.selected { background: #2B7FD2; color: #FFF; }
|
|
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; padding: 20rpx 30rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); background: #FFF; box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.06); }
|
|
</style>
|