653 lines
14 KiB
Vue
653 lines
14 KiB
Vue
<template>
|
||
<view class="apply-delay-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"></text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 内容区域 -->
|
||
<scroll-view class="content-scroll" scroll-y>
|
||
<view class="scroll-content">
|
||
<!-- 申请说明输入框 -->
|
||
<view class="form-item">
|
||
<view class="input-wrapper">
|
||
<text class="input-icon">📄</text>
|
||
<input
|
||
v-model="formData.description"
|
||
class="description-input"
|
||
placeholder="输入申请说明"
|
||
placeholder-style="color: #999;"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日历选择器 -->
|
||
<view class="calendar-section">
|
||
<!-- 月份切换栏 -->
|
||
<view class="month-header">
|
||
<view class="month-nav-btn" @click="prevMonth">
|
||
<text>‹</text>
|
||
</view>
|
||
<view class="month-title">{{ currentYear }}年{{ currentMonth }}月</view>
|
||
<view class="month-nav-btn" @click="nextMonth">
|
||
<text>›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 星期标题 -->
|
||
<view class="weekdays">
|
||
<view class="weekday-item" v-for="day in weekdays" :key="day">{{ day }}</view>
|
||
</view>
|
||
|
||
<!-- 日历网格 -->
|
||
<view class="calendar-grid">
|
||
<view
|
||
class="calendar-day"
|
||
v-for="(dayObj, index) in currentMonthDays"
|
||
:key="index"
|
||
:class="{
|
||
'other-month': !dayObj.isCurrentMonth,
|
||
'today': isToday(dayObj),
|
||
'selected': isSelected(dayObj)
|
||
}"
|
||
@click="selectDate(dayObj)"
|
||
>
|
||
<text class="day-number">{{ dayObj.day }}</text>
|
||
<view class="today-mark" v-if="isToday(dayObj) && !isSelected(dayObj)">
|
||
<text>今</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 具体时间选择 -->
|
||
<view class="form-item time-item" @click="openTimePicker">
|
||
<text class="time-label">具体时间</text>
|
||
<view class="time-value-wrapper">
|
||
<text class="time-value">{{ selectedTime }}</text>
|
||
<text class="arrow">›</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 时间选择器弹窗 -->
|
||
<view v-if="showTimePicker" class="modal-mask" @click="closeTimePicker">
|
||
<view class="modal-content time-modal" @click.stop>
|
||
<view class="modal-title">选择时间</view>
|
||
<picker
|
||
mode="multiSelector"
|
||
:value="timePickerIndex"
|
||
:range="timePickerRange"
|
||
@change="handleTimeChange"
|
||
>
|
||
<view class="picker-display">
|
||
{{ selectedTime }}
|
||
</view>
|
||
</picker>
|
||
<view class="modal-buttons">
|
||
<button class="modal-btn" @click="closeTimePicker">取消</button>
|
||
<button class="modal-btn primary" @click="closeTimePicker">确定</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 确认提交按钮 -->
|
||
<view class="submit-button-wrapper">
|
||
<uv-button
|
||
type="primary"
|
||
size="normal"
|
||
:disabled="!canSubmit"
|
||
@click="handleSubmit"
|
||
>
|
||
确认提交
|
||
</uv-button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue';
|
||
import { onLoad } from '@dcloudio/uni-app';
|
||
|
||
// 表单数据
|
||
const formData = ref({
|
||
description: '',
|
||
selectedDate: '',
|
||
selectedTime: '18:00'
|
||
});
|
||
|
||
// 任务ID
|
||
const taskId = ref(null);
|
||
|
||
// 当前显示的年月
|
||
const currentYear = ref(new Date().getFullYear());
|
||
const currentMonth = ref(new Date().getMonth() + 1);
|
||
|
||
// 选中的日期
|
||
const selectedDate = ref(new Date().toISOString().slice(0, 10));
|
||
|
||
// 星期标题
|
||
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
||
|
||
// 时间选择器
|
||
const showTimePicker = ref(false);
|
||
const timePickerIndex = ref([18, 0]); // [小时, 分钟]
|
||
const timePickerRange = ref([
|
||
Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0')),
|
||
Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'))
|
||
]);
|
||
|
||
// 获取月份的第一天是星期几(0-6,0是星期日)
|
||
const getFirstDayOfMonth = (year, month) => {
|
||
return new Date(year, month - 1, 1).getDay();
|
||
};
|
||
|
||
// 获取月份的天数
|
||
const getDaysInMonth = (year, month) => {
|
||
return new Date(year, month, 0).getDate();
|
||
};
|
||
|
||
// 生成月份的日期数组(包含前后月份的填充日期)
|
||
const generateMonthDays = (year, month) => {
|
||
const firstDay = getFirstDayOfMonth(year, month);
|
||
const daysInMonth = getDaysInMonth(year, month);
|
||
const days = [];
|
||
|
||
// 计算上个月的最后几天
|
||
const prevMonth = month === 1 ? 12 : month - 1;
|
||
const prevYear = month === 1 ? year - 1 : year;
|
||
const prevMonthDays = getDaysInMonth(prevYear, prevMonth);
|
||
|
||
// 填充前面月份的日期(从最后几天开始)
|
||
if (firstDay > 0) {
|
||
for (let i = firstDay - 1; i >= 0; i--) {
|
||
days.push({
|
||
day: prevMonthDays - i,
|
||
year: prevYear,
|
||
month: prevMonth,
|
||
isCurrentMonth: false
|
||
});
|
||
}
|
||
}
|
||
|
||
// 填充当前月份的日期
|
||
for (let i = 1; i <= daysInMonth; i++) {
|
||
days.push({
|
||
day: i,
|
||
year: year,
|
||
month: month,
|
||
isCurrentMonth: true
|
||
});
|
||
}
|
||
|
||
// 填充后面月份的日期(确保有6行,即42个格子)
|
||
const totalDays = days.length;
|
||
const remainingDays = 42 - totalDays;
|
||
const nextMonth = month === 12 ? 1 : month + 1;
|
||
const nextYear = month === 12 ? year + 1 : year;
|
||
|
||
for (let i = 1; i <= remainingDays; i++) {
|
||
days.push({
|
||
day: i,
|
||
year: nextYear,
|
||
month: nextMonth,
|
||
isCurrentMonth: false
|
||
});
|
||
}
|
||
|
||
return days;
|
||
};
|
||
|
||
// 当前月份的日期数组
|
||
const currentMonthDays = computed(() => {
|
||
return generateMonthDays(currentYear.value, currentMonth.value);
|
||
});
|
||
|
||
// 判断是否是今天
|
||
const isToday = (dayObj) => {
|
||
if (!dayObj || !dayObj.day) return false;
|
||
const today = new Date();
|
||
return (
|
||
dayObj.year === today.getFullYear() &&
|
||
dayObj.month === today.getMonth() + 1 &&
|
||
dayObj.day === today.getDate()
|
||
);
|
||
};
|
||
|
||
// 判断是否被选中
|
||
const isSelected = (dayObj) => {
|
||
if (!dayObj || !dayObj.day) return false;
|
||
const selected = new Date(selectedDate.value);
|
||
return (
|
||
dayObj.year === selected.getFullYear() &&
|
||
dayObj.month === selected.getMonth() + 1 &&
|
||
dayObj.day === selected.getDate()
|
||
);
|
||
};
|
||
|
||
// 格式化日期为 YYYY-MM-DD
|
||
const formatDate = (year, month, day) => {
|
||
const m = String(month).padStart(2, '0');
|
||
const d = String(day).padStart(2, '0');
|
||
return `${year}-${m}-${d}`;
|
||
};
|
||
|
||
// 选择日期
|
||
const selectDate = (dayObj) => {
|
||
if (!dayObj || !dayObj.day) return;
|
||
const dateStr = formatDate(dayObj.year, dayObj.month, dayObj.day);
|
||
selectedDate.value = dateStr;
|
||
formData.value.selectedDate = dateStr;
|
||
|
||
// 如果选择的是其他月份的日期,切换到对应月份
|
||
if (!dayObj.isCurrentMonth) {
|
||
currentYear.value = dayObj.year;
|
||
currentMonth.value = dayObj.month;
|
||
}
|
||
};
|
||
|
||
// 上一个月
|
||
const prevMonth = () => {
|
||
if (currentMonth.value === 1) {
|
||
currentMonth.value = 12;
|
||
currentYear.value -= 1;
|
||
} else {
|
||
currentMonth.value -= 1;
|
||
}
|
||
};
|
||
|
||
// 下一个月
|
||
const nextMonth = () => {
|
||
if (currentMonth.value === 12) {
|
||
currentMonth.value = 1;
|
||
currentYear.value += 1;
|
||
} else {
|
||
currentMonth.value += 1;
|
||
}
|
||
};
|
||
|
||
// 格式化时间
|
||
const formatTime = (hour, minute) => {
|
||
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||
};
|
||
|
||
// 选中的时间显示
|
||
const selectedTime = computed(() => {
|
||
return formData.value.selectedTime;
|
||
});
|
||
|
||
// 打开时间选择器
|
||
const openTimePicker = () => {
|
||
const timeParts = formData.value.selectedTime.split(':');
|
||
timePickerIndex.value = [parseInt(timeParts[0]), parseInt(timeParts[1])];
|
||
showTimePicker.value = true;
|
||
};
|
||
|
||
// 关闭时间选择器
|
||
const closeTimePicker = () => {
|
||
showTimePicker.value = false;
|
||
};
|
||
|
||
// 时间选择变化
|
||
const handleTimeChange = (e) => {
|
||
const [hourIndex, minuteIndex] = e.detail.value;
|
||
const hour = parseInt(timePickerRange.value[0][hourIndex]);
|
||
const minute = parseInt(timePickerRange.value[1][minuteIndex]);
|
||
formData.value.selectedTime = formatTime(hour, minute);
|
||
timePickerIndex.value = [hourIndex, minuteIndex];
|
||
};
|
||
|
||
// 是否可以提交
|
||
const canSubmit = computed(() => {
|
||
return formData.value.description.trim() !== '' && formData.value.selectedDate !== '';
|
||
});
|
||
|
||
// 页面加载
|
||
onLoad((options) => {
|
||
taskId.value = options.taskId || options.id;
|
||
|
||
// 初始化选中日期为今天
|
||
const today = new Date();
|
||
selectedDate.value = formatDate(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
||
formData.value.selectedDate = selectedDate.value;
|
||
});
|
||
|
||
// 取消
|
||
const handleCancel = () => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要取消申请吗?未保存的内容将丢失',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.navigateBack();
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 提交申请
|
||
const handleSubmit = () => {
|
||
if (!canSubmit.value) {
|
||
uni.showToast({
|
||
title: '请填写申请说明',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
uni.showLoading({
|
||
title: '提交中...'
|
||
});
|
||
|
||
// 准备提交数据
|
||
const submitData = {
|
||
taskId: taskId.value,
|
||
description: formData.value.description.trim(),
|
||
delayDate: formData.value.selectedDate,
|
||
delayTime: formData.value.selectedTime
|
||
};
|
||
|
||
// TODO: 调用提交接口
|
||
// 实际使用时,应该调用API接口上传数据
|
||
// uni.request({
|
||
// url: '/api/task/delay/apply',
|
||
// method: 'POST',
|
||
// data: submitData,
|
||
// success: (res) => {
|
||
// // 处理成功响应
|
||
// },
|
||
// fail: (err) => {
|
||
// // 处理错误
|
||
// }
|
||
// });
|
||
|
||
// 模拟提交请求
|
||
setTimeout(() => {
|
||
uni.hideLoading();
|
||
|
||
// 将延期申请数据存储到本地,供任务详情页使用
|
||
uni.setStorageSync('delayApplication', {
|
||
taskId: taskId.value,
|
||
description: submitData.description,
|
||
delayDate: submitData.delayDate,
|
||
delayTime: submitData.delayTime
|
||
});
|
||
|
||
uni.showToast({
|
||
title: '申请提交成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
// 延迟返回,让用户看到成功提示
|
||
setTimeout(() => {
|
||
uni.navigateBack();
|
||
}, 1500);
|
||
}, 1000);
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.apply-delay-page {
|
||
min-height: 100vh;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.custom-navbar {
|
||
background-color: #fff;
|
||
padding-top: var(--status-bar-height);
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.navbar-content {
|
||
height: 44px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.nav-btn {
|
||
font-size: 18px;
|
||
color: #333;
|
||
min-width: 40px;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-title {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.content-scroll {
|
||
flex: 1;
|
||
}
|
||
|
||
.scroll-content {
|
||
padding: 16px;
|
||
}
|
||
|
||
.form-item {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
margin-bottom: 16px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.input-icon {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.description-input {
|
||
flex: 1;
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
.calendar-section {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.month-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.month-nav-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24px;
|
||
color: #333;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.month-title {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.weekdays {
|
||
display: flex;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.weekday-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 14px;
|
||
color: #666;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.calendar-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.calendar-day {
|
||
width: calc(100% / 7);
|
||
aspect-ratio: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.calendar-day.other-month {
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.calendar-day.today {
|
||
.day-number {
|
||
color: #2885ff;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.calendar-day.selected {
|
||
background-color: #2885ff;
|
||
border-radius: 50%;
|
||
|
||
.day-number {
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.today-mark {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
.day-number {
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
.today-mark {
|
||
position: absolute;
|
||
top: 2px;
|
||
right: 2px;
|
||
font-size: 10px;
|
||
color: #2885ff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.time-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.time-label {
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
.time-value-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.time-value {
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
.arrow {
|
||
font-size: 18px;
|
||
color: #999;
|
||
}
|
||
|
||
.modal-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: flex-end;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
width: 100%;
|
||
background-color: #fff;
|
||
border-radius: 16px 16px 0 0;
|
||
padding: 20px;
|
||
}
|
||
|
||
.time-modal {
|
||
padding-bottom: 40px;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.picker-display {
|
||
padding: 20px;
|
||
text-align: center;
|
||
font-size: 18px;
|
||
color: #333;
|
||
}
|
||
|
||
.modal-buttons {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.modal-btn {
|
||
flex: 1;
|
||
height: 44px;
|
||
line-height: 44px;
|
||
text-align: center;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
background-color: #f5f5f5;
|
||
color: #333;
|
||
border: none;
|
||
}
|
||
|
||
.modal-btn.primary {
|
||
background-color: #2885ff;
|
||
color: #fff;
|
||
}
|
||
|
||
.submit-button-wrapper {
|
||
padding: 16px;
|
||
background-color: #fff;
|
||
border-top: 1px solid #e5e5e5;
|
||
}
|
||
</style>
|
||
|