hookehuyr

feat(teacher): 新增教师模块路由、表单页面和作业管理页面

添加教师模块相关功能,包括:
1. 在路由配置中添加教师模块路由
2. 创建教师作业新增表单页面
3. 实现教师作业管理日历视图页面
4. 添加相关Vant组件支持
......@@ -30,15 +30,20 @@ declare module 'vue' {
UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default']
UserAgreement: typeof import('./components/ui/UserAgreement.vue')['default']
VanActionSheet: typeof import('vant/es')['ActionSheet']
VanBackTop: typeof import('vant/es')['BackTop']
VanButton: typeof import('vant/es')['Button']
VanCalendar: typeof import('vant/es')['Calendar']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCol: typeof import('vant/es')['Col']
VanConfigProvider: typeof import('vant/es')['ConfigProvider']
VanDatePicker: typeof import('vant/es')['DatePicker']
VanDatetimePicker: typeof import('vant/es')['DatetimePicker']
VanDialog: typeof import('vant/es')['Dialog']
VanDivider: typeof import('vant/es')['Divider']
VanDropdownItem: typeof import('vant/es')['DropdownItem']
VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
VanEmpty: typeof import('vant/es')['Empty']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
......@@ -55,6 +60,7 @@ declare module 'vue' {
VanProgress: typeof import('vant/es')['Progress']
VanRate: typeof import('vant/es')['Rate']
VanRow: typeof import('vant/es')['Row']
VanSearch: typeof import('vant/es')['Search']
VanSwipe: typeof import('vant/es')['Swipe']
VanSwipeItem: typeof import('vant/es')['SwipeItem']
VanTab: typeof import('vant/es')['Tab']
......
/*
* @Date: 2025-03-20 20:36:36
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-13 11:24:11
* @LastEditTime: 2025-06-17 16:47:13
* @FilePath: /mlaj/src/router/routes.js
* @Description: 路由地址映射配置
*/
import checkinRoutes from './checkin'
import teacherRoutes from './teacher'
export const routes = [
{
......@@ -226,4 +227,5 @@ export const routes = [
}
},
...checkinRoutes,
...teacherRoutes,
]
......
/*
* @Date: 2025-06-17 16:46:50
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-18 11:34:36
* @FilePath: /mlaj/src/router/teacher.js
* @Description: 文件描述
*/
export default [
{
path: '/teacher/index',
name: 'Teacher',
component: () => import('../views/teacher/testPage.vue'),
meta: {
title: '教师',
requiresAuth: true
},
},
{
path: '/teacher/form',
name: 'TeacherForm',
component: () => import('../views/teacher/formPage.vue'),
meta: {
title: '教师新增作业',
requiresAuth: true
},
},
]
<!--
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2025-01-20 10:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-18 11:43:03
* @FilePath: /mlaj/src/views/teacher/formPage.vue
* @Description: 教师作业新增表单页面
-->
<template>
<AppLayout title="新增作业">
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
<div class="px-4 py-6">
<FrostedGlass class="rounded-xl overflow-hidden">
<div class="py-4">
<van-form @submit="handleSubmit">
<van-cell-group inset>
<!-- 作业名称 -->
<van-field
v-model="formData.homework_name"
name="homework_name"
label="作业名称"
placeholder="请输入作业名称"
required
:border="false"
:rules="[{ required: true, message: '请输入作业名称' }]"
/>
<!-- 类型 -->
<van-field
v-model="formData.type"
is-link
readonly
name="type"
label="类型"
placeholder="请选择类型"
:border="false"
@click="showTypePicker = true"
/>
<!-- 频次 -->
<van-field
v-model="formData.frequency"
is-link
readonly
name="frequency"
label="频次"
placeholder="请选择频次"
:border="false"
@click="showFrequencyPicker = true"
/>
<!-- 目标总数 -->
<van-field
v-model="formData.target_count"
type="number"
name="target_count"
label="目标总数"
:border="false"
placeholder="请输入目标数量"
/>
<!-- 开始时间 -->
<van-field
v-model="startTimeDisplay"
is-link
readonly
name="start_time"
label="开始时间"
placeholder="请选择开始时间"
:border="false"
@click="showStartTimePicker = true"
/>
<!-- 结束时间 -->
<van-field
v-model="endTimeDisplay"
is-link
readonly
name="end_time"
label="结束时间"
placeholder="请选择结束时间"
:border="false"
@click="showEndTimePicker = true"
/>
<!-- 课程 -->
<van-field
v-model="formData.course"
is-link
readonly
name="course"
label="课程"
placeholder="请选择课程"
:border="false"
@click="showCoursePicker = true"
/>
<!-- 活动 -->
<van-field
v-model="formData.activity"
is-link
readonly
name="activity"
label="活动"
placeholder="请选择活动"
:border="false"
@click="showActivityPicker = true"
/>
<!-- 年级 -->
<van-field
v-model="formData.grade"
is-link
readonly
name="grade"
label="年级"
placeholder="请选择年级"
:border="false"
@click="showGradePicker = true"
/>
<!-- 班级 -->
<van-field
v-model="formData.class_name"
is-link
readonly
name="class_name"
label="班级"
placeholder="请选择班级"
:border="false"
@click="showClassPicker = true"
/>
<!-- 小组 -->
<van-field
v-model="formData.group_name"
is-link
readonly
name="group_name"
label="小组"
placeholder="请选择小组"
:border="false"
@click="showGroupPicker = true"
/>
</van-cell-group>
<!-- 提交按钮 -->
<div style="margin: 16px;">
<van-button
native-type="submit"
type="primary"
block
round
:loading="loading"
class="bg-green-500 hover:bg-green-600 transition-colors"
>
确认并保存
</van-button>
</div>
</van-form>
</div>
</FrostedGlass>
</div>
</div>
<!-- 类型选择器 -->
<van-popup v-model:show="showTypePicker" position="bottom">
<van-picker
:columns="typeOptions"
@confirm="onTypeConfirm"
@cancel="showTypePicker = false"
/>
</van-popup>
<!-- 频次选择器 -->
<van-popup v-model:show="showFrequencyPicker" position="bottom">
<van-picker
:columns="frequencyOptions"
@confirm="onFrequencyConfirm"
@cancel="showFrequencyPicker = false"
/>
</van-popup>
<!-- 开始时间选择器 -->
<van-popup v-model:show="showStartTimePicker" position="bottom">
<van-picker-group
title="选择开始时间"
@confirm="onStartTimeConfirm"
@cancel="showStartTimePicker = false"
>
<van-date-picker v-model="startDate" :min-date="minDate" :max-date="maxDate" />
</van-picker-group>
</van-popup>
<!-- 结束时间选择器 -->
<van-popup v-model:show="showEndTimePicker" position="bottom">
<van-picker-group
title="选择结束时间"
@confirm="onEndTimeConfirm"
@cancel="showEndTimePicker = false"
>
<van-date-picker v-model="endDate" :min-date="minDate" :max-date="maxDate" />
</van-picker-group>
</van-popup>
<!-- 课程选择器 -->
<van-popup v-model:show="showCoursePicker" position="bottom">
<div class="p-4">
<van-search
v-model="courseSearchValue"
placeholder="搜索课程"
@search="searchCourse"
/>
<van-list>
<van-cell
v-for="course in filteredCourses"
:key="course.id"
:title="course.name"
is-link
:border="false"
@click="onCourseSelect(course)"
/>
</van-list>
</div>
</van-popup>
<!-- 活动选择器 -->
<van-popup v-model:show="showActivityPicker" position="bottom">
<div class="p-4">
<van-search
v-model="activitySearchValue"
placeholder="搜索活动"
@search="searchActivity"
/>
<van-list>
<van-cell
v-for="activity in filteredActivities"
:key="activity.id"
:title="activity.name"
is-link
:border="false"
@click="onActivitySelect(activity)"
/>
</van-list>
</div>
</van-popup>
<!-- 年级选择器 -->
<van-popup v-model:show="showGradePicker" position="bottom">
<div class="p-4">
<van-search
v-model="gradeSearchValue"
placeholder="搜索年级"
@search="searchGrade"
/>
<van-list>
<van-cell
v-for="grade in filteredGrades"
:key="grade.id"
:title="grade.name"
is-link
:border="false"
@click="onGradeSelect(grade)"
/>
</van-list>
</div>
</van-popup>
<!-- 班级选择器 -->
<van-popup v-model:show="showClassPicker" position="bottom">
<div class="p-4">
<van-search
v-model="classSearchValue"
placeholder="搜索班级"
@search="searchClass"
/>
<van-list>
<van-cell
v-for="classItem in filteredClasses"
:key="classItem.id"
:title="classItem.name"
is-link
:border="false"
@click="onClassSelect(classItem)"
/>
</van-list>
</div>
</van-popup>
<!-- 小组选择器 -->
<van-popup v-model:show="showGroupPicker" position="bottom">
<div class="p-4">
<van-search
v-model="groupSearchValue"
placeholder="搜索小组"
@search="searchGroup"
/>
<van-list>
<van-cell
v-for="group in filteredGroups"
:key="group.id"
:title="group.name"
is-link
:border="false"
@click="onGroupSelect(group)"
/>
</van-list>
</div>
</van-popup>
</AppLayout>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { showToast, DatePicker, Popup } from 'vant';
import AppLayout from '@/components/layout/AppLayout.vue';
import FrostedGlass from '@/components/ui/FrostedGlass.vue';
import { useTitle } from '@vueuse/core';
const $route = useRoute();
const $router = useRouter();
useTitle($route.meta.title || '新增作业');
// 表单数据
const formData = ref({
homework_name: '',
type: '',
frequency: '',
target_count: '',
start_time: new Date(),
end_time: new Date(),
course: '',
activity: '',
grade: '',
class_name: '',
group_name: ''
});
// 加载状态
const loading = ref(false);
// 弹窗显示状态
const showTypePicker = ref(false);
const showFrequencyPicker = ref(false);
const showStartTimePicker = ref(false);
const showEndTimePicker = ref(false);
const showCoursePicker = ref(false);
const showActivityPicker = ref(false);
const showGradePicker = ref(false);
const showClassPicker = ref(false);
const showGroupPicker = ref(false);
// 日期选择器相关
const startDate = ref(new Date());
const endDate = ref(new Date());
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2030, 11, 31);
// 选项数据
const typeOptions = ref([
{ text: '签到', value: 'checkin' },
{ text: '作业', value: 'homework' },
{ text: '考试', value: 'exam' },
{ text: '活动', value: 'activity' }
]);
const frequencyOptions = ref([
{ text: '一次', value: 'once' },
{ text: '每日', value: 'daily' },
{ text: '每周', value: 'weekly' },
{ text: '每月', value: 'monthly' }
]);
// 搜索值
const courseSearchValue = ref('');
const activitySearchValue = ref('');
const gradeSearchValue = ref('');
const classSearchValue = ref('');
const groupSearchValue = ref('');
// 数据列表
const courses = ref([
{ id: 1, name: '数学课程' },
{ id: 2, name: '语文课程' },
{ id: 3, name: '英语课程' },
{ id: 4, name: '物理课程' }
]);
const activities = ref([
{ id: 1, name: '春游活动' },
{ id: 2, name: '运动会' },
{ id: 3, name: '文艺汇演' },
{ id: 4, name: '科技节' }
]);
const grades = ref([
{ id: 1, name: '一年级' },
{ id: 2, name: '二年级' },
{ id: 3, name: '三年级' },
{ id: 4, name: '四年级' }
]);
const classes = ref([
{ id: 1, name: '一班' },
{ id: 2, name: '二班' },
{ id: 3, name: '三班' },
{ id: 4, name: '四班' }
]);
const groups = ref([
{ id: 1, name: '第一小组' },
{ id: 2, name: '第二小组' },
{ id: 3, name: '第三小组' },
{ id: 4, name: '第四小组' }
]);
// 计算属性 - 时间显示格式
const startTimeDisplay = computed(() => {
if (!formData.value.start_time) return '';
return formatDateTime(formData.value.start_time);
});
const endTimeDisplay = computed(() => {
if (!formData.value.end_time) return '';
return formatDateTime(formData.value.end_time);
});
// 过滤后的数据
const filteredCourses = computed(() => {
if (!courseSearchValue.value) return courses.value;
return courses.value.filter(course =>
course.name.toLowerCase().includes(courseSearchValue.value.toLowerCase())
);
});
const filteredActivities = computed(() => {
if (!activitySearchValue.value) return activities.value;
return activities.value.filter(activity =>
activity.name.toLowerCase().includes(activitySearchValue.value.toLowerCase())
);
});
const filteredGrades = computed(() => {
if (!gradeSearchValue.value) return grades.value;
return grades.value.filter(grade =>
grade.name.toLowerCase().includes(gradeSearchValue.value.toLowerCase())
);
});
const filteredClasses = computed(() => {
if (!classSearchValue.value) return classes.value;
return classes.value.filter(classItem =>
classItem.name.toLowerCase().includes(classSearchValue.value.toLowerCase())
);
});
const filteredGroups = computed(() => {
if (!groupSearchValue.value) return groups.value;
return groups.value.filter(group =>
group.name.toLowerCase().includes(groupSearchValue.value.toLowerCase())
);
});
/**
* 格式化日期时间
* @param {Date} date - 日期对象
* @returns {string} 格式化后的日期时间字符串
*/
const formatDateTime = (date) => {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
/**
* 类型选择确认
* @param {Object} option - 选中的选项
*/
const onTypeConfirm = (option) => {
formData.value.type = option.selectedOptions[0].text;
showTypePicker.value = false;
};
/**
* 频次选择确认
* @param {Object} option - 选中的选项
*/
const onFrequencyConfirm = (option) => {
formData.value.frequency = option.selectedOptions[0].text;
showFrequencyPicker.value = false;
};
/**
* 开始时间确认
*/
const onStartTimeConfirm = () => {
formData.value.start_time = startDate.value;
showStartTimePicker.value = false;
};
/**
* 结束时间确认
*/
const onEndTimeConfirm = () => {
formData.value.end_time = endDate.value;
showEndTimePicker.value = false;
};
/**
* 课程选择
* @param {Object} course - 选中的课程
*/
const onCourseSelect = (course) => {
formData.value.course = course.name;
showCoursePicker.value = false;
courseSearchValue.value = '';
};
/**
* 活动选择
* @param {Object} activity - 选中的活动
*/
const onActivitySelect = (activity) => {
formData.value.activity = activity.name;
showActivityPicker.value = false;
activitySearchValue.value = '';
};
/**
* 年级选择
* @param {Object} grade - 选中的年级
*/
const onGradeSelect = (grade) => {
formData.value.grade = grade.name;
showGradePicker.value = false;
gradeSearchValue.value = '';
};
/**
* 班级选择
* @param {Object} classItem - 选中的班级
*/
const onClassSelect = (classItem) => {
formData.value.class_name = classItem.name;
showClassPicker.value = false;
classSearchValue.value = '';
};
/**
* 小组选择
* @param {Object} group - 选中的小组
*/
const onGroupSelect = (group) => {
formData.value.group_name = group.name;
showGroupPicker.value = false;
groupSearchValue.value = '';
};
/**
* 搜索课程
* @param {string} value - 搜索值
*/
const searchCourse = (value) => {
courseSearchValue.value = value;
};
/**
* 搜索活动
* @param {string} value - 搜索值
*/
const searchActivity = (value) => {
activitySearchValue.value = value;
};
/**
* 搜索年级
* @param {string} value - 搜索值
*/
const searchGrade = (value) => {
gradeSearchValue.value = value;
};
/**
* 搜索班级
* @param {string} value - 搜索值
*/
const searchClass = (value) => {
classSearchValue.value = value;
};
/**
* 搜索小组
* @param {string} value - 搜索值
*/
const searchGroup = (value) => {
groupSearchValue.value = value;
};
/**
* 表单提交处理
* @param {Object} values - 表单值
*/
const handleSubmit = async (values) => {
try {
loading.value = true;
// 验证必填项
if (!formData.value.homework_name) {
showToast('请输入作业名称');
return;
}
// 这里可以调用API提交数据
console.log('提交的表单数据:', formData.value);
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
showToast('保存成功');
// 返回上一页或跳转到列表页
$router.back();
} catch (error) {
console.error('提交失败:', error);
showToast('保存失败,请重试');
} finally {
loading.value = false;
}
};
/**
* 组件挂载时初始化数据
*/
onMounted(() => {
// 这里可以调用API获取课程、活动、年级、班级、小组等数据
console.log('页面初始化');
});
</script>
<style scoped>
/* 自定义样式 */
:deep(.van-field__label) {
color: #333;
font-weight: 500;
}
:deep(.van-field--required .van-field__label::before) {
color: #ee0a24;
}
:deep(.van-button--primary) {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none;
}
:deep(.van-popup) {
border-radius: 16px 16px 0 0;
}
:deep(.van-picker__toolbar) {
border-radius: 16px 16px 0 0;
}
</style>
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-17 20:08:40
* @FilePath: /mlaj/src/views/teacher/testPage.vue
* @Description: 文件描述
-->
<template>
<AppLayout :hasTitle="false">
<van-config-provider :theme-vars="themeVars">
<van-dropdown-menu active-color="#4caf50">
<van-dropdown-item v-model="value1" :options="option1" />
<van-dropdown-item v-model="value2" :options="option2" />
<van-dropdown-item v-model="value3" :options="option3" />
</van-dropdown-menu>
<van-calendar ref="myRefCalendar" :title="taskDetail.title" :poppable="false" :show-confirm="false" :style="{ height: calendarHeight }"
switch-mode="year-month" color="#4caf50" :formatter="formatter" row-height="50" :show-mark="false"
@select="onSelectDay"
@click-subtitle="onClickSubtitle">
</van-calendar>
<div style="padding: 0 1rem;">
<van-row gutter="15">
<van-col span="12">
<van-button type="primary" block icon="add-square">主要按钮</van-button>
</van-col>
<van-col span="12">
<van-button type="primary" block icon="video">主要按钮</van-button>
</van-col>
</van-row>
</div>
<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" :key="index"
:style="{ marginLeft: index > 0 ? '-0.5rem' : '', border: '2px solid #eff6ff', background: '#fff' }" />
</div>
</div>
</div>
<van-tabs v-model:active="active" style="margin: 0 1rem;">
<van-tab title="标签 1"></van-tab>
<van-tab title="标签 2"></van-tab>
<van-tab title="标签 3"></van-tab>
</van-tabs>
<div v-if="active === 0" style="padding: 0 1rem; color: #4caf50;">
<van-list
v-if="checkinDataList.length"
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
class="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>
<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>
<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="暂无数据" />
</div>
<van-back-top right="5vw" bottom="10vh" />
<div style="height: 5rem;"></div>
</van-config-provider>
<van-dialog v-model:show="dialog_show" title="标题" show-cancel-button></van-dialog>
</AppLayout>
</template>
<script setup>
import { ref, onBeforeUnmount, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant';
import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
import VideoPlayer from "@/components/ui/VideoPlayer.vue";
import AudioPlayer from "@/components/ui/AudioPlayer.vue";
import { useTitle } from '@vueuse/core';
import dayjs from 'dayjs';
import { getTaskDetailAPI, getUploadTaskListAPI, delUploadTaskInfoAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from "@/api/checkin";
const route = useRoute()
const router = useRouter()
useTitle(route.meta.title);
const value1 = ref(0);
const value2 = ref('a');
const value3 = ref('v');
const option1 = [
{ text: '全部商品', value: 0 },
{ text: '新款商品', value: 1 },
{ text: '活动商品', value: 2 },
];
const option2 = [
{ text: '默认排序', value: 'a' },
{ text: '好评排序', value: 'b' },
{ text: '销量排序', value: 'c' },
];
const option3 = [
{ text: '默认排序', value: 'v' },
{ text: '好评排序', value: 'b' },
{ text: '销量排序', value: 'c' },
];
const active = ref(0);
const myRefCalendar = ref(null);
// 窗口尺寸相关的响应式数据
const windowHeight = ref(window.innerHeight);
const windowWidth = ref(window.innerWidth);
/**
* 动态计算日历高度
* 根据屏幕尺寸和设备类型自适应调整
*/
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.45;
} else if (windowWidth.value < 414) {
// 中等屏幕手机
heightRatio = 0.42;
} else if (windowWidth.value >= 768) {
// 平板或更大屏幕
heightRatio = 0.35;
}
const calculatedHeight = Math.floor(availableHeight * heightRatio);
// 确保高度在合理范围内
const finalHeight = Math.max(minHeight, Math.min(maxHeight, calculatedHeight));
return `${finalHeight}px`;
});
/**
* 监听窗口尺寸变化
*/
const handleResize = () => {
windowHeight.value = window.innerHeight;
windowWidth.value = window.innerWidth;
};
// 组件挂载时添加事件监听
onMounted(() => {
window.addEventListener('resize', handleResize);
// 监听屏幕方向变化(移动端)
window.addEventListener('orientationchange', () => {
// 延迟更新,等待方向变化完成
setTimeout(handleResize, 100);
});
});
// 存储所有视频播放器的引用
const videoPlayers = ref([]);
// 存储所有音频播放器的引用
const audioPlayers = ref([]);
// 组件卸载前清理播放器引用和事件监听器
onBeforeUnmount(() => {
// 停止所有视频和音频播放
if (videoPlayers.value) {
videoPlayers.value.forEach(player => {
if (player && typeof player?.pause === 'function') {
player?.pause();
}
});
}
stopAllAudio();
// 清空引用数组
if (videoPlayers.value) videoPlayers.value = [];
if (audioPlayers.value) audioPlayers.value = [];
// 清理事件监听器
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
});
/**
* 开始播放指定帖子的视频
* @param {Object} post - 要播放视频的帖子对象
*/
const startPlay = (post) => {
// 确保checkinDataList.value是一个数组
if (checkinDataList.value) {
// 先暂停所有其他视频
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
if (v.id !== post.id) {
v.isPlaying = false;
}
});
});
}
// 设置当前视频为播放状态
post.isPlaying = true;
};
/**
* 处理视频播放事件
* @param {Object} player - 视频播放器实例
* @param {Object} post - 包含视频的帖子对象
*/
const handleVideoPlay = (player, post) => {
stopAllAudio();
};
/**
* 处理视频暂停事件
* @param {Object} post - 包含视频的帖子对象
*/
const handleVideoPause = (post) => {
// 视频暂停时不改变isPlaying状态,保持播放器可见
// 这样用户可以继续从暂停处播放
};
/**
* 停止除当前播放器外的所有其他视频
* @param {Object} currentPlayer - 当前播放的视频播放器实例
* @param {Object} currentPost - 当前播放的帖子对象
*/
const stopOtherVideos = (currentPlayer, currentPost) => {
// 确保videoPlayers.value是一个数组
if (videoPlayers.value) {
// 暂停其他视频播放器
videoPlayers.value.forEach(player => {
if (player !== currentPlayer && player.pause) {
player.pause();
}
});
}
// 更新其他帖子的播放状态
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
if (v.id !== currentPost.id) {
v.isPlaying = false;
}
});
});
};
/**
* 处理音频播放事件
* @param {Object} player - 音频播放器实例
* @param {Object} post - 包含音频的帖子对象
*/
const handleAudioPlay = (player, post) => {
// 停止其他音频播放
stopOtherAudio(player, post);
};
const stopOtherAudio = (currentPlayer, currentPost) => {
// 确保audioPlayers.value是一个数组
if (audioPlayers.value) {
// 暂停其他音频播放器
audioPlayers.value.forEach(player => {
if (player.id!== currentPost.id && player.pause) {
player.pause();
}
});
}
// 更新其他帖子的播放状态
checkinDataList.value.forEach(post => {
if (post.id!== currentPost.id) {
post.isPlaying = false;
}
});
// 停止所有视频播放
stopAllVideos();
}
const stopAllAudio = () => {
// 确保audioPlayers.value是一个数组
if (!audioPlayers.value) return;
audioPlayers.value?.forEach(player => {
// 使用组件暴露的pause方法
if (typeof player.pause === 'function') {
player.pause();
}
});
// 更新所有帖子的播放状态
checkinDataList.value.forEach(post => {
if (post.audio.length) {
post.isPlaying = false;
}
});
}
/**
* 停止所有视频播放
*/
const stopAllVideos = () => {
// 确保videoPlayers.value是一个数组
if (!videoPlayers.value) return;
// 更新所有帖子的播放状态
checkinDataList.value.forEach(p => {
p.videoList.forEach(v => {
v.isPlaying = false;
});
});
};
const themeVars = {
calendarSelectedDayBackground: '#4caf50',
calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)',
calendarInfoLineHeight: '0.3rem',
}
const progress1 = ref(0);
// const progress2 = ref(76);
const teamAvatars = ref([])
// 图片预览相关
const showImagePreview = ref(false);
const startPosition = ref(0);
const currentPost = ref(null);
// 打开图片预览
const openImagePreview = (index, post) => {
currentPost.value = post;
startPosition.value = index;
showImagePreview.value = true;
}
// 图片切换事件处理
const onChange = (index) => {
startPosition.value = index;
}
const formatter = (day) => {
const year = day.date.getFullYear();
const month = day.date.getMonth() + 1;
const date = day.date.getDate();
let checkin_days = myCheckinDates.value;
// 检查当前日期是否在签到日期列表中
if (checkin_days && checkin_days.length > 0) {
// 格式化当前日期为YYYY-MM-DD格式,与checkin_days中的格式匹配
const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`;
// 检查是否已签到
if (checkin_days.includes(formattedDate)) {
day.className = 'calendar-checkin';
day.type = 'selected';
day.bottomInfo = '已签到';
}
}
// 选中今天的日期
if (dayjs(day.date).isSame(new Date(), 'day')) {
day.className = 'calendar-today';
day.type = 'selected';
day.bottomInfo = '今日';
}
return day;
}
const onSelectDay = (day) => {
getTaskDetail(dayjs(day).format('YYYY-MM'));
// 修改浏览器地址把当前的date加入地址栏, 页面不刷新
router.push({
path: route.path,
query: {
...route.query,
date: dayjs(day).format('YYYY-MM-DD')
}
})
// 重置分页参数
page.value = 0
checkinDataList.value = []
finished.value = false
// 重新加载数据
onLoad(dayjs(day).format('YYYY-MM-DD'))
}
const onClickSubtitle = (evt) => {
console.warn('点击了日期标题');
}
const handLike = async (post) => {
if (!post.is_liked) {
const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, })
if (code) {
showSuccessToast('点赞成功')
post.likes++;
post.is_liked = true;
}
} else {
const { code, data } = await dislikeUploadTaskInfoAPI({ checkin_id: post.id, })
if (code) {
showSuccessToast('取消点赞成功')
post.likes--;
post.is_liked = false;
}
}
}
const editCheckin = (post) => {
if (post.file_type === 'image') {
router.push({
path: '/checkin/image',
query: {
post_id: post.id,
type: 'image',
status: 'edit',
}
})
} else if (post.file_type === 'video') {
router.push({
path: '/checkin/video',
query: {
post_id: post.id,
type: 'video',
status: 'edit',
}
})
} else if (post.file_type === 'audio') {
router.push({
path: '/checkin/audio',
query: {
post_id: post.id,
type: 'audio',
status: 'edit',
}
})
}
}
const delCheckin = (post) => {
showConfirmDialog({
title: '温馨提示',
message: '您是否确定要删除该动态?',
confirmButtonColor: '#4caf50',
})
.then(async () => {
// 调用接口
const { code, data } = await delUploadTaskInfoAPI({ i: post.id });
if (code) {
// 删除成功后,刷新页面
showSuccessToast('删除成功');
// router.go(0);
// 删除post_id相应的数据
checkinDataList.value = checkinDataList.value.filter(item => item.id !== post.id);
} else {
showErrorToast('删除失败');
}
})
.catch(() => {
// on cancel
});
}
const taskDetail = ref({});
const myCheckinDates = ref([]);
const checkinDataList = ref([]);
const showProgress = ref(true);
const getTaskDetail = async (month) => {
const { code, data } = await getTaskDetailAPI({ i: route.query.id, month });
if (code) {
taskDetail.value = data;
progress1.value = ((data.checkin_number/data.target_number)*100).toFixed(1); // 计算进度条百分比
showProgress.value = !isNaN(progress1.value); // 如果是NaN,就不显示进度条
teamAvatars.value = data.checkin_avatars;
// 获取当前用户的打卡日期
myCheckinDates.value = data.my_checkin_dates;
// 把['2025-06-06'] 转化为 [6] 只取日期去掉0
// myCheckinDates.value = myCheckinDates.value.map(date => {
// return dayjs(date).date();
// })
}
}
const loading = ref(false)
const finished = ref(false)
const limit = ref(3)
const page = ref(0)
const onLoad = async (date) => {
const nextPage = page.value;
const current_date = date || route.query.date || dayjs().format('YYYY-MM-DD');
//
const res = await getUploadTaskListAPI({
limit: limit.value,
page: nextPage,
task_id: route.query.id,
date: current_date
});
if (res.code) {
// 整理数据结构
checkinDataList.value = [...checkinDataList.value, ...formatData(res.data)];
finished.value = res.data.checkin_list.length < limit.value;
page.value = nextPage + 1;
}
loading.value = false;
};
onMounted(async () => {
const current_date = route.query.date;
if (current_date) {
getTaskDetail(dayjs(current_date).format('YYYY-MM'));
myRefCalendar.value?.reset(new Date(current_date));
onLoad(current_date);
} else {
getTaskDetail(dayjs().format('YYYY-MM'));
onLoad(dayjs().format('YYYY-MM-DD'));
}
})
const formatData = (data) => {
let formattedData = [];
formattedData = data?.checkin_list.map((item, index) => {
let images = [];
let audio = [];
let videoList = [];
if (item.file_type === 'image') {
images = item.files.map(file => {
return file.value;
});
} else if (item.file_type === 'video') {
videoList = item.files.map(file => {
return {
id: file.meta_id,
video: file.value,
videoCover: file.cover,
isPlaying: false,
}
})
} else if (item.file_type === 'audio') {
audio = item.files.map(file => {
return {
title: file.name ? file.name : '打卡音频',
artist: file.artist ? file.artist : '',
url: file.value,
cover: file.cover ? file.cover : '',
}
})
}
return {
id: item.id,
task_id: item.task_id,
user: {
name: item.username,
avatar: item.avatar,
time: item.created_time_desc,
},
content: item.note,
images,
videoList,
audio,
isPlaying: false,
likes: item.like_count,
is_liked: item.is_like,
is_my: item.is_my,
file_type: item.file_type,
}
})
return formattedData;
}
</script>
<style lang="less">
.van-back-top {
background-color: #4caf50;
}
.calendar-checkin {
.van-calendar__selected-day {
background: #a2d8a3 !important;
}
}
.calendar-today {
.van-calendar__selected-day {
background: #FAAB0C !important;
}
}
.text-wrapper {
padding: 1rem;
color: #4caf50;
.text-header {
font-size: 1.15rem;
}
.grade-percentage-main {
padding: 0.75rem 1rem;
}
.class-percentage-main {
padding: 0.75rem 1rem;
}
.upload-wrapper {
display: flex;
margin: 1rem 0;
gap: 1rem;
.upload-boxer {
text-align: center;
border: 1px solid #a2d8a3;
border-radius: 5px;
padding: 1rem 0;
flex: 1;
background-color: #fff;
}
}
}
.post-card {
margin: 1rem 0;
padding: 1rem;
background-color: #FFF;
border-radius: 5px;
.post-header {
margin-bottom: 1rem;
}
.user-info {
margin-left: 0.5rem;
.username {
font-weight: 500;
}
.post-time {
color: gray;
font-size: 0.8rem;
}
}
.post-menu {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.post-content {
.post-text {
color: #666;
margin-bottom: 1rem;
}
.post-media {
.post-images {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.post-video {
margin: 1rem 0;
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.post-audio {
margin: 1rem 0;
}
}
}
.post-footer {
margin-top: 1rem;
color: #666;
.like-icon {
margin-right: 0.25rem;
}
.like-count {
font-size: 0.9rem;
}
}
}
</style>