hookehuyr

refactor(页面布局): 重构打卡页面布局为固定日历和可滚动内容区域

将日历组件改为固定定位,内容区域改为可滚动布局
动态计算日历高度并设置内容区域margin-top
移除日历背景样式以保持整体设计一致
<!--
* @Date: 2025-01-25 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-24 22:41:07
* @LastEditTime: 2025-09-25 09:37:57
* @FilePath: /mlaj/src/components/ui/CollapsibleCalendar.vue
* @Description: 可折叠日历组件
-->
<template>
<div class="collapsible-calendar">
<div class="collapsible-calendar ">
<!-- 折叠状态显示 -->
<div v-if="!isExpanded" class="calendar-collapsed" @click="expandCalendar">
<div class="calendar-header">
......@@ -165,8 +165,8 @@ defineExpose({
}
.calendar-collapsed {
background: linear-gradient(135deg, #ffffff 0%, #f8fffe 100%);
border-radius: 16px;
// background: linear-gradient(135deg, #ffffff 0%, #f8fffe 100%);
// border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 20px rgba(76, 175, 80, 0.08), 0 2px 8px rgba(0, 0, 0, 0.06);
cursor: pointer;
......
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-24 21:56:56
* @LastEditTime: 2025-09-25 09:30:55
* @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
* @Description: 文件描述
-->
<template>
<AppLayout :hasTitle="false">
<van-config-provider :theme-vars="themeVars">
<CollapsibleCalendar
ref="myRefCalendar"
:title="taskDetail.title"
:formatter="formatter"
v-model="selectedDate"
@select="onSelectDay"
@click-subtitle="onClickSubtitle"
/>
<div v-if="showProgress" class="text-wrapper">
<div class="text-header">目标进度</div>
<div style="background-color: #FFF; margin-top: 1rem;">
<div class="grade-percentage-main">
<van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
<van-col span="12">
<span>作业目标</span>
</van-col>
<van-col span="12" style="text-align: right;">
<span style="font-weight: bold;">{{ progress1 }}%</span>
</van-col>
</van-row>
<div style="overflow: hidden;">
<van-progress :percentage="progress1" color="#4caf50" :show-pivot="false" />
</div>
</div>
<!-- <div class="class-percentage-main">
<van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
<van-col span="12">
<span>班级目标</span>
</van-col>
<van-col span="12" style="text-align: right;">
<span style="font-weight: bold;">{{ progress2 }}%</span>
</van-col>
</van-row>
<van-progress :percentage="progress2" color="#4caf50" :show-pivot="false" />
</div> -->
<div style="padding: 0.75rem 1rem;">
<van-image round width="2.8rem" height="2.8rem" :src="item ? item : 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="contain"
v-for="(item, index) in teamAvatars.splice(0, 8)" :key="index"
:style="{ marginLeft: index > 0 ? '-0.5rem' : '', border: '2px solid #eff6ff', background: '#fff' }" />
</div>
</div>
<!-- 固定的日历组件 -->
<div class="fixed-calendar">
<CollapsibleCalendar
ref="calendarRef"
:title="taskDetail.title"
:formatter="formatter"
v-model="selectedDate"
@select="onSelectDay"
@click-subtitle="onClickSubtitle"
/>
</div>
<div class="text-wrapper">
<div class="text-header">作业描述</div>
</div>
<div v-if="!taskDetail.is_finish" class="text-wrapper">
<div class="text-header">打卡类型</div>
<div class="upload-wrapper">
<div @click="goToCheckinImagePage" class="upload-boxer">
<div><van-icon name="photo" size="2.5rem" /></div>
<div style="font-size: 0.85rem;">图文打卡</div>
</div>
<div @click="goToCheckinVideoPage()" class="upload-boxer">
<div><van-icon name="video" size="2.5rem" /></div>
<div style="font-size: 0.85rem;">视频打卡</div>
</div>
<div @click="goToCheckinAudioPage()" class="upload-boxer">
<div><van-icon name="music" size="2.5rem" /></div>
<div style="font-size: 0.85rem;">音频打卡</div>
</div>
<!-- 可滚动的内容区域 -->
<div class="scrollable-content">
<div class="text-wrapper">
<div class="text-header">作业描述</div>
</div>
</div>
<div class="text-wrapper">
<div class="text-header">打卡动态</div>
<van-list
v-if="checkinDataList.length"
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
class="py-3 space-y-4"
>
<div class="post-card" v-for="post in checkinDataList" :key="post.id">
<div class="post-header">
<van-row>
<van-col span="4">
<van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" />
<div v-if="showProgress" class="text-wrapper">
<div class="text-header">目标进度</div>
<div style="background-color: #FFF; margin-top: 1rem;">
<div class="grade-percentage-main">
<van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
<van-col span="12">
<span>作业目标</span>
</van-col>
<van-col span="17">
<div class="user-info">
<div class="username">{{ post.user.name }}</div>
<div class="post-time">{{ post.user.time }}</div>
</div>
<van-col span="12" style="text-align: right;">
<span style="font-weight: bold;">{{ progress1 }}%</span>
</van-col>
<van-col span="3">
<div v-if="post.is_my" class="post-menu">
<van-icon name="edit" @click="editCheckin(post)" />
<van-icon name="delete-o" @click="delCheckin(post)" />
</div>
</van-row>
<div style="overflow: hidden;">
<van-progress :percentage="progress1" color="#4caf50" :show-pivot="false" />
</div>
</div>
<!-- <div class="class-percentage-main">
<van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
<van-col span="12">
<span>班级目标</span>
</van-col>
<van-col span="12" style="text-align: right;">
<span style="font-weight: bold;">{{ progress2 }}%</span>
</van-col>
</van-row>
<van-progress :percentage="progress2" color="#4caf50" :show-pivot="false" />
</div> -->
<div style="padding: 0.75rem 1rem;">
<van-image round width="2.8rem" height="2.8rem" :src="item ? item : 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="contain"
v-for="(item, index) in teamAvatars.splice(0, 8)" :key="index"
:style="{ marginLeft: index > 0 ? '-0.5rem' : '', border: '2px solid #eff6ff', background: '#fff' }" />
</div>
<div class="post-content">
<div class="post-text">{{ post.content }}</div>
<div class="post-media">
<div v-if="post.images.length" class="post-images">
<van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5"
@click="openImagePreview(index, post)" />
</div>
<van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" />
<div v-for="(v, idx) in post.videoList" :key="idx">
<!-- 视频封面和播放按钮 -->
<div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;">
<img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'"
:alt="v.content" class="w-full h-full object-cover" />
<div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
@click="startPlay(v)">
<div
class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
<van-icon name="play-circle-o" class="text-white" size="40" />
</div>
</div>
<div v-if="!taskDetail.is_finish" class="text-wrapper">
<div class="text-header">打卡类型</div>
<div class="upload-wrapper">
<div @click="goToCheckinImagePage" class="upload-boxer">
<div><van-icon name="photo" size="2.5rem" /></div>
<div style="font-size: 0.85rem;">图文打卡</div>
</div>
<div @click="goToCheckinVideoPage()" class="upload-boxer">
<div><van-icon name="video" size="2.5rem" /></div>
<div style="font-size: 0.85rem;">视频打卡</div>
</div>
<div @click="goToCheckinAudioPage()" class="upload-boxer">
<div><van-icon name="music" size="2.5rem" /></div>
<div style="font-size: 0.85rem;">音频打卡</div>
</div>
</div>
</div>
<div class="text-wrapper">
<div class="text-header">打卡动态</div>
<van-list
v-if="checkinDataList.length"
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
class="py-3 space-y-4"
>
<div class="post-card" v-for="post in checkinDataList" :key="post.id">
<div class="post-header">
<van-row>
<van-col span="4">
<van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" />
</van-col>
<van-col span="17">
<div class="user-info">
<div class="username">{{ post.user.name }}</div>
<div class="post-time">{{ post.user.time }}</div>
</div>
</van-col>
<van-col span="3">
<div v-if="post.is_my" class="post-menu">
<van-icon name="edit" @click="editCheckin(post)" />
<van-icon name="delete-o" @click="delCheckin(post)" />
</div>
</van-col>
</van-row>
</div>
<div class="post-content">
<div class="post-text">{{ post.content }}</div>
<div class="post-media">
<div v-if="post.images.length" class="post-images">
<van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5"
@click="openImagePreview(index, post)" />
</div>
<van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" />
<div v-for="(v, idx) in post.videoList" :key="idx">
<!-- 视频封面和播放按钮 -->
<div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;">
<img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'"
:alt="v.content" class="w-full h-full object-cover" />
<div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
@click="startPlay(v)">
<div
class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
<van-icon name="play-circle-o" class="text-white" size="40" />
</div>
</div>
</div>
<!-- 视频播放器 -->
<VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden"
:ref="el => {
if(el) {
// 确保不重复添加
if (!videoPlayers?.includes(el)) {
videoPlayers?.push(el);
}
}
}"
@onPlay="handleVideoPlay(player, post)"
@onPause="handleVideoPause(post)" />
</div>
<!-- 视频播放器 -->
<VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden"
<AudioPlayer
v-if="post.audio.length"
:songs="post.audio"
class="post-audio"
:id="post.id"
:ref="el => {
if(el) {
// 确保不重复添加
if (!videoPlayers?.includes(el)) {
videoPlayers?.push(el);
if (!audioPlayers?.includes(el)) {
audioPlayers?.push(el);
}
}
}"
@onPlay="handleVideoPlay(player, post)"
@onPause="handleVideoPause(post)" />
@play="(player) => handleAudioPlay(player, post)"
/>
</div>
<AudioPlayer
v-if="post.audio.length"
:songs="post.audio"
class="post-audio"
:id="post.id"
:ref="el => {
if(el) {
// 确保不重复添加
if (!audioPlayers?.includes(el)) {
audioPlayers?.push(el);
}
}
}"
@play="(player) => handleAudioPlay(player, post)"
/>
</div>
<div class="post-footer">
<van-icon @click="handLike(post)"name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" />
<span class="like-count">{{ post.likes }}</span>
</div>
</div>
<div class="post-footer">
<van-icon @click="handLike(post)"name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" />
<span class="like-count">{{ post.likes }}</span>
</div>
</div>
</van-list>
<van-empty v-else description="暂无数据" />
<van-back-top right="5vw" bottom="10vh" />
</div>
</van-list>
<van-empty v-else description="暂无数据" />
<van-back-top right="5vw" bottom="10vh" />
</div>
<div style="height: 5rem;"></div>
<div style="height: 5rem;"></div>
</div> <!-- 闭合 scrollable-content -->
</van-config-provider>
<van-dialog v-model:show="dialog_show" title="标题" show-cancel-button></van-dialog>
......@@ -196,52 +202,29 @@ const myRefCalendar = ref(null);
const windowHeight = ref(window.innerHeight);
const windowWidth = ref(window.innerWidth);
// 日历高度相关的响应式数据
const calendarRef = ref(null);
const calendarHeight = ref(200); // 默认高度
/**
* 动态计算日历高度
* 根据屏幕尺寸和设备类型自适应调整
* 动态获取日历组件的实际高度
*/
const calendarHeight = computed(() => {
// 获取可视窗口高度
const viewportHeight = windowHeight.value;
// 预留给其他内容的空间(头部、底部等)
const reservedSpace = 200; // 可根据实际需要调整
// 计算可用高度
const availableHeight = viewportHeight - reservedSpace;
// 设置最小和最大高度限制
const minHeight = 300; // 最小高度
const maxHeight = 500; // 最大高度
// 根据屏幕宽度调整高度比例
let heightRatio = 0.6; // 默认占可用高度的55%
if (windowWidth.value < 375) {
// 小屏手机
heightRatio = 0.95;
} else if (windowWidth.value < 414) {
// 中等屏幕手机
heightRatio = 0.95;
} else if (windowWidth.value >= 768) {
// 平板或更大屏幕
heightRatio = 0.95;
}
const calculatedHeight = Math.floor(availableHeight * heightRatio);
// 确保高度在合理范围内
const finalHeight = Math.max(minHeight, Math.min(maxHeight, calculatedHeight));
return `${finalHeight}px`;
});
const updateCalendarHeight = async () => {
await nextTick();
if (calendarRef.value) {
const rect = calendarRef.value.$el.getBoundingClientRect();
calendarHeight.value = rect.height;
}
};
/**
* 监听窗口尺寸变化
*/
const handleResize = () => {
windowHeight.value = window.innerHeight;
windowWidth.value = window.innerWidth;
windowHeight.value = window.innerHeight;
windowWidth.value = window.innerWidth;
// 重新计算日历高度
updateCalendarHeight();
};
// 组件挂载时添加事件监听
......@@ -252,6 +235,9 @@ onMounted(() => {
// 延迟更新,等待方向变化完成
setTimeout(handleResize, 100);
});
// 初始化时计算日历高度
updateCalendarHeight();
});
// 存储所有视频播放器的引用
......@@ -731,6 +717,25 @@ const formatData = (data) => {
</script>
<style lang="less">
// 固定日历样式
.fixed-calendar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff); // 与AppLayout保持一致的渐变背景
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
// 可滚动内容区域样式
.scrollable-content {
margin-top: v-bind('calendarHeight + "px"'); // 动态计算日历高度
padding-top: 1rem;
height: v-bind('"calc(100vh - " + calendarHeight + "px)"'); // 动态计算剩余高度
overflow-y: auto;
}
.van-back-top {
background-color: #4caf50;
}
......