Files
DJKB/my-vue-app/src/views/secondTop/components/Calendar.vue
chenpanliang 14a536bd1c feat(Calendar): 添加休息天数输入并改进营期设置逻辑
- 在营期设置弹窗中添加休息天数输入字段
- 修改营期结束判断逻辑,不再仅依赖休息日
- 改进用户参数获取逻辑,优先使用路由参数
- 添加测试数据以便在没有营期数据时测试功能
- 优化API请求参数处理,确保总是传递必要参数
2025-08-27 18:13:46 +08:00

1238 lines
31 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>
<div class="calendar-container">
<!-- 头部导航 -->
<div class="calendar-header">
<button @click="previousMonth" class="nav-btn">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
</svg>
</button>
<h2 class="month-year">{{ currentYear }}{{ currentMonth + 1 }}</h2>
<div class="header-actions">
<button v-if="!shouldShowFinishCamp()" @click="showCampModal = true" class="camp-btn">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
设置营期
</button>
<button v-if="shouldShowFinishCamp()" @click="finishCamp" class="camp-btn finish-camp">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
结束营期
</button>
<button @click="nextMonth" class="nav-btn">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</svg>
</button>
</div>
</div>
<!-- 星期标题 -->
<div class="weekdays">
<div v-for="day in weekdays" :key="day" class="weekday">
{{ day }}
</div>
</div>
<!-- 日期网格 -->
<div class="calendar-grid">
<div
v-for="date in calendarDates"
:key="date.key"
:class="[
'date-cell',
{
'other-month': !date.isCurrentMonth,
'today': date.isToday,
'selected': date.isSelected,
'has-event': date.hasEvent
}
]"
@click="selectDate(date, $event)"
>
<span class="date-number">{{ date.day }}</span>
<div v-if="date.hasEvent && !isRestDay(date.dateStr)" class="event-dot" :class="getEventTypeClass(date.dateStr)"></div>
<span v-if="isRestDay(date.dateStr)" class="rest-text"></span>
</div>
</div>
<!-- 悬浮日期信息 -->
<div
v-if="showTooltip && selectedDate"
class="date-tooltip"
:style="{ left: tooltipPosition.x + 'px', top: tooltipPosition.y + 'px' }"
>
<h3>{{ formatSelectedDate }}</h3>
<div v-if="selectedDateEvents.length > 0" class="events-list">
<div v-for="event in selectedDateEvents" :key="event.id" class="event-item">
<span class="event-title">{{ event.title }}</span>
<span class="event-desc">{{ event.description }}</span>
</div>
</div>
<div v-else class="no-events">
暂无日程安排
</div>
</div>
<!-- 设置营期弹窗 -->
<div v-if="showCampModal" class="modal-overlay" @click="showCampModal = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>设置营期</h3>
<button @click="showCampModal = false" class="close-btn">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="campStartDate">营期开始时间</label>
<input
type="date"
id="campStartDate"
v-model="campStartDate"
class="form-input"
/>
</div>
<div class="form-group">
<label for="campDays">接数据天数</label>
<input
type="number"
id="campDays"
v-model="campDays"
min="1"
max="365"
class="form-input"
placeholder="请输入天数"
/>
</div>
<div class="form-group">
<label for="restDays">休息天数</label>
<input
type="number"
id="restDays"
v-model="restDays"
min="1"
max="365"
class="form-input"
placeholder="请输入休息天数"
/>
</div>
</div>
<div class="modal-footer">
<button @click="showCampModal = false" class="cancel-btn">取消</button>
<button @click="saveCampSettings" class="save-btn">保存</button>
</div>
</div>
</div>
<!-- 结束营期确认弹窗 -->
<div v-if="showFinishConfirmModal" class="modal-overlay">
<div class="modal-content confirm-modal" @click.stop>
<div class="modal-header">
<h3>确认结束营期</h3>
</div>
<div class="modal-body">
<p class="confirm-text">确定要结束当前营期吗结束后需要立即设置下一营期</p>
</div>
<div class="modal-footer">
<button @click="cancelFinishCamp" class="cancel-btn">取消</button>
<button @click="confirmFinishCamp" class="confirm-btn">确认结束</button>
</div>
</div>
</div>
<!-- 强制设置下一营期弹窗 -->
<div v-if="showNextCampModal" class="modal-overlay">
<div class="modal-content force-modal" @click.stop>
<div class="modal-header">
<h3>设置下一营期</h3>
<span class="required-text">*必须完成设置才能结束当前营期</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="nextCampStartDate">下一营期开始时间<span class="required">*</span></label>
<input
type="date"
id="nextCampStartDate"
v-model="nextCampStartDate"
class="form-input"
required
/>
</div>
<div class="form-group">
<label for="nextCampDataDays">接数据时间天数<span class="required">*</span></label>
<input
type="number"
id="nextCampDataDays"
v-model="nextCampDataDays"
min="1"
max="365"
class="form-input"
placeholder="请输入接数据天数"
required
/>
</div>
<div class="form-group">
<label for="nextCampRestDays">休息时间天数<span class="required">*</span></label>
<input
type="number"
id="nextCampRestDays"
v-model="nextCampRestDays"
min="1"
max="365"
class="form-input"
placeholder="请输入休息天数"
required
/>
</div>
</div>
<div class="modal-footer">
<button @click="forceNextCampSetting" class="cancel-btn disabled">不能取消</button>
<button @click="saveNextCampSettings" class="save-btn">完成设置</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import {getCampPeriodAdmin} from '@/api/secondTop.js'
// 响应式数据
const currentYear = ref(new Date().getFullYear());
const currentMonth = ref(new Date().getMonth());
const selectedDate = ref(null);
const today = new Date();
const showTooltip = ref(false);
const tooltipPosition = ref({ x: 0, y: 0 });
// 营期设置相关
const showCampModal = ref(false);
const campStartDate = ref('');
const campDays = ref();
const restDays = ref();
const isCampFinished = ref(false);
// 结束营期确认弹框相关
const showFinishConfirmModal = ref(false);
const showNextCampModal = ref(false);
const nextCampStartDate = ref('');
const nextCampDataDays = ref();
const nextCampRestDays = ref();
// 获取本地时区的今日日期字符串
const getTodayString = () => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const todayString = getTodayString();
// 星期标题
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
// 示例事件数据
const events = ref([]);
// 计算属性:当前月份的日期数组
const calendarDates = computed(() => {
const dates = [];
const firstDay = new Date(currentYear.value, currentMonth.value, 1);
const lastDay = new Date(currentYear.value, currentMonth.value + 1, 0);
const startDate = new Date(firstDay);
// 调整到周日开始
startDate.setDate(startDate.getDate() - startDate.getDay());
// 生成6周的日期
for (let i = 0; i < 42; i++) {
const date = new Date(startDate);
date.setDate(startDate.getDate() + i);
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
const isCurrentMonth = date.getMonth() === currentMonth.value;
const isToday = dateStr === todayString;
const hasEvent = events.value.some(event => event.date === dateStr);
dates.push({
key: `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`,
date: date,
day: date.getDate(),
dateStr: dateStr,
isCurrentMonth: isCurrentMonth,
isToday: isToday,
isSelected: selectedDate.value && selectedDate.value.dateStr === dateStr,
hasEvent: hasEvent
});
}
return dates;
});
// 计算属性:格式化选中日期
const formatSelectedDate = computed(() => {
if (!selectedDate.value) return '';
const date = selectedDate.value.date;
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}`;
});
// 计算属性:选中日期的事件
const selectedDateEvents = computed(() => {
if (!selectedDate.value) return [];
return events.value.filter(event => event.date === selectedDate.value.dateStr);
});
// 方法:上一月
const previousMonth = () => {
if (currentMonth.value === 0) {
currentMonth.value = 11;
currentYear.value--;
} else {
currentMonth.value--;
}
};
// 方法:下一月
const nextMonth = () => {
if (currentMonth.value === 11) {
currentMonth.value = 0;
currentYear.value++;
} else {
currentMonth.value++;
}
};
// 方法:选择日期
const selectDate = (date, event) => {
selectedDate.value = date;
if (event) {
tooltipPosition.value = {
x: event.clientX + 10,
y: event.clientY - 10
};
showTooltip.value = true;
// 3秒后自动隐藏
setTimeout(() => {
showTooltip.value = false;
}, 3000);
}
};
// 方法:隐藏悬浮框
const hideTooltip = () => {
showTooltip.value = false;
};
// 方法:保存营期设置
const saveCampSettings = async () => {
if (!campStartDate.value) {
alert('请选择营期开始时间');
return;
}
if (!campDays.value || campDays.value < 1) {
alert('请输入有效的接数据天数');
return;
}
if (!restDays.value || restDays.value < 1) {
alert('请输入有效的休息天数');
return;
}
try {
// 调用API设置营期参数并获取营期安排
const result = await CenterCampPeriodAdmin({
receipt_data_start_time: campStartDate.value,
receipt_data_time: campDays.value.toString(),
rest_days: restDays.value.toString()
});
if (result && result.data) {
showCampModal.value = false;
// 重置表单数据
campStartDate.value = '';
campDays.value = null;
restDays.value = null;
alert('营期设置成功!');
}
} catch (error) {
console.error('保存营期设置失败:', error);
alert('保存营期设置失败,请重试');
}
};
// 方法:生成营期日程安排
const generateCampSchedule = () => {
const startDate = new Date(campStartDate.value);
const dataDays = campDays.value || 2; // 接数据天数默认2天
let currentDate = new Date(startDate);
let eventId = events.value.length + 1;
// 清除之前的营期相关事件
events.value = events.value.filter(event => !event.isCampEvent);
// 添加接数据日程
for (let i = 0; i < dataDays; i++) {
const dateStr = formatDateToString(currentDate);
events.value.push({
id: eventId++,
date: dateStr,
title: `接数据 第${i + 1}`,
description: '营期数据接收阶段',
isCampEvent: true,
type: 'data'
});
currentDate.setDate(currentDate.getDate() + 1);
}
// 添加课程日程
const courses = ['课1', '课2', '课3', '课4'];
courses.forEach((course, index) => {
const dateStr = formatDateToString(currentDate);
events.value.push({
id: eventId++,
date: dateStr,
title: course,
description: `${course}`,
isCampEvent: true,
type: 'course'
});
currentDate.setDate(currentDate.getDate() + 1);
});
// 添加休息日程
const restDateStr = formatDateToString(currentDate);
events.value.push({
id: eventId++,
date: restDateStr,
title: '休息',
description: '营期休息日',
isCampEvent: true,
type: 'rest'
});
};
// 辅助方法:格式化日期为字符串
const formatDateToString = (date) => {
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}`;
};
// 方法:获取事件类型样式类
const getEventTypeClass = (dateStr) => {
const dayEvents = events.value.filter(event => event.date === dateStr && event.isCampEvent);
if (dayEvents.length === 0) return '';
const eventType = dayEvents[0].type;
switch (eventType) {
case 'data':
return 'event-data';
case 'course':
return 'event-course';
case 'rest':
return 'event-rest';
default:
return '';
}
};
// 方法:判断是否为休息日
const isRestDay = (dateStr) => {
const dayEvents = events.value.filter(event => event.date === dateStr && event.isCampEvent && event.type === 'rest');
return dayEvents.length > 0;
};
// 方法:检查营期是否应该结束
const shouldShowFinishCamp = () => {
if (isCampFinished.value) return false;
// 检查是否有营期相关的事件(接数据、课程、休息日等)
const campEvents = events.value.filter(event => event.isCampEvent);
// 如果有营期事件,就显示结束营期按钮
return campEvents.length > 0;
};
// 方法:结束营期
const finishCamp = async () => {
// 显示确认弹框
showFinishConfirmModal.value = true;
};
// 确认结束营期并进入下一营期设置
const confirmFinishCamp = () => {
showFinishConfirmModal.value = false;
showNextCampModal.value = true;
};
// 取消结束营期
const cancelFinishCamp = () => {
showFinishConfirmModal.value = false;
};
// 保存下一营期设置并结束当前营期
const saveNextCampSettings = async () => {
if (!nextCampStartDate.value || !nextCampDataDays.value || !nextCampRestDays.value) {
alert('请填写完整的下一营期信息');
return;
}
try {
// 先结束当前营期
isCampFinished.value = true;
await CenterCampPeriodAdmin({
is_camp_finish: "11"
});
// 设置下一营期
await CenterCampPeriodAdmin({
receipt_data_start_time: nextCampStartDate.value,
receipt_data_time: nextCampDataDays.value.toString(),
rest_days: nextCampRestDays.value.toString()
});
// 关闭弹框并重置数据
showNextCampModal.value = false;
nextCampStartDate.value = '';
nextCampDataDays.value = null;
nextCampRestDays.value = null;
alert('营期设置成功!');
} catch (error) {
console.error('营期设置失败:', error);
isCampFinished.value = false;
alert('营期设置失败,请重试');
}
};
// 强制要求设置下一营期(不允许取消)
const forceNextCampSetting = () => {
alert('必须设置下一营期才能结束当前营期');
};
// 路由实例
const router = useRouter();
// 用户store实例
const userStore = useUserStore();
// 获取通用请求参数的函数
const getRequestParams = () => {
const params = {}
// 优先从路由参数获取
const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
if (routeUserLevel && routeUserName) {
params.user_level = routeUserLevel.toString()
params.user_name = routeUserName
} else if (userStore.userInfo && userStore.userInfo.username) {
// 如果路由没有参数从用户store获取
params.user_name = userStore.userInfo.username
if (userStore.userInfo.user_level) {
params.user_level = userStore.userInfo.user_level.toString()
}
}
return params
}
// 判断是否为路由导航(有路由参数)
const isRouteNavigation = computed(() => {
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
return !!routeUserName
})
// 获取和设置当前营期阶段 getCampPeriodAdmin
async function CenterCampPeriodAdmin(data = {}) {
const params = getRequestParams()
const hasParams = params.user_name
// 根据传入的参数决定传递哪些数据
let Finsh = {}
if (data.is_camp_finish !== undefined) {
// 结束营期时只传递is_camp_finish参数
Finsh.is_camp_finish = data.is_camp_finish
} else if (data.receipt_data_start_time && data.receipt_data_time) {
// 设置营期时,传递开始时间和天数参数
Finsh.receipt_data_start_time = data.receipt_data_start_time
Finsh.receipt_data_time = data.receipt_data_time
if (data.rest_days) {
Finsh.rest_days = data.rest_days
}
} else {
// 兼容原有逻辑,使用全局变量
if (isCampFinished.value) {
Finsh.is_camp_finish = isCampFinished.value
} else if (campStartDate.value && campDays.value) {
// 只有在有营期设置数据时才传递参数
Finsh.receipt_data_start_time = campStartDate.value
Finsh.receipt_data_time = campDays.value.toString()
if (restDays.value) {
Finsh.rest_days = restDays.value.toString()
}
}
// 如果没有营期设置数据Finsh 保持为空对象,用于获取现有数据
}
console.log('Finsh', Finsh)
console.log('params', params)
// 确保总是传递用户参数,即使是获取数据时
const finalParams = hasParams ? {...params, ...Finsh} : (Object.keys(Finsh).length > 0 ? Finsh : {})
const res = await getCampPeriodAdmin(finalParams)
// 如果获取到营期数据,映射到日历中
if (res && res.data && res.data.camp_period) {
mapCampPeriodToCalendar(res.data.camp_period);
}
return res
}
// 方法:将营期时间安排映射到日历
const mapCampPeriodToCalendar = (campPeriod) => {
// 清除之前的营期相关事件
events.value = events.value.filter(event => !event.isCampEvent);
let eventId = events.value.length + 1;
// 解析接数据时间
if (campPeriod.receipt_data_time) {
const dataPeriod = parseDateRange(campPeriod.receipt_data_time);
for (let date = new Date(dataPeriod.start); date <= dataPeriod.end; date.setDate(date.getDate() + 1)) {
const dateStr = formatDateToString(new Date(date));
events.value.push({
id: eventId++,
date: dateStr,
title: '接数据',
description: '营期数据接收阶段',
isCampEvent: true,
type: 'data'
});
}
}
// 解析课程时间
const courses = [
{ key: 'class_one', title: '课1' },
{ key: 'class_two', title: '课2' },
{ key: 'class_three', title: '课3' },
{ key: 'class_four', title: '课4' }
];
let lastCourseEndDate = null;
courses.forEach(course => {
if (campPeriod[course.key]) {
const coursePeriod = parseDateRange(campPeriod[course.key]);
const dateStr = formatDateToString(coursePeriod.start);
events.value.push({
id: eventId++,
date: dateStr,
title: course.title,
description: `营期${course.title}阶段`,
isCampEvent: true,
type: 'course'
});
// 记录最后一个课程的结束日期
if (course.key === 'class_four') {
lastCourseEndDate = coursePeriod.end;
}
}
});
// 在课4之后添加休息日
if (lastCourseEndDate) {
const restDate = new Date(lastCourseEndDate);
restDate.setDate(restDate.getDate() + 1);
const restDateStr = formatDateToString(restDate);
events.value.push({
id: eventId++,
date: restDateStr,
title: '休息',
description: '营期休息日',
isCampEvent: true,
type: 'rest'
});
}
};
// 辅助方法:解析日期范围字符串
const parseDateRange = (dateRangeStr) => {
// 解析格式: "2025-08-15 00:00:00 to 2025-08-16 23:59:59"
const [startStr, endStr] = dateRangeStr.split(' to ');
return {
start: new Date(startStr.split(' ')[0]),
end: new Date(endStr.split(' ')[0])
};
};
// 组件挂载时选中今天并获取营期数据
onMounted(async () => {
console.log('Calendar组件挂载开始初始化...');
const todayDate = calendarDates.value.find(date => date.isToday);
if (todayDate) {
selectedDate.value = todayDate;
}
// 获取现有的营期数据
try {
console.log('开始获取营期数据...');
const result = await CenterCampPeriodAdmin();
console.log('获取营期数据结果:', result);
// 如果没有获取到营期数据,添加一些测试数据以便测试结束营期功能
if (!result || !result.data || !result.data.camp_period) {
console.log('没有获取到营期数据,添加测试数据...');
// 添加一些测试营期事件
events.value.push({
id: 999,
date: formatDateToString(new Date()),
title: '测试营期',
description: '测试营期数据',
isCampEvent: true,
type: 'data'
});
console.log('已添加测试营期数据events:', events.value);
}
} catch (error) {
console.error('获取营期数据失败:', error);
// 即使API失败也添加测试数据
console.log('API失败添加测试数据...');
events.value.push({
id: 999,
date: formatDateToString(new Date()),
title: '测试营期',
description: '测试营期数据',
isCampEvent: true,
type: 'data'
});
}
});
</script>
<style scoped>
.calendar-container {
max-width: 100%;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 24px;
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
color: white;
}
.month-year {
margin: 0;
font-size: 18px;
font-weight: 600;
letter-spacing: 0.5px;
}
.nav-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: 8px;
background: rgba(255, 255, 255, 0.2);
color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.weekday {
padding: 12px 8px;
text-align: center;
font-weight: 600;
font-size: 14px;
color: #6c757d;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0;
background: transparent;
}
.date-cell {
position: relative;
min-height: 44px;
background: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
border: 2px solid transparent;
}
.date-cell:hover {
background: #f8f9fa;
}
.date-cell.other-month {
color: #adb5bd;
background: #f8f9fa;
}
.date-cell.today {
background: #e3f2fd;
}
.date-cell.today .date-number {
background: #2196f3;
color: white;
border-radius: 50%;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.date-cell.selected {
border-color: #667eea;
background: #f0f4ff;
}
.date-number {
font-size: 14px;
font-weight: 500;
margin-bottom: 2px;
}
.event-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #ff6b6b;
position: absolute;
bottom: 8px;
}
.date-cell.has-event .event-dot {
background: #4ecdc4;
}
.date-tooltip {
position: fixed;
z-index: 1000;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 16px;
max-width: 280px;
min-width: 200px;
border: 1px solid #e9ecef;
pointer-events: none;
}
.date-tooltip h3 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: #495057;
border-bottom: 1px solid #e9ecef;
padding-bottom: 8px;
}
.events-list {
space-y: 8px;
}
.event-item {
display: flex;
flex-direction: column;
padding: 12px;
background: white;
border-radius: 8px;
margin-bottom: 8px;
}
.event-title {
font-weight: 600;
color: #495057;
margin-bottom: 4px;
}
.event-desc {
font-size: 14px;
color: #6c757d;
}
.no-events {
color: #adb5bd;
font-style: italic;
text-align: center;
padding: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.calendar-header {
padding: 16px 20px;
}
.month-year {
font-size: 16px;
}
.nav-btn {
width: 32px;
height: 32px;
}
.date-cell {
min-height: 50px;
}
.weekday {
padding: 10px 6px;
font-size: 12px;
}
.date-tooltip {
max-width: 250px;
min-width: 180px;
padding: 12px;
}
.date-tooltip h3 {
font-size: 13px;
}
}
/* 头部操作区域 */
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
/* 设置营期按钮 */
.camp-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: linear-gradient(135deg, #28a745, #20c997);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.camp-btn:hover {
background: linear-gradient(135deg, #218838, #1ea085);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3);
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 480px;
max-height: 90vh;
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid #e9ecef;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #495057;
}
.close-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
color: #6c757d;
transition: all 0.2s ease;
}
.close-btn:hover {
background: #f8f9fa;
color: #495057;
}
.modal-body {
padding: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #495057;
font-size: 14px;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
transition: all 0.2s ease;
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: #4a90e2;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid #e9ecef;
background: #f8f9fa;
}
.cancel-btn, .save-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.cancel-btn {
background: #6c757d;
color: white;
}
.cancel-btn:hover {
background: #5a6268;
}
.save-btn {
background: linear-gradient(135deg, #4a90e2, #357abd);
color: white;
}
.save-btn:hover {
background: linear-gradient(135deg, #357abd, #2968a3);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(74, 144, 226, 0.3);
}
.finish-camp {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
}
.finish-camp:hover {
background: linear-gradient(135deg, #c0392b, #a93226);
transform: translateY(-1px);
}
/* 营期事件类型样式 */
.event-dot.event-data {
background: linear-gradient(135deg, #9c27b0, #7b1fa2);
box-shadow: 0 2px 4px rgba(156, 39, 176, 0.3);
}
.event-dot.event-course {
background: linear-gradient(135deg, #fd7e14, #e55a00);
box-shadow: 0 2px 4px rgba(253, 126, 20, 0.3);
}
.event-dot.event-rest {
background: linear-gradient(135deg, #28a745, #1e7e34);
box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3);
}
/* 休息日文字样式 */
.rest-text {
position: absolute;
bottom: 2px;
right: 2px;
font-size: 10px;
font-weight: bold;
color: #28a745;
background: rgba(40, 167, 69, 0.1);
border-radius: 2px;
padding: 1px 3px;
line-height: 1;
}
/* 确认弹框样式 */
.confirm-modal {
max-width: 420px;
}
.confirm-text {
font-size: 16px;
color: #495057;
text-align: center;
margin: 0;
line-height: 1.5;
}
.confirm-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
}
.confirm-btn:hover {
background: linear-gradient(135deg, #c0392b, #a93226);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(231, 76, 60, 0.3);
}
/* 强制设置弹框样式 */
.force-modal {
max-width: 500px;
}
.force-modal .modal-header {
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
color: white;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.force-modal .modal-header h3 {
color: white;
margin: 0;
}
.required-text {
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
font-weight: 400;
background: rgba(255, 255, 255, 0.2);
padding: 4px 8px;
border-radius: 4px;
}
.required {
color: #e74c3c;
margin-left: 2px;
}
.cancel-btn.disabled {
background: #adb5bd;
color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
}
.cancel-btn.disabled:hover {
background: #adb5bd;
transform: none;
box-shadow: none;
}
/* 表单验证样式 */
.form-input:invalid {
border-color: #e74c3c;
}
.form-input:invalid:focus {
border-color: #e74c3c;
box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.1);
}
/* 强制弹框的保存按钮 */
.force-modal .save-btn {
background: linear-gradient(135deg, #28a745, #20c997);
}
.force-modal .save-btn:hover {
background: linear-gradient(135deg, #218838, #1ea085);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3);
}
</style>