558 lines
17 KiB
Vue
558 lines
17 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="empty-area">
|
||
<up-empty mode="message" text="暂无事件数据" />
|
||
</view>
|
||
<view class="empty-hint">
|
||
<text>调整筛选条件后的事件列表将在此展示</text>
|
||
</view>
|
||
|
||
<!-- 场所抽屉:顶部下拉选项列表 -->
|
||
<up-popup :show="drawer === 'scene'" mode="top" round="24" :overlay-opacity="0.35" :safe-area-inset-bottom="false" :custom-style="{ width: '100vw' }" @close="closeDrawer">
|
||
<view class="option-drawer">
|
||
<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>
|
||
</up-popup>
|
||
|
||
<!-- 状态抽屉:顶部下拉选项列表 -->
|
||
<up-popup :show="drawer === 'status'" mode="top" round="24" :overlay-opacity="0.35" :safe-area-inset-bottom="false" :custom-style="{ width: '100vw' }" @close="closeDrawer">
|
||
<view class="option-drawer">
|
||
<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>
|
||
</up-popup>
|
||
|
||
<!-- 时间抽屉:顶部下拉,内嵌日历范围选择 -->
|
||
<!-- 注意:up-popup 内容区自带 @touchmove.stop.prevent,会吞掉日历内 scroll-view 的滚动手势,
|
||
所以日历不放在 up-popup 插槽里,而是用条件渲染 + 自绘遮罩实现抽屉 -->
|
||
<view v-if="drawer === 'time'" class="time-mask" @click="closeDrawer" @touchmove.stop.prevent="noop" />
|
||
<view v-if="drawer === 'time'" class="time-drawer">
|
||
<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="time-drawer-body">
|
||
<up-calendar
|
||
:key="calendarKey"
|
||
mode="range"
|
||
:show="drawer === 'time'"
|
||
:page-inline="true"
|
||
:show-confirm="false"
|
||
:show-title="false"
|
||
:show-subtitle="true"
|
||
:row-height="48"
|
||
:month-num="calendarMonthNum"
|
||
:min-date="calendarMinDate"
|
||
:max-date="calendarMaxDate"
|
||
:allow-same-day="true"
|
||
:max-range="31"
|
||
range-prompt="单次最多选择31天"
|
||
:show-range-prompt="true"
|
||
start-text="开始"
|
||
end-text="结束"
|
||
color="#2CCB98"
|
||
:default-date="calendarDefaultDate"
|
||
@confirm="onCalendarConfirm"
|
||
@close="closeDrawer"
|
||
/>
|
||
</view>
|
||
<!-- 确定按钮:自绘钉底。组件内 footer 在 pageInline 多层嵌套下定位不可控。
|
||
选满起止日期时 showConfirm=false 的组件会自动 emit confirm(中间态),
|
||
这里只记录值不关闭抽屉,由这枚按钮确认后才写入筛选 -->
|
||
<view class="time-drawer-footer">
|
||
<up-button
|
||
shape="circle"
|
||
:text="calendarReady ? '确定' : '请选择起止日期'"
|
||
color="#2CCB98"
|
||
:disabled="!calendarReady"
|
||
@click="onConfirmClick"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
<script>
|
||
import { getSceneList } from '@/api/scene'
|
||
|
||
// 日历可回溯的年数,按需调整(monthNum 会随 minDate 动态计算,无需同步改)
|
||
const CALENDAR_YEARS_BACK = 1
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
drawer: '', // '' | 'scene' | 'status' | 'time'
|
||
calendarKey: 0, // 强制重建日历,规避内部 selected 状态残留
|
||
pendingRange: [], // 日历临时选中的起止日期,点确定按钮才写入 filters
|
||
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 : '全部状态'
|
||
},
|
||
// 日历可选范围:过去一年 ~ 今天(不传 minDate 时组件默认从当月开始,无法回看历史)。
|
||
calendarMinDate() {
|
||
const d = new Date()
|
||
d.setFullYear(d.getFullYear() - CALENDAR_YEARS_BACK)
|
||
return d.getTime()
|
||
},
|
||
calendarMaxDate() {
|
||
return Date.now()
|
||
},
|
||
// up-calendar 的 monthNum 同时参与 maxDate 兜底计算:未传 maxDate 时月份终点是
|
||
// minDate + monthNum 个月;传了 maxDate 后它又反过来限制可渲染的月份数。
|
||
// 所以这里动态算出 min~max 实际跨过的月数,保证完整展示过去一年。
|
||
calendarMonthNum() {
|
||
return (
|
||
(new Date(this.calendarMaxDate).getFullYear() -
|
||
new Date(this.calendarMinDate).getFullYear()) *
|
||
12 +
|
||
new Date(this.calendarMaxDate).getMonth() -
|
||
new Date(this.calendarMinDate).getMonth() +
|
||
1
|
||
)
|
||
},
|
||
// 日历回显:已选范围作为 defaultDate(range 模式要求数组)。
|
||
calendarDefaultDate() {
|
||
const dates = [this.filters.startDate, this.filters.endDate].filter(Boolean)
|
||
return dates.length === 2 ? dates : []
|
||
},
|
||
// 日历当前是否已选满起止日期(pendingRange 在日历选中时更新)。
|
||
calendarReady() {
|
||
return this.pendingRange.length >= 2
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.loadScenes()
|
||
},
|
||
methods: {
|
||
// 动态加载场所列表:复用场景接口,后端 /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') {
|
||
// 重建日历实例:up-calendar 内部选中态不会随 defaultDate 变化重置
|
||
this.calendarKey += 1
|
||
// 回显已提交的日期范围,避免重开抽屉时按钮误判为“未选择”
|
||
const dates = [this.filters.startDate, this.filters.endDate].filter(Boolean)
|
||
this.pendingRange = dates.length === 2 ? dates : []
|
||
}
|
||
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 })
|
||
},
|
||
// 日历选中变化(showConfirm=false 时组件在选满起止时自动触发 confirm):
|
||
// 只记录临时选择,不关闭抽屉——最终提交由底部自绘确定按钮完成。
|
||
onCalendarConfirm(dateArr) {
|
||
if (!Array.isArray(dateArr) || dateArr.length < 2) {
|
||
return
|
||
}
|
||
this.pendingRange = dateArr
|
||
},
|
||
// 自绘确定按钮:提交临时选择并写入筛选值。
|
||
// range 模式选满后 confirm 会带完整日期数组(起止之间每一天都在内),
|
||
// 所以取数组首尾作为起止日期,而非前两个元素。
|
||
onConfirmClick() {
|
||
if (!this.calendarReady) {
|
||
return
|
||
}
|
||
const start = this.pendingRange[0]
|
||
const end = this.pendingRange[this.pendingRange.length - 1]
|
||
this.filters.startDate = start
|
||
this.filters.endDate = end
|
||
this.filters.timeText = `${start} 至 ${end}`
|
||
this.closeDrawer()
|
||
this.handle()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/styles/common.css';
|
||
|
||
.filters {
|
||
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;
|
||
}
|
||
|
||
/* 场所/状态选项抽屉 */
|
||
.option-drawer {
|
||
background: #ffffff;
|
||
padding-bottom: 24rpx;
|
||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 时间抽屉:自绘遮罩 + 顶部面板(不经过 up-popup,避免其 touchmove 拦截吞掉日历滚动手势) */
|
||
.time-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.35);
|
||
z-index: 10075;
|
||
}
|
||
|
||
.time-drawer {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 66vh; /* 固定高度:日历窗口+按钮填满,按钮钉底 */
|
||
z-index: 10076;
|
||
background: #ffffff;
|
||
border-bottom-left-radius: 32rpx;
|
||
border-bottom-right-radius: 32rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.time-drawer-body {
|
||
/* 占满抽屉剩余空间;日历内部 scroll-view 通过 calc 抬高到填满 body,
|
||
组件内的确定按钮再用绝对定位钉回 body 底部,消除按钮下方空白 */
|
||
flex: 1;
|
||
min-height: 0;
|
||
position: relative;
|
||
}
|
||
|
||
/* 日历滚动窗口高度。
|
||
pageInline 下 up-calendar 内部仍套着 u-popup(.u-popup → .u-transition → .u-popup__content),
|
||
这几层默认高度都是 auto,导致 scroll-view 的百分比高度无法解析
|
||
(退化成内容高度=全部月份堆叠、滚动窗口失效)。这里把中间层逐级撑到 100%,
|
||
再让 scroll-view 用 flex 填满 header 以下的剩余空间,实现自适应、无空隙。 */
|
||
.time-drawer-body :deep(.u-popup),
|
||
.time-drawer-body :deep(.u-transition),
|
||
.time-drawer-body :deep(.u-popup__content) {
|
||
height: 100%;
|
||
}
|
||
|
||
.time-drawer-body :deep(.u-calendar) {
|
||
box-sizing: border-box;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 0 16rpx;
|
||
}
|
||
|
||
.time-drawer-body :deep(.u-calendar-header) {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.time-drawer-body :deep(.u-calendar .u-scroll-view),
|
||
.time-drawer-body :deep(.u-calendar scroll-view) {
|
||
flex: 1;
|
||
min-height: 0;
|
||
height: auto !important;
|
||
}
|
||
|
||
/* 确定按钮:不依赖组件内 footer(在多层嵌套里定位/裁切不可控),
|
||
在 body 底部自绘一枚按钮,选满起止日期后可点 */
|
||
.time-drawer-footer {
|
||
flex-shrink: 0;
|
||
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
||
border-top: 1rpx solid #f0f1f3;
|
||
background: #ffffff;
|
||
}
|
||
|
||
/* 副标题(当前滚动到的月份):弱化为居中小胶囊 */
|
||
.time-drawer-body :deep(.u-calendar-header__subtitle) {
|
||
align-self: center;
|
||
margin: 8rpx 0 4rpx;
|
||
padding: 4rpx 24rpx;
|
||
background: #f2f3f5;
|
||
border-radius: 999rpx;
|
||
height: auto;
|
||
line-height: 40rpx;
|
||
font-size: 24rpx;
|
||
font-weight: 400;
|
||
color: #6b6f76;
|
||
}
|
||
|
||
/* 星期行 */
|
||
.time-drawer-body :deep(.u-calendar-header__weekdays) {
|
||
padding: 8rpx 0;
|
||
border-bottom: 1rpx solid #f0f1f3;
|
||
}
|
||
|
||
.time-drawer-body :deep(.u-calendar-header__weekdays__weekday) {
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
color: #909399;
|
||
line-height: 44rpx;
|
||
}
|
||
|
||
.time-drawer-body :deep(.u-calendar-month-wrapper) {
|
||
/* 不能加横向 padding:首日偏移用 JS 实测容器宽计算、格子宽用 CSS 百分比,
|
||
两者对 padding 的包含不同会造成偏差,偏移大的月份首行末尾格子会被挤换行 */
|
||
margin-top: 0;
|
||
}
|
||
|
||
/* 每月标题行 */
|
||
.time-drawer-body :deep(.u-calendar-month__title) {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #2a2d33;
|
||
padding: 16rpx 12rpx;
|
||
line-height: 64rpx;
|
||
height: 64rpx;
|
||
}
|
||
|
||
/* 月份背景水印:默认 155px 过大过亮,缩小压淡 */
|
||
.time-drawer-body :deep(.u-calendar-month__days__month-mark-wrapper__text) {
|
||
font-size: 110rpx;
|
||
color: rgba(231, 232, 234, 0.4);
|
||
}
|
||
|
||
.time-drawer-body :deep(.u-calendar-month__days__day) {
|
||
padding: 8rpx;
|
||
}
|
||
|
||
/* 日期字号恢复正常可读尺寸(此前随抽屉缩小被误压) */
|
||
.time-drawer-body :deep(.u-calendar-month__days__day__select__info) {
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
color: #2a2d33;
|
||
}
|
||
|
||
/* 开始/结束提示小字 */
|
||
.time-drawer-body :deep(.u-calendar-month__days__day__select__buttom-info) {
|
||
font-size: 20rpx;
|
||
color: #6b6f76;
|
||
}
|
||
</style>
|