OfficeSystem/pages/task/apply-delay/index.vue

667 lines
14 KiB
Vue
Raw Normal View History

2025-11-05 16:05:34 +08:00
<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>
2025-11-13 16:33:00 +08:00
import { ref, computed } from 'vue';
2025-11-05 16:05:34 +08:00
import { onLoad } from '@dcloudio/uni-app';
2025-11-13 16:33:00 +08:00
import { applyTaskDelay } from '@/api';
2025-11-05 16:05:34 +08:00
// 表单数据
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
});
}
// 填充后面月份的日期确保有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;
});
2025-11-13 16:33:00 +08:00
// 组合延期时间YYYY-MM-DD HH:mm:ss
const buildExpireDateTime = () => {
if (!formData.value.selectedDate) return '';
const rawTime = formData.value.selectedTime || '00:00';
const hasSeconds = /^\d{2}:\d{2}:\d{2}$/.test(rawTime);
const timeSegment = hasSeconds ? rawTime : `${rawTime}:00`;
return `${formData.value.selectedDate} ${timeSegment}`;
};
2025-11-05 16:05:34 +08:00
// 打开时间选择器
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();
}
}
});
};
// 提交申请
2025-11-13 16:33:00 +08:00
const handleSubmit = async () => {
2025-11-05 16:05:34 +08:00
if (!canSubmit.value) {
uni.showToast({
title: '请填写申请说明',
icon: 'none'
});
return;
}
2025-11-13 16:33:00 +08:00
if (!taskId.value) {
uni.showToast({
title: '缺少任务ID',
icon: 'none'
});
return;
}
const expireDateTime = buildExpireDateTime();
const payload = {
bstId: taskId.value,
bstType: 'UPDATE_TASK',
createRemark: formData.value.description.trim(),
data: JSON.stringify({
expireTime: expireDateTime
})
};
2025-11-05 16:05:34 +08:00
uni.showLoading({
title: '提交中...'
});
2025-11-13 16:33:00 +08:00
try {
await applyTaskDelay(payload);
2025-11-05 16:05:34 +08:00
// 将延期申请数据存储到本地,供任务详情页使用
uni.setStorageSync('delayApplication', {
taskId: taskId.value,
2025-11-13 16:33:00 +08:00
description: payload.createRemark,
delayDate: formData.value.selectedDate,
delayTime: formData.value.selectedTime,
expireTime: expireDateTime
2025-11-05 16:05:34 +08:00
});
2025-11-13 16:33:00 +08:00
uni.hideLoading();
2025-11-05 16:05:34 +08:00
uni.showToast({
title: '申请提交成功',
icon: 'success'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
2025-11-13 16:33:00 +08:00
} catch (error) {
uni.hideLoading();
console.error('申请延期失败:', error);
const message = error?.data?.message || error?.errMsg || '申请失败,请稍后再试';
uni.showToast({
title: message,
icon: 'none'
});
}
2025-11-05 16:05:34 +08:00
};
</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>