Files
yunzerwebsiteallinone/uniapp/pages/tools/schedule/index.vue
T
2026-07-15 18:14:17 +08:00

429 lines
8.5 KiB
Vue

<template>
<view class="page">
<view class="toolbar">
<view class="search-bar">
<FaIcon name="magnifying-glass" color="#909399" :size="16" />
<input
class="search-input"
v-model="keyword"
type="text"
placeholder="搜索日程内容"
placeholder-class="search-placeholder"
confirm-type="search"
@confirm="loadSchedules"
/>
<view v-if="keyword" class="search-clear" @tap="clearSearch">
<FaIcon name="circle-xmark" color="#c0c4cc" :size="16" />
</view>
</view>
<view class="tabs">
<view
v-for="tab in tabs"
:key="tab.value"
class="tab"
:class="{ active: status === tab.value }"
@tap="changeStatus(tab.value)"
>{{ tab.label }}</view>
</view>
</view>
<scroll-view class="list-scroll" scroll-y :show-scrollbar="false">
<view v-if="loading" class="state-box">
<text class="state-text">加载中...</text>
</view>
<view v-else-if="groupedList.length === 0" class="state-box">
<FaIcon name="calendar-days" color="#c0c4cc" :size="40" />
<text class="state-text">{{ keyword ? '未找到相关日程' : '暂无日程,点击右下角新建' }}</text>
</view>
<view v-else class="schedule-list">
<view v-for="group in groupedList" :key="group.key" class="group">
<text class="group-title">{{ group.label }}</text>
<view
v-for="item in group.items"
:key="item.id"
class="schedule-card"
:class="{ done: item.completed }"
>
<view class="schedule-main" @tap="openDetail(item.id)">
<view class="schedule-head">
<view class="status-dot" :class="item.completed ? 'done' : 'pending'" />
<text class="schedule-title">{{ getScheduleSummary(item) }}</text>
</view>
<view class="schedule-meta">
<FaIcon name="clock" color="#909399" :size="12" />
<text>{{ formatDate(item.datetime, true) }}</text>
</view>
<view v-if="item.remindChannels?.length" class="channel-row">
<text
v-for="ch in item.remindChannels"
:key="ch"
class="channel-mini"
>{{ getRemindChannelLabel(ch) }}</text>
<text v-if="item.remindMinutes" class="remind-text">{{ getRemindMinutesLabel(item.remindMinutes) }}</text>
</view>
</view>
<view class="action-row">
<view
class="action-btn"
:class="{ disabled: item.completed }"
@tap="!item.completed && onToggleComplete(item)"
>
<FaIcon name="check" :color="item.completed ? '#c0c4cc' : '#3c9cff'" :size="14" />
<text>{{ item.completed ? '已完成' : '标记完成' }}</text>
</view>
<view v-if="!item.completed" class="action-btn" @tap="openEdit(item.id)">
<FaIcon name="pen-to-square" color="#3c9cff" :size="14" />
<text>编辑</text>
</view>
</view>
</view>
</view>
</view>
<view class="bottom-space" />
</scroll-view>
<view class="fab" @tap="createSchedule">
<FaIcon name="plus" color="#ffffff" :size="22" />
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { fetchSchedules, toggleScheduleComplete, getScheduleSummary, getRemindChannelLabel, getRemindMinutesLabel } from '@/api/schedule.js'
import { formatDate, dayLabel, isToday, startOfDay } from '@/utils/date.js'
import { isLoggedIn } from '@/utils/auth.js'
const keyword = ref('')
const status = ref('all')
const schedules = ref([])
const loading = ref(false)
const tabs = [
{ value: 'all', label: '全部' },
{ value: 'pending', label: '待办' },
{ value: 'done', label: '已完成' }
]
const groupedList = computed(() => {
const map = new Map()
schedules.value.forEach(item => {
const key = startOfDay(item.datetime).getTime()
if (!map.has(key)) {
map.set(key, {
key,
label: dayLabel(item.datetime),
isToday: isToday(item.datetime),
items: []
})
}
map.get(key).items.push(item)
})
return [...map.values()].sort((a, b) => a.key - b.key)
})
onMounted(() => {
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
loadSchedules()
})
onShow(() => {
if (isLoggedIn()) loadSchedules()
})
async function loadSchedules() {
loading.value = true
try {
const res = await fetchSchedules({
keyword: keyword.value,
status: status.value
})
schedules.value = res.list
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function clearSearch() {
keyword.value = ''
loadSchedules()
}
function changeStatus(value) {
status.value = value
loadSchedules()
}
function createSchedule() {
uni.navigateTo({ url: '/pages/tools/schedule/edit' })
}
function openDetail(id) {
uni.navigateTo({ url: `/pages/tools/schedule/detail?id=${id}` })
}
function openEdit(id) {
uni.navigateTo({ url: `/pages/tools/schedule/edit?id=${id}` })
}
async function onToggleComplete(item) {
try {
await toggleScheduleComplete(item.id)
await loadSchedules()
uni.showToast({
title: item.completed ? '已标为待办' : '已标记完成',
icon: 'none'
})
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
background: $color-bg-page;
display: flex;
flex-direction: column;
}
.toolbar {
flex-shrink: 0;
padding: 20rpx $page-padding-x 0;
background: $color-card;
box-shadow: $shadow-header;
}
.search-bar {
display: flex;
align-items: center;
gap: 12rpx;
height: 72rpx;
padding: 0 24rpx;
background: $color-bg-page;
border-radius: $radius-full;
}
.search-input {
flex: 1;
height: 72rpx;
font-size: 28rpx;
color: $color-text;
}
.search-placeholder {
color: $color-text-muted;
font-size: 28rpx;
}
.search-clear {
display: flex;
padding: 8rpx;
}
.tabs {
display: flex;
gap: 16rpx;
padding: 20rpx 0 24rpx;
}
.tab {
padding: 10rpx 28rpx;
border-radius: $radius-full;
font-size: 26rpx;
color: $color-text-secondary;
background: $color-bg-page;
&.active {
color: $color-primary;
background: $color-primary-bg;
font-weight: 500;
}
}
.list-scroll {
flex: 1;
height: 0;
}
.schedule-list {
padding: 24rpx $page-padding-x 0;
}
.group {
margin-bottom: 28rpx;
}
.group-title {
display: block;
font-size: 26rpx;
font-weight: 600;
color: $color-text-secondary;
margin-bottom: 16rpx;
padding-left: 4rpx;
}
.schedule-card {
@include card;
margin-bottom: 16rpx;
overflow: hidden;
&.done .schedule-title {
text-decoration: line-through;
color: $color-text-muted;
}
}
.schedule-main {
padding: 24rpx 24rpx 16rpx;
&:active {
background: $color-bg-page;
}
}
.schedule-head {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.status-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
flex-shrink: 0;
&.pending {
background: $color-primary;
}
&.done {
background: #67c23a;
}
}
.schedule-title {
flex: 1;
font-size: 30rpx;
font-weight: 600;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.schedule-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8rpx;
font-size: 24rpx;
color: $color-text-muted;
padding-left: 24rpx;
margin-top: 8rpx;
}
.channel-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10rpx;
padding-left: 24rpx;
margin-top: 12rpx;
}
.channel-mini {
font-size: 22rpx;
color: $color-primary;
background: $color-primary-bg;
padding: 4rpx 12rpx;
border-radius: $radius-sm;
}
.remind-text {
font-size: 22rpx;
color: $color-text-muted;
}
.action-row {
display: flex;
border-top: 1rpx solid $color-divider;
}
.action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 20rpx 0;
font-size: 24rpx;
color: $color-text-muted;
& + & {
border-left: 1rpx solid $color-divider;
}
&.danger {
color: #f56c6c;
}
&.disabled {
color: #c0c4cc;
}
&:active {
background: $color-bg-page;
}
}
.state-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160rpx 48rpx;
gap: 20rpx;
}
.state-text {
font-size: 26rpx;
color: $color-text-muted;
text-align: center;
}
.fab {
position: fixed;
right: 40rpx;
bottom: calc(40rpx + env(safe-area-inset-bottom));
width: 104rpx;
height: 104rpx;
border-radius: 50%;
background: $color-primary;
box-shadow: 0 8rpx 24rpx rgba(60, 156, 255, 0.35);
display: flex;
align-items: center;
justify-content: center;
&:active {
background: $color-primary-dark;
}
}
.bottom-space {
height: 160rpx;
}
</style>