逾期申请

This commit is contained in:
WindowBird 2025-11-05 16:05:34 +08:00
parent 3d0ea8cc69
commit 2913d96097
3 changed files with 661 additions and 13 deletions

View File

@ -61,6 +61,13 @@
"navigationBarTitleText": "提交详情"
}
},
{
"path": "pages/apply-delay/index",
"style": {
"navigationBarTitleText": "申请详情",
"navigationStyle": "custom"
}
}
],

652
pages/apply-delay/index.vue Normal file
View File

@ -0,0 +1,652 @@
<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-60
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
});
}
// 642
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>

View File

@ -326,19 +326,8 @@ const submitTask = () => {
//
const applyDelay = () => {
uni.showModal({
title: '申请延期',
content: '确定要申请延期吗?',
success: (res) => {
if (res.confirm) {
console.log("申请延期", task.value.id);
uni.showToast({
title: '延期申请已提交',
icon: 'success'
});
// API
}
}
uni.navigateTo({
url: `/pages/apply-delay/index?taskId=${task.value.id || ''}`
});
};