OfficeSystem/pages/add-event/index.vue
2025-11-04 17:10:40 +08:00

899 lines
23 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="add-event-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<text class="nav-btn" @click="handleCancel">取消</text>
<text class="nav-title">新建日程</text>
<text class="nav-btn nav-btn-primary" @click="handleSave">完成</text>
</view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-scroll" scroll-y>
<!-- 标题输入 -->
<view class="form-item">
<input
v-model="formData.title"
class="title-input"
placeholder="添加标题"
placeholder-style="color: #999;"
/>
</view>
<!-- 时间日期选择 -->
<view class="form-item time-section">
<view class="time-item" @click="openStartPicker">
<view class="time-icon">🕐</view>
<view class="time-content">
<text class="time-value">{{ startTime }}</text>
<text class="time-date">{{ startDateText }}</text>
</view>
<text class="arrow"></text>
</view>
<text class="time-separator">→</text>
<view class="time-item" @click="openEndPicker">
<view class="time-icon">🕐</view>
<view class="time-content">
<text class="time-value">{{ endTime }}</text>
<text class="time-date">{{ endDateText }}</text>
</view>
<text class="arrow"></text>
</view>
</view>
<!-- 全天开关 -->
<view class="form-item switch-item">
<text class="label">全天</text>
<switch :checked="formData.allDay" @change="toggleAllDay" color="#2885ff" />
</view>
<!-- 重复选项 -->
<view class="form-item clickable-item" @click="openRepeatPicker">
<text class="label">{{ repeatText }}</text>
<text class="arrow"></text>
</view>
<!-- 描述输入 -->
<view class="form-item clickable-item" @click="showDescInput = true">
<view class="desc-icon">📄</view>
<view class="desc-content">
<text v-if="formData.description" class="desc-text">{{ formData.description }}</text>
<text v-else class="desc-placeholder">添加描述</text>
</view>
<text class="arrow"></text>
</view>
<!-- 日程颜色 -->
<view class="form-item clickable-item" @click="showColorPicker = true">
<view class="color-dot" :style="{ backgroundColor: formData.color }"></view>
<text class="label">日程颜色</text>
<text class="arrow"></text>
</view>
<!-- 提醒设置 -->
<view class="form-item clickable-item" @click="showReminderPicker = true">
<view class="reminder-icon">🔔</view>
<text class="reminder-text">{{ reminderText }}</text>
<text class="arrow"></text>
</view>
</scroll-view>
<!-- 开始日期时间选择弹窗 -->
<view v-if="showStartDatePicker || showStartTimePicker" class="modal-mask" @click="closeStartPicker">
<view class="modal-content datetime-modal" @click.stop>
<view class="modal-title">选择开始时间</view>
<view class="datetime-content">
<!-- 日期选择 -->
<view class="date-section">
<text class="section-label">日期</text>
<uv-calendar
ref="startDateCalendar"
mode="single"
@confirm="handleStartDateConfirm"
/>
</view>
<!-- 时间选择 -->
<view class="time-section-picker">
<text class="section-label">时间</text>
<picker
mode="multiSelector"
:value="startPickerIndex"
:range="startPickerRange"
@change="handleStartTimeChange"
>
<view class="picker-display">
{{ formatTime(formData.startHour, formData.startMin) }}
</view>
</picker>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="closeStartPicker">取消</button>
<button class="modal-btn primary" @click="closeStartPicker">确定</button>
</view>
</view>
</view>
<!-- 结束日期时间选择弹窗 -->
<view v-if="showEndDatePicker || showEndTimePicker" class="modal-mask" @click="closeEndPicker">
<view class="modal-content datetime-modal" @click.stop>
<view class="modal-title">选择结束时间</view>
<view class="datetime-content">
<!-- 日期选择 -->
<view class="date-section">
<text class="section-label">日期</text>
<uv-calendar
ref="endDateCalendar"
mode="single"
@confirm="handleEndDateConfirm"
/>
</view>
<!-- 时间选择 -->
<view class="time-section-picker">
<text class="section-label">时间</text>
<picker
mode="multiSelector"
:value="endPickerIndex"
:range="endPickerRange"
@change="handleEndTimeChange"
>
<view class="picker-display">
{{ formatTime(formData.endHour, formData.endMin) }}
</view>
</picker>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="closeEndPicker">取消</button>
<button class="modal-btn primary" @click="closeEndPicker">确定</button>
</view>
</view>
</view>
<!-- 描述输入弹窗 -->
<view v-if="showDescInput" class="modal-mask" @click="showDescInput = false">
<view class="modal-content" @click.stop>
<view class="modal-title">添加描述</view>
<textarea
v-model="formData.description"
class="desc-textarea"
placeholder="请输入描述"
auto-height
/>
<view class="modal-buttons">
<button class="modal-btn" @click="showDescInput = false">取消</button>
<button class="modal-btn primary" @click="showDescInput = false">确定</button>
</view>
</view>
</view>
<!-- 颜色选择弹窗 -->
<view v-if="showColorPicker" class="modal-mask" @click="showColorPicker = false">
<view class="modal-content" @click.stop>
<view class="modal-title">选择颜色</view>
<view class="color-options">
<view
v-for="color in colorOptions"
:key="color"
class="color-option"
:class="{ active: formData.color === color }"
:style="{ backgroundColor: color }"
@click="selectColor(color)"
></view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="showColorPicker = false">取消</button>
<button class="modal-btn primary" @click="showColorPicker = false">确定</button>
</view>
</view>
</view>
<!-- 重复选择弹窗 -->
<view v-if="showRepeatPicker" class="modal-mask" @click="handleCancelRepeat">
<view class="modal-content" @click.stop>
<view class="modal-title">重复设置</view>
<view class="repeat-options">
<view
v-for="item in repeatOptions"
:key="item.value"
class="repeat-option"
:class="{ active: tempRepeat === item.value }"
@click="tempRepeat = item.value"
>
<text>{{ item.label }}</text>
<text v-if="tempRepeat === item.value" class="check">✓</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="handleCancelRepeat">取消</button>
<button class="modal-btn primary" @click="handleConfirmRepeat">确定</button>
</view>
</view>
</view>
<!-- 提醒选择弹窗 -->
<view v-if="showReminderPicker" class="modal-mask" @click="showReminderPicker = false">
<view class="modal-content" @click.stop>
<view class="modal-title">提醒设置</view>
<view class="reminder-options">
<view
v-for="item in reminderOptions"
:key="item.value"
class="reminder-option"
:class="{ active: formData.reminder === item.value }"
@click="selectReminder(item.value)"
>
<text>{{ item.label }}</text>
<text v-if="formData.reminder === item.value" class="check">✓</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn" @click="showReminderPicker = false">取消</button>
<button class="modal-btn primary" @click="showReminderPicker = false">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
// 表单数据
const formData = ref({
title: '',
startDate: '',
startHour: 16,
startMin: 0,
endDate: '',
endHour: 17,
endMin: 0,
allDay: false,
repeat: 'none',
description: '',
color: '#B3D9FF',
reminder: 15 // 提前多少分钟提醒
});
// 显示状态
const showStartTimePicker = ref(false);
const showStartDatePicker = ref(false);
const showEndTimePicker = ref(false);
const showEndDatePicker = ref(false);
const showDescInput = ref(false);
const showColorPicker = ref(false);
const showRepeatPicker = ref(false);
const showReminderPicker = ref(false);
// 临时数据(用于弹窗暂存)
const tempRepeat = ref('none');
// 时间选择器相关
const hourOptions = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0') + '时');
const minOptions = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0') + '分');
const startPickerRange = ref([hourOptions, minOptions]);
const startPickerIndex = ref([16, 0]);
const endPickerRange = ref([hourOptions, minOptions]);
const endPickerIndex = ref([17, 0]);
const startDateCalendar = ref(null);
const endDateCalendar = ref(null);
// 颜色选项
const colorOptions = [
'#B3D9FF', // 更淡的蓝色
'#FFE8CC', // 更淡的橙色
'#FFD6D9', // 更淡的红色
'#D4F3E8', // 更淡的绿色
'#FFF4CC', // 更淡的黄色
'#E6E5FC', // 更淡的紫色
];
// 重复选项
const repeatOptions = [
{ label: '不重复', value: 'none' },
{ label: '每天', value: 'daily' },
{ label: '每周', value: 'weekly' },
{ label: '每月', value: 'monthly' },
{ label: '每年', value: 'yearly' }
];
// 提醒选项
const reminderOptions = [
{ label: '不提醒', value: 0 },
{ label: '开始时', value: 0 },
{ label: '开始前5分钟', value: 5 },
{ label: '开始前15分钟', value: 15 },
{ label: '开始前30分钟', value: 30 },
{ label: '开始前1小时', value: 60 },
{ label: '开始前1天', value: 1440 }
];
// 格式化时间
const formatTime = (hour, min) => {
const h = String(hour).padStart(2, '0');
const m = String(min).padStart(2, '0');
return `${h}:${m}`;
};
// 格式化日期文本
const formatDateText = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const month = date.getMonth() + 1;
const day = date.getDate();
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const weekday = weekdays[date.getDay()];
return `${month}${day}${weekday}`;
};
// 计算属性
const startTime = computed(() => {
return formData.value.allDay ? '全天' : formatTime(formData.value.startHour, formData.value.startMin);
});
const endTime = computed(() => {
return formData.value.allDay ? '' : formatTime(formData.value.endHour, formData.value.endMin);
});
const startDateText = computed(() => {
return formatDateText(formData.value.startDate);
});
const endDateText = computed(() => {
return formatDateText(formData.value.endDate || formData.value.startDate);
});
// 打开开始时间选择器
const openStartPicker = () => {
if (formData.value.allDay) {
showStartDatePicker.value = true;
} else {
showStartDatePicker.value = true;
showStartTimePicker.value = true;
}
// 更新选择器索引
startPickerIndex.value = [formData.value.startHour, formData.value.startMin];
};
// 打开结束时间选择器
const openEndPicker = () => {
if (formData.value.allDay) {
showEndDatePicker.value = true;
} else {
showEndDatePicker.value = true;
showEndTimePicker.value = true;
}
// 更新选择器索引
endPickerIndex.value = [formData.value.endHour, formData.value.endMin];
};
// 关闭开始选择器
const closeStartPicker = () => {
showStartDatePicker.value = false;
showStartTimePicker.value = false;
};
// 关闭结束选择器
const closeEndPicker = () => {
showEndDatePicker.value = false;
showEndTimePicker.value = false;
};
const reminderText = computed(() => {
if (formData.value.reminder === 0) {
return '不提醒';
}
if (formData.value.reminder === 1440) {
return '开始前1天,应用弹窗提醒我';
}
if (formData.value.reminder === 60) {
return `开始前1小时,应用弹窗提醒我`;
}
return `开始前${formData.value.reminder}分钟,应用弹窗提醒我`;
});
const repeatText = computed(() => {
const option = repeatOptions.find(opt => opt.value === formData.value.repeat);
return option ? option.label : '不重复';
});
// 初始化日期
const initDates = (dateStr = '') => {
// 如果没有传入日期参数,使用今天
if (!dateStr) {
const today = new Date();
dateStr = today.toISOString().slice(0, 10);
}
formData.value.startDate = dateStr;
formData.value.endDate = dateStr;
// 设置默认时间为当前时间到一小时后(如果不是全天)
if (!formData.value.allDay) {
const today = new Date();
formData.value.startHour = today.getHours();
formData.value.startMin = Math.ceil(today.getMinutes() / 5) * 5; // 向上取整到5的倍数
const endTime = new Date(today);
endTime.setHours(today.getHours() + 1);
formData.value.endHour = endTime.getHours();
formData.value.endMin = endTime.getMinutes();
startPickerIndex.value = [formData.value.startHour, formData.value.startMin];
endPickerIndex.value = [formData.value.endHour, formData.value.endMin];
}
};
// 处理开始日期确认
const handleStartDateConfirm = (e) => {
const formattedDate = formatDateToYYYYMMDD(e);
if (formattedDate) {
formData.value.startDate = formattedDate;
// 如果结束日期未设置或早于开始日期,更新结束日期
if (!formData.value.endDate || formData.value.endDate < formData.value.startDate) {
formData.value.endDate = formData.value.startDate;
}
}
};
// 处理开始时间变化
const handleStartTimeChange = (e) => {
const [hourIndex, minIndex] = e.detail.value;
formData.value.startHour = hourIndex;
formData.value.startMin = minIndex;
startPickerIndex.value = [hourIndex, minIndex];
// 如果结束时间早于开始时间,自动调整结束时间
if (formData.value.endDate === formData.value.startDate) {
const startTotalMin = formData.value.startHour * 60 + formData.value.startMin;
const endTotalMin = formData.value.endHour * 60 + formData.value.endMin;
if (endTotalMin <= startTotalMin) {
const newEndTotalMin = startTotalMin + 60; // 默认延长1小时
formData.value.endHour = Math.floor(newEndTotalMin / 60) % 24;
formData.value.endMin = newEndTotalMin % 60;
endPickerIndex.value = [formData.value.endHour, formData.value.endMin];
}
}
};
// 处理结束日期确认
const handleEndDateConfirm = (e) => {
const formattedDate = formatDateToYYYYMMDD(e);
if (formattedDate) {
formData.value.endDate = formattedDate;
}
};
// 处理结束时间变化
const handleEndTimeChange = (e) => {
const [hourIndex, minIndex] = e.detail.value;
formData.value.endHour = hourIndex;
formData.value.endMin = minIndex;
endPickerIndex.value = [hourIndex, minIndex];
};
// 格式化日期为 YYYY-MM-DD复用首页的逻辑
function formatDateToYYYYMMDD(dateInput) {
if (!dateInput) return '';
let dateStr = '';
if (typeof dateInput === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateInput)) {
return dateInput;
}
if (typeof dateInput === 'string') {
dateStr = dateInput;
} else if (dateInput?.date) {
dateStr = dateInput.date;
} else if (dateInput?.value) {
dateStr = dateInput.value;
} else if (Array.isArray(dateInput) && dateInput.length > 0) {
dateStr = typeof dateInput[0] === 'string' ? dateInput[0] : (dateInput[0]?.date || dateInput[0]?.value || '');
}
if (!dateStr) return '';
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) {
return dateStr.slice(0, 10);
}
return '';
}
// 切换全天
const toggleAllDay = () => {
formData.value.allDay = !formData.value.allDay;
if (formData.value.allDay) {
formData.value.startHour = 0;
formData.value.startMin = 0;
formData.value.endHour = 23;
formData.value.endMin = 59;
startPickerIndex.value = [0, 0];
endPickerIndex.value = [23, 59];
}
};
// 选择颜色
const selectColor = (color) => {
formData.value.color = color;
};
// 打开重复选择弹窗
const openRepeatPicker = () => {
// 打开弹窗时,将当前值复制到临时变量
tempRepeat.value = formData.value.repeat;
showRepeatPicker.value = true;
};
// 取消重复选择
const handleCancelRepeat = () => {
// 恢复原始值
tempRepeat.value = formData.value.repeat;
showRepeatPicker.value = false;
};
// 确认重复选择
const handleConfirmRepeat = () => {
// 将临时变量的值更新到表单数据
formData.value.repeat = tempRepeat.value;
showRepeatPicker.value = false;
};
// 选择提醒
const selectReminder = (value) => {
formData.value.reminder = value;
};
// 取消
const handleCancel = () => {
uni.navigateBack();
};
// 保存
const handleSave = () => {
if (!formData.value.title.trim()) {
uni.showToast({
title: '请输入标题',
icon: 'none'
});
return;
}
// 准备返回的数据
const eventData = {
title: formData.value.title,
date: formData.value.startDate,
startHour: formData.value.startHour,
startMin: formData.value.startMin,
endHour: formData.value.endHour,
endMin: formData.value.endMin,
color: formData.value.color,
allDay: formData.value.allDay,
repeat: formData.value.repeat,
description: formData.value.description,
reminder: formData.value.reminder
};
// 使用全局存储传递数据,在首页的 onShow 中读取
uni.setStorageSync('newEventData', eventData);
// 返回上一页
uni.navigateBack();
};
// 页面加载时接收参数
onLoad((options) => {
const dateStr = options?.date || '';
initDates(dateStr);
});
</script>
<style lang="scss" scoped>
.add-event-page {
min-height: 100vh;
background: #fff;
}
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: #fff;
border-bottom: 1px solid #e5e5e5;
padding-top: var(--status-bar-height, 0);
}
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.nav-btn {
font-size: 16px;
color: #333;
padding: 8px;
}
.nav-btn-primary {
color: #2885ff;
}
.nav-title {
font-size: 17px;
font-weight: 600;
color: #333;
}
.content-scroll {
margin-top: calc(var(--status-bar-height, 0) + 45px);
height: calc(100vh - var(--status-bar-height, 0) - 45px);
}
.form-item {
padding: 16px;
border-bottom: 1px solid #f5f5f5;
display: flex;
align-items: center;
}
.title-input {
width: 100%;
font-size: 18px;
padding: 8px 0;
}
.time-section {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 16px;
}
.time-item {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.time-icon {
font-size: 20px;
}
.time-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.time-value {
font-size: 24px;
font-weight: 600;
color: #333;
}
.time-date {
font-size: 12px;
color: #999;
}
.time-separator {
margin: 0 16px;
font-size: 20px;
color: #ccc;
}
.arrow {
font-size: 20px;
color: #ccc;
margin-left: auto;
}
.switch-item {
justify-content: space-between;
}
.label {
font-size: 16px;
color: #333;
}
.clickable-item {
cursor: pointer;
}
.desc-icon {
font-size: 20px;
margin-right: 12px;
}
.desc-content {
flex: 1;
}
.desc-text {
font-size: 16px;
color: #333;
}
.desc-placeholder {
font-size: 16px;
color: #999;
}
.color-dot {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 12px;
}
.reminder-icon {
font-size: 20px;
margin-right: 12px;
}
.reminder-text {
flex: 1;
font-size: 16px;
color: #333;
}
/* 弹窗样式 */
.modal-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-content {
background: #fff;
border-radius: 12px;
padding: 24px;
width: 80%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.modal-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
text-align: center;
}
.desc-textarea {
width: 100%;
min-height: 120px;
padding: 12px;
border: 1px solid #e5e5e5;
border-radius: 8px;
font-size: 16px;
}
.color-options {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
margin-bottom: 20px;
}
.color-option {
width: 50px;
height: 50px;
border-radius: 50%;
border: 3px solid transparent;
cursor: pointer;
transition: all 0.3s;
}
.color-option.active {
border-color: #2885ff;
transform: scale(1.1);
}
.repeat-options,
.reminder-options {
margin-bottom: 20px;
}
.repeat-option,
.reminder-option {
padding: 16px;
border-bottom: 1px solid #f5f5f5;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.repeat-option:last-child,
.reminder-option:last-child {
border-bottom: none;
}
.repeat-option.active,
.reminder-option.active {
color: #2885ff;
}
.check {
color: #2885ff;
font-size: 18px;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 20px;
}
.modal-btn {
padding: 10px 24px;
border: none;
background: #f5f5f5;
border-radius: 8px;
font-size: 16px;
color: #333;
}
.modal-btn.primary {
background: #2885ff;
color: #fff;
}
.datetime-modal {
max-width: 90%;
}
.datetime-content {
max-height: 60vh;
overflow-y: auto;
}
.date-section {
margin-bottom: 20px;
}
.section-label {
display: block;
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.time-section-picker {
margin-top: 20px;
}
.picker-display {
padding: 12px;
border: 1px solid #e5e5e5;
border-radius: 8px;
text-align: center;
font-size: 16px;
color: #333;
}
</style>