798 lines
22 KiB
Vue
798 lines
22 KiB
Vue
<!-- 事件页:使用 uview-plus 筛选与空状态组件 -->
|
||
<template>
|
||
<view class="page page-base">
|
||
<up-navbar title="事件" :fixed="false" :safe-area-inset-top="true" :border="false" left-icon="" />
|
||
|
||
<view class="filters">
|
||
<view class="filter-cell" @click="openDrawer('scene')">
|
||
<view class="filter-trigger" :class="{ 'filter-trigger--active': filters.sceneId !== 0 }">
|
||
<text class="filter-text">{{ sceneLabel }}</text>
|
||
<up-icon name="arrow-down" size="13" :color="filters.sceneId !== 0 ? '#12a37a' : '#909399'" />
|
||
</view>
|
||
</view>
|
||
<view class="filter-cell" @click="openDrawer('status')">
|
||
<view class="filter-trigger" :class="{ 'filter-trigger--active': filters.status !== 0 }">
|
||
<text class="filter-text">{{ statusLabel }}</text>
|
||
<up-icon name="arrow-down" size="13" :color="filters.status !== 0 ? '#12a37a' : '#909399'" />
|
||
</view>
|
||
</view>
|
||
<view class="filter-cell" @click="openDrawer('time')">
|
||
<view class="filter-trigger" :class="{ 'filter-trigger--active': !!filters.startDate }">
|
||
<text class="filter-text">{{ filters.timeText || '时间' }}</text>
|
||
<up-icon name="arrow-down" size="13" :color="filters.startDate ? '#12a37a' : '#909399'" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 数据层:筛选栏以下区域;三个下拉抽屉都锚定在这层顶部(筛选栏下缘)展开 -->
|
||
<view class="content-area">
|
||
<view class="content-body">
|
||
<view class="empty-area">
|
||
<up-empty mode="message" text="暂无事件数据" />
|
||
</view>
|
||
<view class="empty-hint">
|
||
<text>调整筛选条件后的事件列表将在此展示</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 遮罩:盖住筛选栏以下全部内容 -->
|
||
<view v-if="drawer" class="content-mask" @click="closeDrawer" @touchmove.stop.prevent="noop" />
|
||
|
||
<!-- 场所抽屉:从筛选栏下缘展开 -->
|
||
<view v-if="drawer === 'scene'" class="drop-panel drop-panel--options">
|
||
<view class="option-drawer-header">
|
||
<text class="option-drawer-title">选择场所</text>
|
||
<view class="option-drawer-close" @click="closeDrawer">
|
||
<up-icon name="close" size="20" color="#909399" />
|
||
</view>
|
||
</view>
|
||
<view class="option-drawer-list">
|
||
<view
|
||
v-for="opt in sceneOptions"
|
||
:key="opt.id"
|
||
class="option-drawer-item"
|
||
:class="{ 'option-drawer-item--active': filters.sceneId === opt.id }"
|
||
@click="selectScene(opt.id)"
|
||
>
|
||
<text class="option-drawer-item__text">{{ opt.name }}</text>
|
||
<up-icon v-if="filters.sceneId === opt.id" name="checkmark" size="16" color="#2CCB98" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 状态抽屉:从筛选栏下缘展开 -->
|
||
<view v-if="drawer === 'status'" class="drop-panel drop-panel--options">
|
||
<view class="option-drawer-header">
|
||
<text class="option-drawer-title">选择状态</text>
|
||
<view class="option-drawer-close" @click="closeDrawer">
|
||
<up-icon name="close" size="20" color="#909399" />
|
||
</view>
|
||
</view>
|
||
<view class="option-drawer-list">
|
||
<view
|
||
v-for="opt in statusOptions"
|
||
:key="opt.id"
|
||
class="option-drawer-item"
|
||
:class="{ 'option-drawer-item--active': filters.status === opt.id }"
|
||
@click="selectStatus(opt.id)"
|
||
>
|
||
<text class="option-drawer-item__text">{{ opt.name }}</text>
|
||
<up-icon v-if="filters.status === opt.id" name="checkmark" size="16" color="#2CCB98" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 时间抽屉:从筛选栏下缘展开(滚动式日历:懒渲染,窗口外月份占位) -->
|
||
<view v-if="drawer === 'time'" class="drop-panel drop-panel--calendar">
|
||
<view class="time-drawer-header">
|
||
<text class="time-drawer-title">选择时间范围</text>
|
||
<view class="time-drawer-close" @click="closeDrawer">
|
||
<up-icon name="close" size="20" color="#909399" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 快捷范围:点选直接填满起止 -->
|
||
<view class="quick-row">
|
||
<view
|
||
v-for="q in quickRanges"
|
||
:key="q.label"
|
||
class="quick-chip"
|
||
:class="{ 'quick-chip--active': activeQuick === q.label }"
|
||
@click="applyQuick(q)"
|
||
>
|
||
<text>{{ q.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 星期行:钉在滚动区上方 -->
|
||
<view class="cal-weekdays">
|
||
<text v-for="w in weekdayLabels" :key="w" class="cal-weekday">{{ w }}</text>
|
||
</view>
|
||
|
||
<!-- 滚动日历:scroll-view 纵向滚月,懒渲染(窗口外月份只渲染占位高度) -->
|
||
<view class="cal-scroll-wrap">
|
||
<scroll-view
|
||
class="cal-scroll"
|
||
scroll-y
|
||
:scroll-into-view="calAnchorId"
|
||
scroll-anchoring
|
||
@scroll="onCalScroll"
|
||
>
|
||
<view
|
||
v-for="m in renderedMonths"
|
||
:key="m.key"
|
||
:id="m.key"
|
||
class="cal-month"
|
||
:style="m.placeholder ? { height: m.height + 'rpx' } : {}"
|
||
>
|
||
<template v-if="!m.placeholder">
|
||
<view class="cal-month-title">
|
||
<text>{{ m.title }}</text>
|
||
</view>
|
||
<view class="cal-grid">
|
||
<view
|
||
v-for="(cell, i) in m.cells"
|
||
:key="i"
|
||
class="cal-cell"
|
||
:class="cell.cls"
|
||
@click="pickDate(cell)"
|
||
>
|
||
<view v-if="cell.day" class="cal-day" :class="cell.dayCls">
|
||
<text class="cal-day-num">{{ cell.day }}</text>
|
||
<text v-if="cell.tag" class="cal-day-tag">{{ cell.tag }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<view class="time-drawer-footer">
|
||
<up-button
|
||
shape="circle"
|
||
:text="rangeReady ? '确定' : '请选择起止日期'"
|
||
color="#2CCB98"
|
||
:disabled="!rangeReady"
|
||
@click="onConfirmClick"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
<script>
|
||
import { getSceneList } from '@/api/scene'
|
||
|
||
// 日历可回溯的年数(仅约束可翻到的最早月份,不影响性能)
|
||
const CALENDAR_YEARS_BACK = 10
|
||
const WEEKDAY_LABELS = ['一', '二', '三', '四', '五', '六', '日']
|
||
|
||
const pad2 = (n) => String(n).padStart(2, '0')
|
||
// Date -> 'YYYY-MM-DD'(本地时区)
|
||
const fmtDate = (d) => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
||
// 'YYYY-MM-DD' -> 当日零点 Date
|
||
const parseDate = (s) => {
|
||
const [y, m, d] = s.split('-').map(Number)
|
||
return new Date(y, m - 1, d)
|
||
}
|
||
// 生成快捷范围选项:近 N 天(含今天)
|
||
const quickRange = (days) => {
|
||
const end = new Date()
|
||
const start = new Date()
|
||
start.setDate(start.getDate() - (days - 1))
|
||
return { label: `近${days}天`, start: fmtDate(start), end: fmtDate(end) }
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
drawer: '', // '' | 'scene' | 'status' | 'time'
|
||
windowWidth: 375, // 窗口宽(px),onLoad 时取系统信息,用于 px→rpx 换算
|
||
calScroll: 0, // 当前滚动位置(rpx 近似,用于懒渲染窗口判定)
|
||
calAnchorId: '', // scroll-into-view 定位的月份锚点
|
||
pendingStart: '', // 临时起点
|
||
pendingEnd: '', // 临时终点
|
||
activeQuick: '',
|
||
weekdayLabels: WEEKDAY_LABELS,
|
||
quickRanges: [quickRange(7), quickRange(30), quickRange(90)],
|
||
filters: {
|
||
sceneId: 0, // 0 表示全部
|
||
status: 0,
|
||
startDate: '',
|
||
endDate: '',
|
||
timeText: ''
|
||
},
|
||
sceneOptions: [{ id: 0, name: '全部' }],
|
||
statusOptions: [
|
||
{ id: 0, name: '全部' },
|
||
{ id: 1, name: '未处理' },
|
||
{ id: 2, name: '已处理' },
|
||
{ id: 3, name: '已忽略' }
|
||
]
|
||
}
|
||
},
|
||
computed: {
|
||
sceneLabel() {
|
||
const hit = this.sceneOptions.find((o) => o.id === this.filters.sceneId)
|
||
return hit ? hit.name : '全部场所'
|
||
},
|
||
statusLabel() {
|
||
const hit = this.statusOptions.find((o) => o.id === this.filters.status)
|
||
return hit ? hit.name : '全部状态'
|
||
},
|
||
// 可选范围边界
|
||
minLimit() {
|
||
const d = new Date()
|
||
d.setFullYear(d.getFullYear() - CALENDAR_YEARS_BACK)
|
||
d.setDate(1)
|
||
return fmtDate(d)
|
||
},
|
||
maxLimit() {
|
||
return fmtDate(new Date())
|
||
},
|
||
// 全部月份元数据:从最早月到当月,含每月份的估算高度(用于占位与滚动定位)
|
||
monthMetas() {
|
||
const metas = []
|
||
const start = parseDate(this.minLimit)
|
||
const now = new Date()
|
||
const endY = now.getFullYear()
|
||
const endM = now.getMonth()
|
||
let y = start.getFullYear()
|
||
let m = start.getMonth()
|
||
let top = 0
|
||
while (y < endY || (y === endY && m <= endM)) {
|
||
const weeks = this.monthWeeks(y, m)
|
||
// 标题行 + 周数×行高(格子 aspect 决定行高≈格子宽×1.15,按 750rpx/7 估算)
|
||
const height = 96 + weeks * Math.round((750 / 7) * 1.15) + 16
|
||
metas.push({
|
||
key: `m-${y}-${m}`,
|
||
year: y,
|
||
month: m,
|
||
title: `${y}年${pad2(m + 1)}月`,
|
||
top,
|
||
height
|
||
})
|
||
top += height
|
||
if (m === 11) {
|
||
y += 1
|
||
m = 0
|
||
} else {
|
||
m += 1
|
||
}
|
||
}
|
||
return metas
|
||
},
|
||
// 懒渲染:只渲染滚动窗口(±4 个月)内的月份,窗口外用等高占位
|
||
renderedMonths() {
|
||
// 滚动位置近似映射到月份索引:找 top 包含 calScroll 的月份
|
||
const viewRpx = 1400 // 滚动窗口可视高度近似(rpx)
|
||
const buffer = 3000 // 窗口前后缓冲(rpx)
|
||
const lo = this.calScroll - buffer
|
||
const hi = this.calScroll + viewRpx + buffer
|
||
return this.monthMetas.map((meta) => {
|
||
const inWindow = meta.top + meta.height > lo && meta.top < hi
|
||
if (!inWindow) {
|
||
return { ...meta, placeholder: true, cells: [] }
|
||
}
|
||
return {
|
||
...meta,
|
||
placeholder: false,
|
||
cells: this.buildMonthCells(meta.year, meta.month)
|
||
}
|
||
})
|
||
},
|
||
// 起止都已选定
|
||
rangeReady() {
|
||
return !!this.pendingStart && !!this.pendingEnd
|
||
}
|
||
},
|
||
onLoad() {
|
||
// px→rpx 换算基准
|
||
const info = uni.getWindowInfo ? uni.getWindowInfo() : uni.getSystemInfoSync()
|
||
this.windowWidth = info.windowWidth || 375
|
||
this.loadScenes()
|
||
},
|
||
methods: {
|
||
// 单格数据:区间内/端点/禁用态一次性算好,模板零逻辑
|
||
buildCell(dateStr, day) {
|
||
const disabled = dateStr < this.minLimit || dateStr > this.maxLimit
|
||
let dayCls = ''
|
||
let tag = ''
|
||
if (!disabled) {
|
||
if (this.pendingStart && dateStr === this.pendingStart) {
|
||
dayCls = 'cal-day--start'
|
||
tag = this.pendingStart === this.pendingEnd ? '起/止' : '开始'
|
||
} else if (this.pendingEnd && dateStr === this.pendingEnd) {
|
||
dayCls = 'cal-day--end'
|
||
tag = '结束'
|
||
} else if (
|
||
this.pendingStart &&
|
||
this.pendingEnd &&
|
||
dateStr > this.pendingStart &&
|
||
dateStr < this.pendingEnd
|
||
) {
|
||
dayCls = 'cal-day--in'
|
||
}
|
||
}
|
||
return { day, date: dateStr, disabled, cls: { 'cal-cell--disabled': disabled }, dayCls, tag }
|
||
},
|
||
// 某年某月的格子(含前置空格)
|
||
buildMonthCells(y, m) {
|
||
const firstDay = new Date(y, m, 1)
|
||
const lead = (firstDay.getDay() + 6) % 7
|
||
const daysInMonth = new Date(y, m + 1, 0).getDate()
|
||
const cells = []
|
||
for (let i = 0; i < lead; i++) cells.push({ day: 0 })
|
||
for (let d = 1; d <= daysInMonth; d++) {
|
||
cells.push(this.buildCell(`${y}-${pad2(m + 1)}-${pad2(d)}`, d))
|
||
}
|
||
return cells
|
||
},
|
||
// 某月占满的周数(1 个前置空格也占一行)
|
||
monthWeeks(y, m) {
|
||
const firstDay = new Date(y, m, 1)
|
||
const lead = (firstDay.getDay() + 6) % 7
|
||
const daysInMonth = new Date(y, m + 1, 0).getDate()
|
||
return Math.ceil((lead + daysInMonth) / 7)
|
||
},
|
||
// 动态加载场所列表:复用场景接口,后端 /scene/list 就绪后自动走真实请求。
|
||
// 失败时保留"全部"选项不阻塞筛选(toast 由请求封装统一弹出)。
|
||
async loadScenes() {
|
||
try {
|
||
const data = await getSceneList()
|
||
const list = Array.isArray(data) ? data : []
|
||
this.sceneOptions = [{ id: 0, name: '全部' }, ...list]
|
||
} catch (error) {
|
||
this.sceneOptions = [{ id: 0, name: '全部' }]
|
||
}
|
||
},
|
||
noop() {},
|
||
openDrawer(name) {
|
||
if (name === 'time') {
|
||
// 回显已提交的起止;无提交时默认定位当月
|
||
this.pendingStart = this.filters.startDate
|
||
this.pendingEnd = this.filters.endDate
|
||
this.activeQuick = ''
|
||
const focus = this.filters.startDate || fmtDate(new Date())
|
||
const d = parseDate(focus)
|
||
const meta = this.monthMetas.find(
|
||
(mm) => mm.year === d.getFullYear() && mm.month === d.getMonth()
|
||
)
|
||
this.calScroll = meta ? meta.top : 0
|
||
// scroll-into-view 需要值变化才生效:先清空再在下个循环赋值
|
||
this.calAnchorId = ''
|
||
this.$nextTick(() => {
|
||
this.calAnchorId = meta ? meta.key : ''
|
||
})
|
||
}
|
||
this.drawer = name
|
||
},
|
||
closeDrawer() {
|
||
this.drawer = ''
|
||
},
|
||
// 场所/状态抽屉点选:写入筛选值并收起抽屉。
|
||
selectScene(id) {
|
||
this.filters.sceneId = id
|
||
this.closeDrawer()
|
||
this.handle()
|
||
},
|
||
selectStatus(id) {
|
||
this.filters.status = id
|
||
this.closeDrawer()
|
||
this.handle()
|
||
},
|
||
// 下拉选项变化:触发筛选回调,便于后续接事件列表接口。
|
||
handle() {
|
||
// eslint-disable-next-line no-console
|
||
console.log('[event filters]', { ...this.filters })
|
||
},
|
||
// 滚动:把 px 滚动值换算回 rpx(750 / 窗口宽),驱动懒渲染窗口移动
|
||
onCalScroll(e) {
|
||
const px = e.detail.scrollTop || 0
|
||
const rpx = px * (750 / this.windowWidth)
|
||
// 跨过一定占位高度差再更新,减少 computed 重算频率
|
||
if (Math.abs(rpx - this.calScroll) > 200) {
|
||
this.calScroll = rpx
|
||
}
|
||
},
|
||
// 点击日期:第一击定起点,第二击定终点(早于起点则重定起点),选满后再点重选
|
||
pickDate(cell) {
|
||
if (!cell.day || cell.disabled) return
|
||
const date = cell.date
|
||
if (!this.pendingStart || (this.pendingStart && this.pendingEnd)) {
|
||
this.pendingStart = date
|
||
this.pendingEnd = ''
|
||
this.activeQuick = ''
|
||
} else if (date < this.pendingStart) {
|
||
this.pendingStart = date
|
||
this.activeQuick = ''
|
||
} else {
|
||
this.pendingEnd = date
|
||
this.activeQuick = ''
|
||
}
|
||
},
|
||
// 快捷范围:直接填满起止并定位到起点月
|
||
applyQuick(q) {
|
||
this.pendingStart = q.start
|
||
this.pendingEnd = q.end
|
||
this.activeQuick = q.label
|
||
const d = parseDate(q.start)
|
||
const meta = this.monthMetas.find(
|
||
(mm) => mm.year === d.getFullYear() && mm.month === d.getMonth()
|
||
)
|
||
this.calAnchorId = ''
|
||
this.$nextTick(() => {
|
||
this.calAnchorId = meta ? meta.key : ''
|
||
})
|
||
},
|
||
// 确定按钮:提交临时起止并写入筛选值
|
||
onConfirmClick() {
|
||
if (!this.rangeReady) {
|
||
return
|
||
}
|
||
this.filters.startDate = this.pendingStart
|
||
this.filters.endDate = this.pendingEnd
|
||
this.filters.timeText = `${this.pendingStart} 至 ${this.pendingEnd}`
|
||
this.closeDrawer()
|
||
this.handle()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/styles/common.css';
|
||
|
||
/* 页面容器:纵向 flex,让数据层撑满筛选栏以下的全部剩余视口 */
|
||
.page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.filters {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: stretch;
|
||
min-height: 96rpx;
|
||
padding: 16rpx 24rpx;
|
||
background: #ffffff;
|
||
border-top: 1rpx solid #ededed;
|
||
}
|
||
|
||
.filter-cell {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
+ .filter-cell {
|
||
margin-left: 18rpx;
|
||
}
|
||
}
|
||
|
||
.filter-trigger {
|
||
height: 100%;
|
||
min-height: 64rpx;
|
||
padding: 0 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #f5f6f8;
|
||
border: 1rpx solid transparent;
|
||
border-radius: 12rpx;
|
||
color: #606266;
|
||
font-size: 26rpx;
|
||
transition: background-color 0.2s, border-color 0.2s;
|
||
|
||
&--active {
|
||
background: #e9fff3;
|
||
border-color: #2ccb98;
|
||
color: #12a37a;
|
||
}
|
||
}
|
||
|
||
.filter-text {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.empty-area {
|
||
min-height: 720rpx;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
padding-top: 120rpx;
|
||
}
|
||
|
||
.empty-hint {
|
||
margin-top: -40rpx;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: #b5b9c0;
|
||
}
|
||
|
||
/* 数据层:撑满筛选栏以下全部视口,是三个下拉抽屉的定位锚点 */
|
||
.content-area {
|
||
position: relative;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* 遮罩:盖住筛选栏以下内容,点击关闭 */
|
||
.content-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.35);
|
||
z-index: 10;
|
||
}
|
||
|
||
/* 下拉面板:从数据层顶部(筛选栏下缘)展开 */
|
||
.drop-panel {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
background: #ffffff;
|
||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.08);
|
||
z-index: 11;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-bottom-left-radius: 24rpx;
|
||
border-bottom-right-radius: 24rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 场所/状态选项面板:内容自适应高度 */
|
||
.drop-panel--options {
|
||
max-height: 60vh;
|
||
}
|
||
|
||
/* 时间面板:日历占剩余空间,但不超过数据层高度(时间抽屉时遮罩仍完整盖住剩余空间) */
|
||
.drop-panel--calendar {
|
||
height: 66vh;
|
||
max-height: 100%;
|
||
}
|
||
|
||
.option-drawer-header {
|
||
padding: 28rpx 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1rpx solid #f0f1f3;
|
||
}
|
||
|
||
.option-drawer-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #2a2d33;
|
||
}
|
||
|
||
.option-drawer-close {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.option-drawer-list {
|
||
padding: 12rpx 24rpx 24rpx;
|
||
}
|
||
|
||
.option-drawer-item {
|
||
min-height: 96rpx;
|
||
padding: 0 28rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-radius: 14rpx;
|
||
|
||
&:active {
|
||
background: #f7f8fa;
|
||
}
|
||
|
||
&--active {
|
||
background: #e9fff3;
|
||
}
|
||
}
|
||
|
||
.option-drawer-item__text {
|
||
font-size: 30rpx;
|
||
color: #2a2d33;
|
||
}
|
||
|
||
.option-drawer-item--active .option-drawer-item__text {
|
||
color: #2CCB98;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 时间面板头/底部(面板已锚定在筛选栏下方,不再 fixed) */
|
||
.time-drawer-header {
|
||
padding: 20rpx 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1rpx solid #f0f1f3;
|
||
}
|
||
|
||
.time-drawer-title {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: #2a2d33;
|
||
}
|
||
|
||
.time-drawer-close {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* 快捷范围行 */
|
||
.quick-row {
|
||
display: flex;
|
||
padding: 20rpx 24rpx 4rpx;
|
||
}
|
||
|
||
.quick-chip {
|
||
flex: 1;
|
||
margin-right: 16rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #f5f6f8;
|
||
border: 1rpx solid transparent;
|
||
border-radius: 999rpx;
|
||
font-size: 24rpx;
|
||
color: #606266;
|
||
|
||
&:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
&--active {
|
||
background: #e9fff3;
|
||
border-color: #2ccb98;
|
||
color: #12a37a;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
/* 星期行:钉在滚动区上方 */
|
||
.cal-weekdays {
|
||
display: flex;
|
||
padding: 0 16rpx;
|
||
border-bottom: 1rpx solid #f0f1f3;
|
||
}
|
||
|
||
.cal-weekday {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: #909399;
|
||
line-height: 48rpx;
|
||
}
|
||
|
||
/* 滚动日历容器:占满抽屉剩余空间 */
|
||
.cal-scroll-wrap {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.cal-scroll {
|
||
height: 100%;
|
||
}
|
||
|
||
/* 月份块 */
|
||
.cal-month {
|
||
padding: 8rpx 16rpx 0;
|
||
}
|
||
|
||
.cal-month-title {
|
||
height: 72rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #2a2d33;
|
||
}
|
||
|
||
/* 日期网格:固定 42 格(前置空格 + 当月天),节点数恒定 */
|
||
.cal-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.cal-cell {
|
||
width: calc(100% / 7);
|
||
box-sizing: border-box;
|
||
padding: 4rpx;
|
||
aspect-ratio: 1 / 1.15;
|
||
display: flex;
|
||
align-items: stretch;
|
||
|
||
&--disabled {
|
||
opacity: 0.3;
|
||
}
|
||
}
|
||
|
||
.cal-day {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 12rpx;
|
||
|
||
&:active {
|
||
background: #f2f3f5;
|
||
}
|
||
|
||
&--start,
|
||
&--end {
|
||
background: #2ccb98;
|
||
}
|
||
|
||
&--start.cal-day--end {
|
||
background: #2ccb98;
|
||
}
|
||
|
||
&--in {
|
||
background: #e9fff3;
|
||
border-radius: 0;
|
||
|
||
&:active {
|
||
background: #ddf6ec;
|
||
}
|
||
}
|
||
}
|
||
|
||
.cal-day-num {
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
color: #2a2d33;
|
||
line-height: 40rpx;
|
||
}
|
||
|
||
.cal-day--start .cal-day-num,
|
||
.cal-day--end .cal-day-num {
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.cal-day--in .cal-day-num {
|
||
color: #12a37a;
|
||
}
|
||
|
||
/* 开始/结束小字标注 */
|
||
.cal-day-tag {
|
||
font-size: 18rpx;
|
||
line-height: 24rpx;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
/* 起止回显:只占内容高度,不与日历滚动区平分空间 */
|
||
.range-preview {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 26rpx;
|
||
color: #6b6f76;
|
||
min-height: 56rpx;
|
||
}
|
||
|
||
.time-drawer-footer {
|
||
flex-shrink: 0;
|
||
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
||
border-top: 1rpx solid #f0f1f3;
|
||
background: #ffffff;
|
||
}
|
||
</style>
|