Files
yunzerwebsiteallinone/uniapp/pages/tools/schedule/reminder.vue
T

441 lines
9.2 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>
<view class="page">
<!-- 本周概览 -->
<view class="hero-card">
<view class="hero-left">
<text class="hero-sub">查看每周日程提醒</text>
<view class="hero-num-row">
<text class="hero-label">本周日程(条)</text>
<text class="hero-num">{{ weekItems.length }}</text>
</view>
</view>
<view class="hero-illus">
<FaIcon name="bell" color="#ffffff" :size="26" />
</view>
</view>
<!-- 周历条 -->
<view class="week-card">
<view
v-for="d in weekDays"
:key="d.key"
class="week-item"
@tap="pickWeekDay(d)"
>
<text class="week-name">{{ d.name }}</text>
<view
class="week-date"
:class="{ today: d.isToday, selected: d.key === selectedKey }"
>{{ d.day }}</view>
<view class="week-dot" :class="{ show: d.count > 0 }" />
</view>
</view>
<!-- 我的日程 -->
<view class="section-head">
<text class="section-title">我的日程({{ shownItems.length }})</text>
<view class="range-tabs">
<text class="range-tab" :class="{ active: range === 'day' }" @tap="showToday">今日</text>
<text class="range-sep">|</text>
<text class="range-tab" :class="{ active: range === 'week' }" @tap="range = 'week'">本周</text>
</view>
</view>
<u-empty
v-if="!loading && shownItems.length === 0"
mode="list"
:icon-size="72"
:text="range === 'week' ? '本周暂无日程提醒' : '当天暂无日程提醒'"
/>
<view v-else class="rem-list">
<view v-for="item in shownItems" :key="item.id" class="rem-card" @tap="openDetail(item.id)">
<view class="rem-time">
<text class="rem-day">{{ shortDayLabel(item.datetime) }}</text>
<text class="rem-clock">{{ timeText(item.datetime) }}</text>
</view>
<view class="rem-body">
<text class="rem-title">{{ item.content || item.title }}</text>
<view class="rem-meta">
<FaIcon name="clock" color="#94a3b8" :size="11" />
<text class="rem-meta-text">{{ fullTime(item.datetime) }}</text>
</view>
<view class="rem-meta">
<FaIcon name="bell" color="#94a3b8" :size="11" />
<text class="rem-meta-text">{{ remindLine(item) }}</text>
</view>
</view>
</view>
</view>
<view class="bottom-space" />
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { fetchSchedules, getRemindChannelLabel, getRemindMinutesLabel } from '@/api/schedule.js'
import { pad, startOfDay, isToday } from '@/utils/date.js'
import { isLoggedIn } from '@/utils/auth.js'
/** 周一为一周第一天 */
const WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日']
const schedules = ref([])
const loading = ref(false)
const range = ref('day')
const selectedDate = ref(startOfDay(new Date()))
function dateKey(value) {
const d = value instanceof Date ? value : new Date(value)
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
function startOfWeek(base = new Date()) {
const d = startOfDay(base)
const offset = (d.getDay() + 6) % 7
return new Date(d.getFullYear(), d.getMonth(), d.getDate() - offset)
}
const weekStart = computed(() => startOfWeek(selectedDate.value))
const selectedKey = computed(() => dateKey(selectedDate.value))
/** 只保留已设置提醒渠道的日程 */
const reminders = computed(() =>
schedules.value.filter((item) => item.remindChannels && item.remindChannels.length > 0)
)
/** 提醒按日期聚合,key 为 YYYY-MM-DD */
const reminderMap = computed(() => {
const map = {}
reminders.value.forEach((item) => {
if (!item.datetime) return
const key = dateKey(new Date(item.datetime))
if (!map[key]) map[key] = []
map[key].push(item)
})
return map
})
const weekDays = computed(() => {
const base = weekStart.value
return Array.from({ length: 7 }, (_, i) => {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + i)
const key = dateKey(d)
return {
key,
date: d,
name: '周' + WEEK_LABELS[i],
day: d.getDate(),
isToday: isToday(d),
count: (reminderMap.value[key] || []).length
}
})
})
/** 本周(周一~周日)的提醒,按时间升序 */
const weekItems = computed(() => {
const base = weekStart.value
const startTs = base.getTime()
const endTs = startTs + 7 * 24 * 60 * 60 * 1000
return reminders.value
.filter((item) => item.datetime >= startTs && item.datetime < endTs)
.sort((a, b) => a.datetime - b.datetime)
})
const shownItems = computed(() => {
if (range.value === 'week') return weekItems.value
return (reminderMap.value[selectedKey.value] || [])
.slice()
.sort((a, b) => a.datetime - b.datetime)
})
function timeText(timestamp) {
const d = new Date(timestamp)
return `${pad(d.getHours())}:${pad(d.getMinutes())}`
}
function fullTime(timestamp) {
const d = new Date(timestamp)
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${timeText(timestamp)}`
}
function shortDayLabel(timestamp) {
if (isToday(timestamp)) return '今天'
const d = new Date(timestamp)
return '周' + WEEK_LABELS[(d.getDay() + 6) % 7]
}
function remindLine(item) {
const channels = (item.remindChannels || [])
.map((ch) => getRemindChannelLabel(ch))
.join('、')
const parts = [getRemindMinutesLabel(item.remindMinutes)]
if (channels) parts.push(channels)
return parts.join(' · ')
}
function pickWeekDay(d) {
selectedDate.value = startOfDay(d.date)
range.value = 'day'
}
function showToday() {
selectedDate.value = startOfDay(new Date())
range.value = 'day'
}
function openDetail(id) {
uni.navigateTo({ url: `/pages/tools/schedule/detail?id=${id}` })
}
onMounted(() => {
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
loadReminders()
})
async function loadReminders() {
loading.value = true
try {
const res = await fetchSchedules({ pageSize: 100 })
schedules.value = res.list
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
padding: 24rpx $page-padding-x 0;
background: $color-bg-page;
box-sizing: border-box;
}
/* ---------- 本周概览 ---------- */
.hero-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-radius: $radius-lg;
background: linear-gradient(135deg, #1e3a8a, #2563eb);
box-shadow: 0 12rpx 30rpx rgba(37, 99, 235, 0.28);
}
.hero-left {
flex: 1;
min-width: 0;
}
.hero-sub {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.78);
}
.hero-num-row {
display: flex;
align-items: baseline;
gap: 16rpx;
margin-top: 12rpx;
}
.hero-label {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
.hero-num {
font-size: 56rpx;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.hero-illus {
flex-shrink: 0;
width: 96rpx;
height: 96rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
}
/* ---------- 周历条 ---------- */
.week-card {
display: flex;
margin-top: 20rpx;
padding: 20rpx 8rpx 16rpx;
background: $color-card;
border-radius: $radius-lg;
box-shadow: $shadow-card;
}
.week-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.week-name {
font-size: 22rpx;
color: $color-text-muted;
}
.week-date {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: $color-text;
&.today {
color: $color-primary;
font-weight: 600;
}
&.selected {
background: $color-primary;
color: #ffffff;
font-weight: 600;
}
}
.week-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: transparent;
&.show {
background: $color-primary;
}
}
/* ---------- 我的日程 ---------- */
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 32rpx 0 16rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: $color-text;
}
.range-tabs {
display: flex;
align-items: center;
gap: 14rpx;
}
.range-tab {
font-size: 26rpx;
color: $color-text-muted;
&.active {
color: $color-primary;
font-weight: 600;
}
}
.range-sep {
font-size: 22rpx;
color: $color-border;
}
.rem-list {
display: flex;
flex-direction: column;
}
.rem-card {
@include card;
display: flex;
gap: 24rpx;
padding: 26rpx 24rpx;
margin-bottom: 16rpx;
&:active {
background: $color-bg-page;
}
}
.rem-time {
flex-shrink: 0;
width: 110rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6rpx;
}
.rem-day {
font-size: 24rpx;
color: $color-text-muted;
}
.rem-clock {
font-size: 32rpx;
font-weight: 700;
color: $color-text;
}
.rem-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10rpx;
border-left: 1rpx solid $color-divider;
padding-left: 24rpx;
}
.rem-title {
font-size: 29rpx;
font-weight: 600;
color: $color-text;
word-break: break-all;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.rem-meta {
display: flex;
align-items: center;
gap: 10rpx;
}
.rem-meta-text {
flex: 1;
min-width: 0;
font-size: 23rpx;
color: $color-text-muted;
word-break: break-all;
}
.bottom-space {
height: 60rpx;
}
</style>