SEC_Warehouse/pages/event/index.vue

798 lines
22 KiB
Vue
Raw Permalink 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.

<!-- 事件页使用 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, // 窗口宽pxonLoad 时取系统信息,用于 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 滚动值换算回 rpx750 / 窗口宽),驱动懒渲染窗口移动
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>