统计调整
This commit is contained in:
parent
c69caf1f85
commit
50ad4eb5e3
@ -56,6 +56,13 @@ export const getKyXcCourseListApi = async (params: any) => {
|
||||
return await get("/api/pb/getKyXcCourseList", params);
|
||||
};
|
||||
|
||||
/**
|
||||
* 就餐点名课程列表接口
|
||||
*/
|
||||
export const getJcDmCourseListApi = async (params: any) => {
|
||||
return await get("/api/pb/getJcDmCourseList", params);
|
||||
};
|
||||
|
||||
/**
|
||||
* 值周巡查接口 - 获取值周排班数据
|
||||
*/
|
||||
|
||||
@ -23,3 +23,9 @@ export const findAllZw = () => {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<!-- 选择器触发按钮 -->
|
||||
<view class="picker-item" :style="customStyle" @click="showPicker">
|
||||
<text>{{ curStudentLabel || displayPlaceholder }}</text>
|
||||
<uni-icons :type="iconArrow" size="14" :color="iconColor"></uni-icons>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
|
||||
<!-- 弹窗选择器 -->
|
||||
@ -155,8 +155,6 @@ const setValueByData = (student: any) => {
|
||||
|
||||
// 显示选择器弹窗
|
||||
const showPicker = async () => {
|
||||
console.log('🖱️ 用户点击学生选择器');
|
||||
|
||||
// 设置用户选择标志
|
||||
isUserSelecting.value = true;
|
||||
|
||||
@ -165,7 +163,6 @@ const showPicker = async () => {
|
||||
|
||||
// 如果已经有学生数据,直接显示弹窗
|
||||
if (studentList.value.length > 0) {
|
||||
console.log('✅ 学生数据已存在,直接显示弹窗');
|
||||
filteredStudentList.value = studentList.value;
|
||||
showPopup.value = true;
|
||||
return;
|
||||
@ -173,27 +170,19 @@ const showPicker = async () => {
|
||||
|
||||
// 如果正在加载中,不重复加载
|
||||
if (isLoading.value) {
|
||||
console.log('⏳ 正在加载中,请稍候');
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接尝试从父组件获取班级信息
|
||||
console.log('📞 尝试从父组件获取班级信息');
|
||||
|
||||
// 通过 emit 请求父组件提供班级信息
|
||||
emit('getClassInfo');
|
||||
|
||||
// 延迟一下,给父组件时间处理
|
||||
setTimeout(() => {
|
||||
console.log('⏰ 延迟检查是否获取到班级信息');
|
||||
console.log('🔍 当前 props.classInfo:', props.classInfo);
|
||||
|
||||
// 如果 props.classInfo 有值,直接使用
|
||||
if (props.classInfo && props.classInfo.njId && props.classInfo.bjId) {
|
||||
console.log('✅ 发现 props.classInfo,直接加载学生数据');
|
||||
loadStudentDataByClassInfo(props.classInfo);
|
||||
} else {
|
||||
console.log('❌ 没有获取到班级信息,尝试直接调用API');
|
||||
// 尝试直接调用API,使用默认的年级班级ID
|
||||
// 这里需要从全局状态或者其他地方获取班级信息
|
||||
// 暂时先显示提示
|
||||
@ -207,28 +196,22 @@ const showPicker = async () => {
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
const loadStudentDataByClassInfo = async (classInfo: any) => {
|
||||
console.log('🔄 通过班级信息加载学生数据:', classInfo);
|
||||
|
||||
if (!classInfo || !classInfo.njId || !classInfo.bjId) {
|
||||
console.log('⚠️ 班级信息不完整,清空学生数据');
|
||||
clearStudentData();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
console.log('🚀 开始加载学生数据');
|
||||
|
||||
const params = {
|
||||
njId: classInfo.njId,
|
||||
bjIds: [classInfo.bjId]
|
||||
};
|
||||
|
||||
console.log('📤 发送API请求,参数:', params);
|
||||
const res = await findStudentInfoByNjAndBjApi(params);
|
||||
|
||||
if (res && res.result && res.result.length > 0) {
|
||||
console.log('✅ 学生数据加载成功:', res.result);
|
||||
studentList.value = res.result;
|
||||
studentRange.value = res.result.map((item: any) => item.xm || '');
|
||||
|
||||
@ -237,23 +220,17 @@ const loadStudentDataByClassInfo = async (classInfo: any) => {
|
||||
|
||||
// 如果有默认值,设置选中项
|
||||
if (props.modelValue) {
|
||||
const index = studentList.value.findIndex((item: any) => item.id === props.modelValue);
|
||||
const modelId = props.modelValue.id || props.modelValue.xsId || props.modelValue;
|
||||
const index = studentList.value.findIndex((item: any) => item.id === modelId);
|
||||
if (index >= 0) {
|
||||
curIndex.value = index;
|
||||
curStudentLabel.value = studentRange.value[index];
|
||||
}
|
||||
} else {
|
||||
// 没有默认值时,不设置 curIndex,保持弹窗打开状态
|
||||
console.log('🎯 不自动选择,等待用户手动选择');
|
||||
curStudentLabel.value = "";
|
||||
}
|
||||
|
||||
// 如果弹窗已打开,显示学生列表
|
||||
if (showPopup.value) {
|
||||
console.log('📋 弹窗已打开,显示学生列表');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 没有找到学生数据');
|
||||
clearStudentData();
|
||||
}
|
||||
} catch (error) {
|
||||
@ -266,35 +243,23 @@ const loadStudentDataByClassInfo = async (classInfo: any) => {
|
||||
|
||||
// 直接设置学生数据的方法
|
||||
const setStudentData = (data: any[]) => {
|
||||
console.log('🔄 直接设置学生数据:', data?.length, '个学生');
|
||||
|
||||
if (data && data.length > 0) {
|
||||
console.log('✅ 设置学生数据成功');
|
||||
studentList.value = data;
|
||||
studentRange.value = data.map((item: any) => item.xm || '');
|
||||
|
||||
// 更新过滤列表
|
||||
filteredStudentList.value = studentList.value;
|
||||
|
||||
console.log('📋 学生选项列表:', studentRange.value.slice(0, 5)); // 只显示前5个选项
|
||||
|
||||
// 如果有默认值,设置选中项
|
||||
if (props.modelValue) {
|
||||
const index = studentList.value.findIndex((item: any) => item.id === props.modelValue);
|
||||
const modelId = props.modelValue.id || props.modelValue.xsId || props.modelValue;
|
||||
const index = studentList.value.findIndex((item: any) => item.id === modelId);
|
||||
if (index >= 0) {
|
||||
curIndex.value = index;
|
||||
curStudentLabel.value = studentRange.value[index];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎯 学生选择器状态更新完成:', {
|
||||
studentListLength: studentList.value.length,
|
||||
studentRangeLength: studentRange.value.length,
|
||||
curIndex: curIndex.value,
|
||||
curStudentLabel: curStudentLabel.value
|
||||
});
|
||||
} else {
|
||||
console.log('⚠️ 学生数据为空,清空数据');
|
||||
clearStudentData();
|
||||
}
|
||||
};
|
||||
@ -313,35 +278,28 @@ const clearStudentData = () => {
|
||||
|
||||
// 搜索输入处理
|
||||
const onSearchInput = () => {
|
||||
console.log('🔍 搜索关键词:', searchKeyword.value);
|
||||
filteredStudentList.value = filterStudents(studentList.value, searchKeyword.value);
|
||||
};
|
||||
|
||||
// 选择学生
|
||||
const selectStudent = (student: any) => {
|
||||
console.log('🎯 选择学生:', student);
|
||||
selectedStudent.value = student;
|
||||
};
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
console.log('❌ 取消选择');
|
||||
showPopup.value = false;
|
||||
selectedStudent.value = null;
|
||||
};
|
||||
|
||||
// 确认选择
|
||||
const handleConfirm = () => {
|
||||
console.log('✅ 确认选择:', selectedStudent.value);
|
||||
|
||||
if (props.disabled) {
|
||||
console.log('⏸️ 组件已禁用,跳过处理');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedStudent.value) {
|
||||
const student = selectedStudent.value;
|
||||
console.log('✅ 选中的学生:', student);
|
||||
curStudentLabel.value = student.xm;
|
||||
|
||||
// 发送数据给父组件
|
||||
@ -360,18 +318,8 @@ const handleConfirm = () => {
|
||||
student: student
|
||||
};
|
||||
|
||||
console.log('📤 发送给父组件的数据:', result);
|
||||
console.log('🔍 学生数据字段映射:', {
|
||||
'student.id': student.id,
|
||||
'result.xsId': result.xsId,
|
||||
'student.xm': student.xm,
|
||||
'result.xsxm': result.xsxm,
|
||||
'result.studentName': result.studentName
|
||||
});
|
||||
emit("change", student);
|
||||
emit("update:modelValue", result);
|
||||
} else {
|
||||
console.log('❌ 未选择学生');
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
@ -381,22 +329,12 @@ const handleConfirm = () => {
|
||||
|
||||
// 加载学生数据
|
||||
const loadStudentData = async () => {
|
||||
console.log('🔄 加载学生数据,年级班级:', {
|
||||
njId: props.njId,
|
||||
bjId: props.bjId,
|
||||
njIds: props.njIds,
|
||||
bjIds: props.bjIds,
|
||||
autoLoad: props.autoLoad
|
||||
});
|
||||
|
||||
if (isLoading.value) {
|
||||
console.log('⏳ 正在加载中,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有年级班级信息
|
||||
if (!props.njId && !props.bjId && (!props.njIds || props.njIds.length === 0) && (!props.bjIds || props.bjIds.length === 0)) {
|
||||
console.log('⚠️ 没有年级班级信息,清空数据');
|
||||
// 如果没有年级班级信息,清空学生列表并显示占位符
|
||||
studentList.value = [];
|
||||
studentRange.value = [];
|
||||
@ -407,7 +345,6 @@ const loadStudentData = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🚀 开始加载学生数据');
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const params: any = {};
|
||||
@ -425,32 +362,31 @@ const loadStudentData = async () => {
|
||||
params.bjIds = props.bjIds;
|
||||
}
|
||||
|
||||
console.log('📤 发送API请求,参数:', params);
|
||||
const res = await findStudentInfoByNjAndBjApi(params);
|
||||
console.log('📥 API响应:', res);
|
||||
studentList.value = res.result || [];
|
||||
|
||||
if (studentList.value.length > 0) {
|
||||
console.log('✅ 成功加载学生数据,数量:', studentList.value.length);
|
||||
studentRange.value = studentList.value.map((student: any) => student.xm);
|
||||
|
||||
// 更新过滤列表
|
||||
filteredStudentList.value = studentList.value;
|
||||
|
||||
console.log('📋 学生选项列表:', studentRange.value);
|
||||
|
||||
// 如果有默认值,设置默认选择
|
||||
if (props.defaultValue) {
|
||||
console.log('🎯 设置默认值:', props.defaultValue);
|
||||
setValueByData(props.defaultValue);
|
||||
} else if (props.modelValue) {
|
||||
const modelId = props.modelValue.id || props.modelValue.xsId || props.modelValue;
|
||||
const index = studentList.value.findIndex((item: any) => item.id === modelId);
|
||||
if (index >= 0) {
|
||||
curIndex.value = index;
|
||||
curStudentLabel.value = studentRange.value[index];
|
||||
}
|
||||
} else {
|
||||
console.log('🎯 不自动选择,等待用户手动选择');
|
||||
// 不自动选择第一个学生,让用户手动选择
|
||||
// 不设置 curIndex,保持弹窗打开状态
|
||||
curStudentLabel.value = "";
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 没有加载到学生数据');
|
||||
studentRange.value = [];
|
||||
filteredStudentList.value = [];
|
||||
curStudentLabel.value = "";
|
||||
@ -467,62 +403,41 @@ const loadStudentData = async () => {
|
||||
studentRange.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
console.log('🏁 loadStudentData 完成');
|
||||
}
|
||||
};
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal && newVal.id) {
|
||||
setValueByData(newVal);
|
||||
watch(() => props.modelValue, (newVal, oldVal) => {
|
||||
if (newVal && (newVal.id || newVal.xsId)) {
|
||||
// 如果学生列表已加载,立即设置回显
|
||||
if (studentList.value.length > 0) {
|
||||
setValueByData(newVal);
|
||||
}
|
||||
// 学生列表未加载,等待 loadStudentData 完成后再设置
|
||||
} else if (!newVal) {
|
||||
curStudentLabel.value = "";
|
||||
}
|
||||
}, { immediate: true });
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 监听年级班级变化,自动加载学生数据
|
||||
watch([() => props.njId, () => props.bjId, () => props.njIds, () => props.bjIds], (newValues, oldValues) => {
|
||||
console.log('👀 年级班级变化:', {
|
||||
newValues,
|
||||
oldValues,
|
||||
autoLoad: props.autoLoad
|
||||
});
|
||||
|
||||
if (props.autoLoad) {
|
||||
console.log('🔄 触发自动加载学生数据');
|
||||
loadStudentData();
|
||||
} else {
|
||||
console.log('⏸️ autoLoad 为 false,跳过加载');
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 监听 classInfo 变化,自动加载学生数据
|
||||
watch(() => props.classInfo, (newClassInfo, oldClassInfo) => {
|
||||
console.log('👀 classInfo 变化:', {
|
||||
newClassInfo,
|
||||
oldClassInfo
|
||||
});
|
||||
|
||||
if (newClassInfo && newClassInfo.njId && newClassInfo.bjId) {
|
||||
console.log('🔄 通过 classInfo 加载学生数据');
|
||||
loadStudentDataByClassInfo(newClassInfo);
|
||||
} else {
|
||||
console.log('⚠️ classInfo 不完整,清空学生数据');
|
||||
clearStudentData();
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 监听 studentData 变化,直接设置学生数据(参考 BasicPicker 的设计模式)
|
||||
watch(() => props.studentData, (newStudentData, oldStudentData) => {
|
||||
console.log('👀 studentData 变化:', {
|
||||
newStudentData,
|
||||
oldStudentData,
|
||||
newStudentDataLength: newStudentData?.length,
|
||||
oldStudentDataLength: oldStudentData?.length
|
||||
});
|
||||
|
||||
if (newStudentData && newStudentData.length > 0) {
|
||||
console.log('✅ 接收到学生数据,直接设置');
|
||||
console.log('📊 学生数据详情:', newStudentData.slice(0, 3)); // 只显示前3个学生
|
||||
|
||||
// 直接设置数据,参考 BasicPicker 的模式
|
||||
studentList.value = newStudentData;
|
||||
studentRange.value = newStudentData.map((item: any) => item.xm || ''); // 使用正确的字段名 xm
|
||||
@ -530,11 +445,10 @@ watch(() => props.studentData, (newStudentData, oldStudentData) => {
|
||||
// 更新过滤列表
|
||||
filteredStudentList.value = studentList.value;
|
||||
|
||||
console.log('📋 学生选项列表:', studentRange.value.slice(0, 5)); // 只显示前5个选项
|
||||
|
||||
// 如果有默认值,设置选中项
|
||||
if (props.modelValue) {
|
||||
const index = studentList.value.findIndex((item: any) => item.id === props.modelValue);
|
||||
const modelId = props.modelValue.id || props.modelValue.xsId || props.modelValue;
|
||||
const index = studentList.value.findIndex((item: any) => item.id === modelId);
|
||||
if (index >= 0) {
|
||||
curIndex.value = index;
|
||||
curStudentLabel.value = studentRange.value[index];
|
||||
@ -543,43 +457,17 @@ watch(() => props.studentData, (newStudentData, oldStudentData) => {
|
||||
// 没有默认值时,不设置 curIndex,保持弹窗打开状态
|
||||
curStudentLabel.value = "";
|
||||
}
|
||||
|
||||
console.log('🎯 学生选择器状态更新完成:', {
|
||||
studentListLength: studentList.value.length,
|
||||
studentRangeLength: studentRange.value.length,
|
||||
curIndex: curIndex.value,
|
||||
curStudentLabel: curStudentLabel.value
|
||||
});
|
||||
} else {
|
||||
console.log('⚠️ 学生数据为空,清空数据');
|
||||
clearStudentData();
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 监听 range 变化,直接设置学生数据(参考 BasicPicker 的设计模式)
|
||||
watch(() => props.range, (newRange, oldRange) => {
|
||||
console.log('👀 range 变化:', {
|
||||
newRange,
|
||||
oldRange,
|
||||
newRangeLength: newRange?.length,
|
||||
oldRangeLength: oldRange?.length
|
||||
});
|
||||
|
||||
if (newRange && newRange.length > 0) {
|
||||
console.log('✅ 接收到 range 数据,直接设置');
|
||||
|
||||
// 直接设置数据,参考 BasicPicker 的模式
|
||||
studentRange.value = newRange;
|
||||
|
||||
console.log('📋 学生选项列表:', studentRange.value.slice(0, 5)); // 只显示前5个选项
|
||||
|
||||
console.log('🎯 学生选择器状态更新完成:', {
|
||||
studentRangeLength: studentRange.value.length,
|
||||
curIndex: curIndex.value,
|
||||
curStudentLabel: curStudentLabel.value
|
||||
});
|
||||
} else {
|
||||
console.log('⚠️ range 数据为空,清空数据');
|
||||
clearStudentData();
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
@ -612,49 +500,24 @@ defineExpose({
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
console.log('🚀 组件已挂载,props:', {
|
||||
njId: props.njId,
|
||||
bjId: props.bjId,
|
||||
autoLoad: props.autoLoad,
|
||||
classInfo: props.classInfo,
|
||||
studentData: props.studentData,
|
||||
range: props.range,
|
||||
studentDataLength: props.studentData?.length,
|
||||
rangeLength: props.range?.length
|
||||
});
|
||||
if (props.autoLoad) {
|
||||
loadStudentData();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 props 变化
|
||||
watch(() => [props.njId, props.bjId, props.njIds, props.bjIds], (newValues, oldValues) => {
|
||||
console.log('🔍 Props 变化监听:', {
|
||||
newValues,
|
||||
oldValues,
|
||||
props: {
|
||||
njId: props.njId,
|
||||
bjId: props.bjId,
|
||||
njIds: props.njIds,
|
||||
bjIds: props.bjIds
|
||||
}
|
||||
});
|
||||
}, { immediate: true, deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.picker-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 7px 15px;
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
justify-content: space-between;
|
||||
min-height: 40px;
|
||||
padding: 24rpx;
|
||||
background: #f8f9fa;
|
||||
border: 1rpx solid #e9ecef;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:active {
|
||||
background-color: #e6f7ff;
|
||||
@ -662,18 +525,8 @@ watch(() => [props.njId, props.bjId, props.njIds, props.bjIds], (newValues, oldV
|
||||
}
|
||||
|
||||
text {
|
||||
margin-right: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 80%; // 内容占80%宽度
|
||||
max-width: 80%; // 确保最大宽度不超过80%
|
||||
}
|
||||
|
||||
// 箭头容器,确保箭头始终显示
|
||||
.uni-icons {
|
||||
flex: 0 0 auto; // 箭头不缩放,保持固定大小
|
||||
margin-left: 8px; // 与文字保持间距
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -227,6 +227,34 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/statistics/jlb/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "俱乐部选课",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/statistics/jlb/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "俱乐部统计",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/statistics/zb/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "值班统计",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/statistics/zb/pdfPreview",
|
||||
"style": {
|
||||
"navigationBarTitleText": "PDF预览",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/statistics/jc/unmeal-teacher-list",
|
||||
"style": {
|
||||
@ -1189,6 +1217,12 @@
|
||||
"navigationBarTitleText": "就餐点名详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/routine/jc/dm",
|
||||
"style": {
|
||||
"navigationBarTitleText": "点名"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/view/quantitativeAssessment/assessment/assessment",
|
||||
"style": {
|
||||
|
||||
@ -533,6 +533,22 @@ const sections = reactive<Section[]>([
|
||||
permissionKey: "analysis-jctj",
|
||||
path: "/pages/statistics/jc/index",
|
||||
},
|
||||
{
|
||||
id: "analysis3",
|
||||
icon: "jlb",
|
||||
text: "俱乐部统计",
|
||||
show: true,
|
||||
permissionKey: "analysis-jlb",
|
||||
path: "/pages/statistics/jlb/index",
|
||||
},
|
||||
{
|
||||
id: "analysis4",
|
||||
icon: "zbtj",
|
||||
text: "值班统计",
|
||||
show: true,
|
||||
permissionKey: "analysis-zbtj",
|
||||
path: "/pages/statistics/zb/index",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@ -42,8 +42,9 @@
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="class-badge" :class="{ 'badge-attended': classItem.attended, 'badge-unattended': !classItem.attended }">
|
||||
{{ classItem.attended ? '已完成' : '待点名' }}
|
||||
<view class="class-teacher-badge" :class="{ 'badge-attended': classItem.attended, 'badge-unattended': !classItem.attended }">
|
||||
<text v-if="classItem.jsNames" class="teacher-names">{{ classItem.jsNames }}</text>
|
||||
<text v-else class="no-teacher">未分配</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -68,6 +69,7 @@ interface ClassAttendanceInfo {
|
||||
bjmcName: string;
|
||||
attended: boolean;
|
||||
attendedText: string;
|
||||
jsNames?: string;
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
@ -147,7 +149,8 @@ const loadClassAttendanceStatistics = async () => {
|
||||
bjId: item.bjId,
|
||||
bjmcName: item.bjmcName,
|
||||
attended: item.attended === 1 || item.attended === true,
|
||||
attendedText: item.attendedText || (item.attended ? '已点名' : '未点名')
|
||||
attendedText: item.attendedText || (item.attended ? '已点名' : '未点名'),
|
||||
jsNames: item.jsNames || ''
|
||||
}));
|
||||
|
||||
// 根据type筛选数据
|
||||
@ -366,20 +369,34 @@ onLoad((options: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
.class-badge {
|
||||
.class-teacher-badge {
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
max-width: 200rpx;
|
||||
text-align: center;
|
||||
|
||||
&.badge-attended {
|
||||
background-color: #DCFCE7;
|
||||
color: #22C55E;
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
&.badge-unattended {
|
||||
background-color: #FEE2E2;
|
||||
color: #EF4444;
|
||||
background-color: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.teacher-names {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-teacher {
|
||||
color: #999;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
165
src/pages/statistics/jlb/detail.vue
Normal file
165
src/pages/statistics/jlb/detail.vue
Normal file
@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<view class="jlb-statistics-container">
|
||||
<!-- 顶部Tab -->
|
||||
<view class="tab-container">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'overview' }"
|
||||
@click="switchTab('overview')"
|
||||
>
|
||||
总览
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'routine' }"
|
||||
@click="switchTab('routine')"
|
||||
>
|
||||
常规
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 总览内容 -->
|
||||
<OverviewComponent v-if="activeTab === 'overview'" :xkId="xkId" />
|
||||
|
||||
<!-- 常规内容 -->
|
||||
<RoutineComponent v-if="activeTab === 'routine'" :xkId="xkId" />
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view v-if="loading" class="loading-overlay">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import OverviewComponent from './overview.vue'
|
||||
import RoutineComponent from './routine.vue'
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('overview')
|
||||
const loading = ref(false)
|
||||
const xkId = ref('')
|
||||
|
||||
// 页面加载时获取xkId参数
|
||||
onLoad((options: any) => {
|
||||
console.log('俱乐部统计详情页加载,参数:', options)
|
||||
if (options && options.xkId) {
|
||||
xkId.value = options.xkId
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '缺少选课ID参数',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 方法
|
||||
const switchTab = (tab: string) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.jlb-statistics-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
font-size: 15px;
|
||||
color: #666;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
color: #3B82F6;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
background-color: #3B82F6;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
496
src/pages/statistics/jlb/index.vue
Normal file
496
src/pages/statistics/jlb/index.vue
Normal file
@ -0,0 +1,496 @@
|
||||
<template>
|
||||
<view class="xk-list-container">
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header">
|
||||
<text class="page-title">俱乐部统计</text>
|
||||
<text class="page-subtitle">{{ xkList.length }}个选课</text>
|
||||
</view>
|
||||
|
||||
<!-- 学期选择器 -->
|
||||
<view class="filter-bar">
|
||||
<view class="filter-label">学期选择:</view>
|
||||
<uni-data-select
|
||||
v-model="searchForm.xqId"
|
||||
:localdata="xqList"
|
||||
placeholder="选择学期"
|
||||
@change="handleXqChange"
|
||||
class="xq-select"
|
||||
></uni-data-select>
|
||||
</view>
|
||||
|
||||
<!-- 选课列表 -->
|
||||
<view class="section" v-if="xkList.length > 0">
|
||||
<view class="section-title">
|
||||
选课信息 ({{ xkList.length }}个选课)
|
||||
</view>
|
||||
|
||||
<!-- 选课列表 -->
|
||||
<view class="xk-list">
|
||||
<view
|
||||
v-for="xk in xkList"
|
||||
:key="xk.id"
|
||||
class="xk-item bg-white r-md p-12"
|
||||
@click="goToXkCourse(xk)"
|
||||
>
|
||||
<view class="xk-header">
|
||||
<view class="xk-title">{{ xk.xkmc }}</view>
|
||||
<view class="xk-actions">
|
||||
<view v-if="xk.xkStatus !== '已结束'" class="xk-status" :class="getStatusClass(xk.xkStatus)">
|
||||
{{ xk.xkStatus }}
|
||||
</view>
|
||||
<view v-else class="more-btn" @click.stop="showMoreOptions(xk)">
|
||||
<image src="/static/base/view/more.png" class="more-icon" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="xk-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">选课类型:</text>
|
||||
<text class="info-value">{{ xk.xklxName || '未知' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">学期名称:</text>
|
||||
<text class="info-value">{{ xk.xqmc || '未知' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">年级范围:</text>
|
||||
<text class="info-value">{{ xk.njmc || '全部' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" v-if="loading">
|
||||
<view class="loading-text">正在加载...</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else-if="!loading && xkList.length === 0 && hasSearched">
|
||||
<view class="empty-icon">📚</view>
|
||||
<view class="empty-text">暂无俱乐部选课数据</view>
|
||||
<view class="empty-tip">当前学期没有俱乐部选课</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
// 定义类型接口
|
||||
interface XkInfo {
|
||||
id: string
|
||||
xkmc: string
|
||||
xklxId: string
|
||||
xklxName: string
|
||||
njId: string
|
||||
njmc: string
|
||||
xqId: string
|
||||
xqmc: string
|
||||
xkkstime: string
|
||||
xkjstime: string
|
||||
xkzt: number
|
||||
xkStatus: string
|
||||
allXkNum: number
|
||||
yfXkNum: number
|
||||
wfXkNum: number
|
||||
status: string
|
||||
createdTime: string
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
xqId: '' // 学期ID
|
||||
})
|
||||
|
||||
// 学期列表
|
||||
const xqList = ref<Array<{ value: string; text: string }>>([])
|
||||
|
||||
// 响应式数据
|
||||
const xkList = ref<XkInfo[]>([])
|
||||
const loading = ref(false)
|
||||
const hasSearched = ref(false)
|
||||
const hasMore = ref(false)
|
||||
|
||||
// 页面加载
|
||||
onLoad(() => {
|
||||
console.log('俱乐部选课列表页面加载')
|
||||
loadXqList()
|
||||
})
|
||||
|
||||
// 获取学期列表
|
||||
const loadXqList = async () => {
|
||||
try {
|
||||
const response: any = await get('/api/xq/find')
|
||||
console.log('学期列表原始数据:', response)
|
||||
|
||||
// 转换数据格式为 uni-data-select 需要的格式
|
||||
const data = Array.isArray(response) ? response : (response?.result || response?.data || response?.rows || [])
|
||||
console.log('提取的学期数据:', data)
|
||||
|
||||
xqList.value = data.map((item: any) => ({
|
||||
value: item.id,
|
||||
text: item.xqmc || item.name
|
||||
}))
|
||||
console.log('转换后的下拉选项:', xqList.value)
|
||||
|
||||
// 查找当前学期(dqxq = '是')
|
||||
const currentXq = data.find((item: any) => item.dqxq === '是')
|
||||
if (currentXq) {
|
||||
searchForm.xqId = currentXq.id
|
||||
console.log('默认选择当前学期:', searchForm.xqId)
|
||||
} else if (xqList.value.length > 0) {
|
||||
// 如果没有当前学期,选择第一个
|
||||
searchForm.xqId = xqList.value[0].value
|
||||
console.log('默认选择第一个学期:', searchForm.xqId)
|
||||
}
|
||||
|
||||
// 自动触发查询
|
||||
if (searchForm.xqId) {
|
||||
loadXkList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取学期列表失败:', error)
|
||||
xqList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 处理学期选择变化
|
||||
const handleXqChange = (value: string) => {
|
||||
console.log('选择的学期ID:', value)
|
||||
// 选择学期后自动查询
|
||||
loadXkList()
|
||||
}
|
||||
|
||||
// 加载俱乐部选课列表
|
||||
const loadXkList = async () => {
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
hasSearched.value = true
|
||||
|
||||
try {
|
||||
console.log('查询俱乐部选课列表,学期ID:', searchForm.xqId)
|
||||
|
||||
// 调用后端接口查询俱乐部选课列表
|
||||
const response = await get('/api/xk/getClubXkList', {
|
||||
xqId: searchForm.xqId,
|
||||
xklxId: '816059832' // 俱乐部选课类型ID
|
||||
})
|
||||
|
||||
console.log('API返回结果:', response)
|
||||
|
||||
if (response.resultCode === 1 && response.result) {
|
||||
xkList.value = response.result || []
|
||||
} else {
|
||||
throw new Error(response.message || '查询失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询俱乐部选课列表失败:', error)
|
||||
uni.showToast({
|
||||
title: '查询失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
xkList.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到俱乐部统计详情页面
|
||||
const goToXkCourse = (xk: XkInfo) => {
|
||||
console.log('跳转到俱乐部统计详情:', xk)
|
||||
uni.navigateTo({
|
||||
url: `/pages/statistics/jlb/detail?xkId=${xk.id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 显示更多选项 - 直接跳转到俱乐部统计详情页面
|
||||
const showMoreOptions = (xk: XkInfo) => {
|
||||
console.log('点击更多按钮,直接跳转到俱乐部统计详情:', xk)
|
||||
goToXkCourse(xk)
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case '未开始':
|
||||
return 'status-pending'
|
||||
case '进行中':
|
||||
return 'status-active'
|
||||
case '已结束':
|
||||
return 'status-ended'
|
||||
default:
|
||||
return 'status-unknown'
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xk-list-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 28rpx;
|
||||
color: #3B82F6;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.xq-select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.xk-list {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.xk-item {
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.xk-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 15rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.xk-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.xk-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xk-status {
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-weight: bold;
|
||||
|
||||
&.status-pending {
|
||||
background-color: #fff7e6;
|
||||
color: #fa8c16;
|
||||
border: 1rpx solid #ffd591;
|
||||
}
|
||||
|
||||
&.status-active {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
border: 1rpx solid #b7eb8f;
|
||||
}
|
||||
|
||||
&.status-ended {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
border: 1rpx solid #ffccc7;
|
||||
}
|
||||
|
||||
&.status-unknown {
|
||||
background-color: #f5f5f5;
|
||||
color: #999;
|
||||
border: 1rpx solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #f5f5f5;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #e6f7ff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #d9d9d9;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.xk-info {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
margin-right: 10rpx;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 40rpx;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 100rpx;
|
||||
|
||||
.loading-text {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.r-md {
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.p-12 {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
// uni-data-select 组件样式调整
|
||||
:deep(.xq-select) {
|
||||
.uni-select {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #e9ecef;
|
||||
height: 70rpx;
|
||||
min-height: 70rpx;
|
||||
}
|
||||
|
||||
.uni-select__input-text {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.uni-select__input-placeholder {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.uni-select__selector {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
968
src/pages/statistics/jlb/overview.vue
Normal file
968
src/pages/statistics/jlb/overview.vue
Normal file
@ -0,0 +1,968 @@
|
||||
<template>
|
||||
<view class="overview-container">
|
||||
<!-- 加载遮罩层 -->
|
||||
<view v-if="loading" class="loading-mask">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">数据加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="overview-scroll"
|
||||
@scroll="handleScroll"
|
||||
:scroll-top="scrollTop"
|
||||
:enable-back-to-top="true"
|
||||
>
|
||||
<!-- 维度1: 2025年秋期俱乐部周末班统计 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">2025年秋期俱乐部周末班</text>
|
||||
<text class="section-subtitle">课程统计</text>
|
||||
</view>
|
||||
<view class="summary-stats">
|
||||
<view class="stat-row">
|
||||
<!-- 总人数 - 全宽 -->
|
||||
<view class="stat-card stat-card-full" :style="{ backgroundColor: summaryStats[0].bgColor }">
|
||||
<view class="card-icon" :style="{ backgroundColor: summaryStats[0].iconBg }">
|
||||
<text class="card-icon-text" :style="{ color: summaryStats[0].color }">{{ summaryStats[0].icon }}</text>
|
||||
</view>
|
||||
<view class="card-content">
|
||||
<text class="card-label">{{ summaryStats[0].label }}</text>
|
||||
<text class="card-value" :style="{ color: summaryStats[0].color }">{{ summaryStats[0].value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 其他统计项 - 两列布局 -->
|
||||
<view class="stat-grid">
|
||||
<view
|
||||
class="stat-card"
|
||||
v-for="(item, index) in summaryStats.slice(1)"
|
||||
:key="index"
|
||||
:style="{ backgroundColor: item.bgColor }"
|
||||
:class="{ 'stat-card-no-icon': item.noIcon }"
|
||||
>
|
||||
<view v-if="!item.noIcon" class="card-icon" :style="{ backgroundColor: item.iconBg }">
|
||||
<text class="card-icon-text" :style="{ color: item.color }">{{ item.icon }}</text>
|
||||
</view>
|
||||
<view class="card-content">
|
||||
<text class="card-label">{{ item.label }}</text>
|
||||
<text class="card-value" :style="{ color: item.color }">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 维度2: 课程类型统计 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">课程类型</text>
|
||||
<text class="section-subtitle">人数分布</text>
|
||||
</view>
|
||||
<view class="course-type-content">
|
||||
<view class="course-type-chart">
|
||||
<view v-if="!courseTypeChart.series[0].data.length" class="chart-placeholder">暂无数据</view>
|
||||
<QiunDataCharts
|
||||
v-else
|
||||
class="chart-instance chart-pie"
|
||||
canvas-id="course-type-pie"
|
||||
type="pie"
|
||||
:chart-data="courseTypeChart"
|
||||
:opts="pieChartOpts"
|
||||
:animation="false"
|
||||
:in-scroll-view="true"
|
||||
/>
|
||||
</view>
|
||||
<view v-if="courseTypeLegend.length" class="course-type-legend">
|
||||
<view
|
||||
class="course-type-legend-item"
|
||||
v-for="(item, index) in courseTypeLegend"
|
||||
:key="index"
|
||||
>
|
||||
<view class="legend-left">
|
||||
<view class="legend-dot" :style="{ backgroundColor: item.color }" />
|
||||
<text class="legend-name">
|
||||
{{ item.name }}:{{ item.totalCount }}<text class="legend-unit">限</text>
|
||||
</text>
|
||||
</view>
|
||||
<view class="legend-right">
|
||||
<text class="legend-registered">{{ item.registeredCount }}人</text>
|
||||
<text class="legend-percent">{{ item.percent }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 维度3: 课程教师统计 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">课程教师</text>
|
||||
<text class="section-subtitle">校内外分布</text>
|
||||
</view>
|
||||
<view class="teacher-content">
|
||||
<view class="teacher-chart-wrapper">
|
||||
<QiunDataCharts
|
||||
class="chart-instance chart-ring"
|
||||
canvas-id="teacher-ring"
|
||||
type="ring"
|
||||
:chart-data="teacherChart"
|
||||
:opts="ringChartOpts"
|
||||
:animation="false"
|
||||
:in-scroll-view="true"
|
||||
/>
|
||||
</view>
|
||||
<view class="teacher-side">
|
||||
<view class="teacher-legend">
|
||||
<view class="legend-item" v-for="(item, index) in teacherLegend" :key="index">
|
||||
<view class="legend-info">
|
||||
<view class="legend-left">
|
||||
<view class="legend-dot" :style="{ backgroundColor: item.color }" />
|
||||
<text class="legend-name">{{ item.label }}</text>
|
||||
</view>
|
||||
<text class="legend-value">{{ item.value }}人</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 维度4: 开课课程统计 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">开课课程</text>
|
||||
<text class="section-subtitle">按课程类型统计</text>
|
||||
</view>
|
||||
<view class="chart-content chart-content-column">
|
||||
<view v-if="!courseHoursChart.categories.length" class="chart-placeholder">暂无数据</view>
|
||||
<QiunDataCharts
|
||||
v-else
|
||||
class="chart-instance chart-column"
|
||||
canvas-id="course-hours-column"
|
||||
type="column"
|
||||
:chart-data="courseHoursChart"
|
||||
:opts="columnChartOpts"
|
||||
:animation="false"
|
||||
:in-scroll-view="true"
|
||||
:ontouch="true"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import QiunDataCharts from '@/components/charts/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue';
|
||||
import { get } from '@/utils/request';
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
xkId: string
|
||||
}>();
|
||||
|
||||
// 滚动优化
|
||||
const scrollTop = ref(0);
|
||||
const lastScrollTime = ref(0);
|
||||
const loading = ref(false);
|
||||
|
||||
// 滚动事件处理(节流优化)
|
||||
const handleScroll = (e: any) => {
|
||||
const now = Date.now();
|
||||
if (now - lastScrollTime.value < 100) {
|
||||
return;
|
||||
}
|
||||
lastScrollTime.value = now;
|
||||
};
|
||||
|
||||
// 维度1: 2025年秋期俱乐部周末班统计数据
|
||||
const summaryStats = ref([
|
||||
{
|
||||
label: '课程限报人数',
|
||||
value: 0,
|
||||
icon: '👥',
|
||||
color: '#4F46E5',
|
||||
bgColor: '#EEF2FF',
|
||||
iconBg: '#E0E7FF',
|
||||
noIcon: false
|
||||
},
|
||||
{
|
||||
label: '已报名',
|
||||
value: 0,
|
||||
icon: '✓',
|
||||
color: '#22C55E',
|
||||
bgColor: '#F0FDF4',
|
||||
iconBg: '#DCFCE7',
|
||||
noIcon: false
|
||||
},
|
||||
{
|
||||
label: '未报名',
|
||||
value: 0,
|
||||
icon: '✕',
|
||||
color: '#EF4444',
|
||||
bgColor: '#FEF2F2',
|
||||
iconBg: '#FEE2E2',
|
||||
noIcon: false
|
||||
},
|
||||
{
|
||||
label: '缴费金额',
|
||||
value: '¥0',
|
||||
icon: '',
|
||||
color: '#0EA5E9',
|
||||
bgColor: '#F0F9FF',
|
||||
iconBg: '#E0F2FE',
|
||||
noIcon: true // 不显示图标
|
||||
},
|
||||
{
|
||||
label: '退费金额',
|
||||
value: '¥0',
|
||||
icon: '',
|
||||
color: '#F59E0B',
|
||||
bgColor: '#FFFBEB',
|
||||
iconBg: '#FEF3C7',
|
||||
noIcon: true // 不显示图标
|
||||
}
|
||||
]);
|
||||
|
||||
// 维度2: 课程类型统计数据
|
||||
const courseTypeColors = ['#4F46E5', '#0EA5E9', '#22C55E', '#F59E0B', '#EF4444', '#8B5CF6', '#14B8A6', '#6366F1'];
|
||||
|
||||
const courseTypeChart = ref<any>({
|
||||
series: [
|
||||
{
|
||||
name: '课程类型',
|
||||
data: []
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const pieChartOpts = ref({
|
||||
color: courseTypeColors,
|
||||
height: 420,
|
||||
padding: [20, 20, 0, 15],
|
||||
enableScroll: false,
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
dataLabel: true,
|
||||
title: {
|
||||
name: '',
|
||||
},
|
||||
subtitle: {
|
||||
name: '',
|
||||
},
|
||||
extra: {
|
||||
pie: {
|
||||
activeOpacity: 0.5,
|
||||
activeRadius: 10,
|
||||
offsetAngle: 0,
|
||||
labelWidth: 25,
|
||||
border: true,
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFFFFF',
|
||||
linearType: 'none',
|
||||
radius: 140,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 维度3: 课程教师统计数据
|
||||
const teacherChart = ref<any>({
|
||||
series: [
|
||||
{
|
||||
name: '教师统计',
|
||||
data: []
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const ringChartOpts = computed(() => ({
|
||||
color: ['#4F46E5', '#0EA5E9'],
|
||||
height: 350,
|
||||
padding: [10, 20, 0, 20],
|
||||
enableScroll: false,
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
dataLabel: {
|
||||
show: false
|
||||
},
|
||||
title: {
|
||||
name: String(teacherChart.value.series[0].data.reduce((sum: number, item: any) => sum + (item.value || 0), 0)),
|
||||
fontSize: 30,
|
||||
fontWeight: 'bold',
|
||||
color: '#3b82f6',
|
||||
offsetY: -2
|
||||
},
|
||||
subtitle: {
|
||||
name: '教师总数',
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
offsetY: 5
|
||||
},
|
||||
extra: {
|
||||
ring: {
|
||||
ringWidth: 25,
|
||||
activeOpacity: 0.5,
|
||||
activeRadius: 10,
|
||||
offsetAngle: 0,
|
||||
labelWidth: 15,
|
||||
border: true,
|
||||
borderWidth: 3,
|
||||
borderColor: '#FFFFFF'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// 教师图例数据
|
||||
const teacherLegend = computed(() => {
|
||||
const data = teacherChart.value.series[0].data;
|
||||
const colors = ['#4F46E5', '#0EA5E9'];
|
||||
return data.map((item: any, index: number) => ({
|
||||
label: item.name,
|
||||
value: item.value,
|
||||
color: colors[index]
|
||||
}));
|
||||
});
|
||||
|
||||
const courseTypeLegend = computed(() => {
|
||||
const data = (courseTypeChart.value.series?.[0]?.data || []) as any[];
|
||||
const total = data.reduce((sum: number, item: any) => sum + (item.value || 0), 0);
|
||||
return data.map((item: any, index: number) => ({
|
||||
name: item.name || '',
|
||||
totalCount: item.totalCount || 0,
|
||||
registeredCount: item.registeredCount || 0,
|
||||
color: courseTypeColors[index % courseTypeColors.length],
|
||||
percent: total > 0 ? Math.round(((item.value || 0) / total) * 100) : 0
|
||||
}));
|
||||
});
|
||||
|
||||
// 维度4: 开课课时统计数据
|
||||
const courseHoursChart = ref<any>({
|
||||
categories: [],
|
||||
series: [
|
||||
{
|
||||
name: '开课课程数量',
|
||||
data: []
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const columnChartOpts = ref({
|
||||
color: ['#4F46E5', '#3B82F6'],
|
||||
height: 320,
|
||||
padding: [15, 20, 10, 20],
|
||||
enableScroll: true,
|
||||
dataLabel: true,
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
float: 'center',
|
||||
fontSize: 13,
|
||||
lineHeight: 11,
|
||||
margin: 8
|
||||
},
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
fontSize: 12,
|
||||
fontColor: '#666',
|
||||
itemCount: 4,
|
||||
scrollShow: true,
|
||||
scrollAlign: 'left'
|
||||
},
|
||||
yAxis: {
|
||||
gridType: 'dash',
|
||||
dashLength: 2,
|
||||
splitNumber: 5,
|
||||
showTitle: false,
|
||||
fontSize: 11,
|
||||
fontColor: '#666',
|
||||
data: [
|
||||
{
|
||||
min: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
extra: {
|
||||
column: {
|
||||
type: 'group',
|
||||
width: 30,
|
||||
seriesGap: 15,
|
||||
categoryGap: 15,
|
||||
activeBgColor: '#000000',
|
||||
activeBgOpacity: 0.08,
|
||||
linearType: 'custom',
|
||||
barBorderCircle: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 加载总览统计数据
|
||||
const loadOverviewData = async () => {
|
||||
if (!props.xkId) {
|
||||
console.error('缺少xkId参数');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
console.log('加载俱乐部总览统计数据,xkId:', props.xkId);
|
||||
|
||||
const response = await get('/api/xkkc/statistics/clubOverviewDashboard', {
|
||||
xkId: props.xkId
|
||||
});
|
||||
|
||||
if (response.resultCode === 1 && response.result) {
|
||||
const dashboard = response.result;
|
||||
|
||||
// 1. 更新俱乐部周末班统计
|
||||
if (dashboard.registrationStats) {
|
||||
const reg = dashboard.registrationStats;
|
||||
|
||||
// 格式化金额(大金额转换为万元)
|
||||
const formatAmount = (amount: number) => {
|
||||
if (amount >= 10000) {
|
||||
return `¥${(amount / 10000).toFixed(2)}万`;
|
||||
}
|
||||
return `¥${amount.toLocaleString()}`;
|
||||
};
|
||||
|
||||
summaryStats.value = [
|
||||
{
|
||||
label: '课程限报人数',
|
||||
value: reg.totalCount || 0,
|
||||
icon: '👥',
|
||||
color: '#4F46E5',
|
||||
bgColor: '#EEF2FF',
|
||||
iconBg: '#E0E7FF',
|
||||
noIcon: false
|
||||
},
|
||||
{
|
||||
label: '已报名',
|
||||
value: reg.registeredCount || 0,
|
||||
icon: '✓',
|
||||
color: '#22C55E',
|
||||
bgColor: '#F0FDF4',
|
||||
iconBg: '#DCFCE7',
|
||||
noIcon: false
|
||||
},
|
||||
{
|
||||
label: '未报名',
|
||||
value: reg.unregisteredCount || 0,
|
||||
icon: '✕',
|
||||
color: '#EF4444',
|
||||
bgColor: '#FEF2F2',
|
||||
iconBg: '#FEE2E2',
|
||||
noIcon: false
|
||||
},
|
||||
{
|
||||
label: '缴费金额',
|
||||
value: formatAmount(reg.paidAmount || 0),
|
||||
icon: '',
|
||||
color: '#0EA5E9',
|
||||
bgColor: '#F0F9FF',
|
||||
iconBg: '#E0F2FE',
|
||||
noIcon: true // 不显示图标
|
||||
},
|
||||
{
|
||||
label: '退费金额',
|
||||
value: formatAmount(reg.refundAmount || 0),
|
||||
icon: '',
|
||||
color: '#F59E0B',
|
||||
bgColor: '#FFFBEB',
|
||||
iconBg: '#FEF3C7',
|
||||
noIcon: true // 不显示图标
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// 2. 更新课程类型统计
|
||||
if (dashboard.courseTypeStats && dashboard.courseTypeStats.length > 0) {
|
||||
const typeData = dashboard.courseTypeStats;
|
||||
const totalCount = typeData.reduce((sum: number, item: any) => sum + (item.totalCount || 0), 0);
|
||||
|
||||
courseTypeChart.value = {
|
||||
series: [
|
||||
{
|
||||
name: '课程类型',
|
||||
data: typeData.map((item: any) => {
|
||||
const percent = totalCount > 0 ? Math.round((item.totalCount / totalCount) * 100) : 0;
|
||||
return {
|
||||
name: item.typeName,
|
||||
value: item.totalCount,
|
||||
labelText: `${item.typeName} ${percent}%`, // 饼图上显示:篮球 43%
|
||||
percent,
|
||||
totalCount: item.totalCount,
|
||||
registeredCount: item.registeredCount
|
||||
};
|
||||
})
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 更新课程教师统计
|
||||
if (dashboard.teacherStats) {
|
||||
const teacher = dashboard.teacherStats;
|
||||
teacherChart.value = {
|
||||
series: [
|
||||
{
|
||||
name: '教师统计',
|
||||
data: [
|
||||
{
|
||||
name: '校内教师',
|
||||
value: teacher.insideCount || 0,
|
||||
label: `${teacher.insideCount || 0}人`
|
||||
},
|
||||
{
|
||||
name: '校外教师',
|
||||
value: teacher.outsideCount || 0,
|
||||
label: `${teacher.outsideCount || 0}人`
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 更新开课课时统计
|
||||
if (dashboard.courseHoursStats && dashboard.courseHoursStats.length > 0) {
|
||||
courseHoursChart.value = {
|
||||
categories: dashboard.courseHoursStats.map((item: any) => item.typeName),
|
||||
series: [
|
||||
{
|
||||
name: '开课课程数量',
|
||||
data: dashboard.courseHoursStats.map((item: any) => item.courseCount)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
} else {
|
||||
console.error('获取总览统计数据失败:', response);
|
||||
uni.showToast({
|
||||
title: response.message || '数据加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载总览统计数据异常:', error);
|
||||
uni.showToast({
|
||||
title: '网络异常,请稍后重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadOverviewData();
|
||||
});
|
||||
|
||||
// 监听xkId变化,重新加载数据
|
||||
watch(() => props.xkId, (newVal) => {
|
||||
if (newVal) {
|
||||
loadOverviewData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.overview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.overview-scroll {
|
||||
flex: 1;
|
||||
padding: 15px 0 20px 0;
|
||||
background-color: #f8f9fa;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
margin: 0 15px 15px;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #fafbfc;
|
||||
margin: -15px -15px 15px -15px;
|
||||
padding: 15px;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2933;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #3B82F6;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计卡片
|
||||
.summary-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
gap: 12px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
|
||||
&.stat-card-full {
|
||||
width: 100%;
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 无图标卡片样式
|
||||
&.stat-card-no-icon {
|
||||
.card-content {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.card-icon-text {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: keep-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图表容器
|
||||
.chart-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.chart-content-column {
|
||||
height: 320px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chart-instance {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
|
||||
.course-type-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.course-type-chart {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.course-type-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.course-type-legend-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f8faff;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
.course-type-legend-item .legend-name {
|
||||
font-size: 14px;
|
||||
color: #1f2933;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.legend-unit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: linear-gradient(135deg, #4F46E5 0%, #3B82F6 100%);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border-radius: 50%;
|
||||
margin-left: 2px;
|
||||
box-shadow: 0 2px 4px rgba(79, 70, 229, 0.2);
|
||||
}
|
||||
|
||||
.legend-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.legend-registered {
|
||||
font-size: 14px;
|
||||
color: #3b82f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.course-type-legend-item .legend-percent {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
// 教师统计内容
|
||||
.teacher-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.teacher-chart-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
height: 350px;
|
||||
margin: 0 auto;
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
|
||||
.chart-ring {
|
||||
width: 100% !important;
|
||||
height: 350px !important;
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
|
||||
.teacher-side {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.teacher-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
background: #f8faff;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
|
||||
.legend-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.legend-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-name {
|
||||
font-size: 14px;
|
||||
color: #1f2933;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.legend-value {
|
||||
font-size: 14px;
|
||||
color: #3b82f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 加载遮罩层
|
||||
.loading-mask {
|
||||
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: 9999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
509
src/pages/statistics/jlb/routine.vue
Normal file
509
src/pages/statistics/jlb/routine.vue
Normal file
@ -0,0 +1,509 @@
|
||||
<template>
|
||||
<view class="routine-container">
|
||||
<!-- 加载遮罩层 -->
|
||||
<view v-if="loading" class="loading-mask">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">数据加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="routine-scroll"
|
||||
@scroll="handleScroll"
|
||||
:scroll-top="scrollTop"
|
||||
:enable-back-to-top="true"
|
||||
>
|
||||
<view class="filter-bar">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">时间范围</text>
|
||||
<uni-datetime-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
:range-separator="'至'"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</view>
|
||||
<view class="filter-actions">
|
||||
<view class="filter-button primary" @click="loadRoutineData">查询</view>
|
||||
<view class="filter-button" @click="resetToCurrentWeek(true)">本周</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 维度5: 点名情况 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">点名情况</text>
|
||||
<text class="section-subtitle">按上课周期统计</text>
|
||||
</view>
|
||||
<view v-if="attendanceData.length" class="table-wrapper">
|
||||
<view class="table-row table-header">
|
||||
<text class="table-cell table-cell-week">上课周期</text>
|
||||
<text class="table-cell" v-for="(col, idx) in attendanceColumns" :key="idx">{{ col }}</text>
|
||||
</view>
|
||||
<view class="table-row" v-for="(week, index) in attendanceData" :key="index">
|
||||
<text class="table-cell table-cell-week">{{ week.weekName }}</text>
|
||||
<text class="table-cell" v-for="(metric, idx) in week.metrics" :key="idx">{{ metric.display }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-placeholder">暂无数据</view>
|
||||
</view>
|
||||
|
||||
<!-- 维度6: 缺勤情况 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">缺勤情况</text>
|
||||
<text class="section-subtitle">按上课周期统计</text>
|
||||
</view>
|
||||
<view v-if="absenceData.length" class="table-wrapper">
|
||||
<view class="table-row table-header">
|
||||
<text class="table-cell table-cell-week">上课周期</text>
|
||||
<text class="table-cell" v-for="(col, idx) in absenceColumns" :key="idx">{{ col }}</text>
|
||||
</view>
|
||||
<view class="table-row" v-for="(week, index) in absenceData" :key="index">
|
||||
<text class="table-cell table-cell-week">{{ week.weekName }}</text>
|
||||
<text class="table-cell" v-for="(metric, idx) in week.metrics" :key="idx">{{ metric.display }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-placeholder">暂无数据</view>
|
||||
</view>
|
||||
|
||||
<!-- 维度7: 巡查情况 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">巡查情况</text>
|
||||
<text class="section-subtitle">按上课周期统计</text>
|
||||
</view>
|
||||
<view v-if="inspectionData.length" class="table-wrapper">
|
||||
<view class="table-row table-header">
|
||||
<text class="table-cell table-cell-week">上课周期</text>
|
||||
<text class="table-cell" v-for="(col, idx) in inspectionColumns" :key="idx">{{ col }}</text>
|
||||
</view>
|
||||
<view class="table-row" v-for="(week, index) in inspectionData" :key="index">
|
||||
<text class="table-cell table-cell-week">{{ week.weekName }}</text>
|
||||
<text class="table-cell" v-for="(metric, idx) in week.metrics" :key="idx">{{ metric.display }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-placeholder">暂无数据</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { get } from '@/utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
xkId: string
|
||||
}>();
|
||||
|
||||
// 滚动优化
|
||||
const scrollTop = ref(0);
|
||||
const lastScrollTime = ref(0);
|
||||
const loading = ref(false);
|
||||
|
||||
// 滚动事件处理(节流优化)
|
||||
const handleScroll = (e: any) => {
|
||||
const now = Date.now();
|
||||
if (now - lastScrollTime.value < 100) {
|
||||
return;
|
||||
}
|
||||
lastScrollTime.value = now;
|
||||
};
|
||||
|
||||
// 维度5: 点名情况数据
|
||||
const attendanceData = ref<any[]>([]);
|
||||
|
||||
// 维度6: 缺勤情况数据
|
||||
const absenceData = ref<any[]>([]);
|
||||
|
||||
// 维度7: 巡查情况数据
|
||||
const inspectionData = ref<any[]>([]);
|
||||
|
||||
type DateRange = [string, string];
|
||||
const dateRange = ref<DateRange>(['', '']);
|
||||
const DATE_FORMAT = 'YYYY-MM-DD';
|
||||
|
||||
const getCurrentWeekRange = (): DateRange => {
|
||||
const today = dayjs();
|
||||
const monday = today.day() === 0 ? today.subtract(6, 'day') : today.subtract(today.day() - 1, 'day');
|
||||
const sunday = monday.add(6, 'day');
|
||||
return [monday.format(DATE_FORMAT), sunday.format(DATE_FORMAT)];
|
||||
};
|
||||
|
||||
const resetToCurrentWeek = (shouldLoad = false) => {
|
||||
dateRange.value = getCurrentWeekRange();
|
||||
if (shouldLoad) {
|
||||
loadRoutineData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDateChange = (value: any) => {
|
||||
if (Array.isArray(value) && value.length === 2) {
|
||||
dateRange.value = [value[0], value[1]] as DateRange;
|
||||
}
|
||||
};
|
||||
|
||||
const ensureDateRange = () => {
|
||||
const [start, end] = dateRange.value;
|
||||
if (!start || !end) {
|
||||
dateRange.value = getCurrentWeekRange();
|
||||
}
|
||||
};
|
||||
|
||||
const attendanceColumns = computed(() =>
|
||||
attendanceData.value.length
|
||||
? (attendanceData.value[0] as any).metrics.map((metric: any) => metric.label)
|
||||
: []
|
||||
);
|
||||
|
||||
const absenceColumns = computed(() =>
|
||||
absenceData.value.length
|
||||
? (absenceData.value[0] as any).metrics.map((metric: any) => metric.label)
|
||||
: []
|
||||
);
|
||||
|
||||
const inspectionColumns = computed(() =>
|
||||
inspectionData.value.length
|
||||
? (inspectionData.value[0] as any).metrics.map((metric: any) => metric.label)
|
||||
: []
|
||||
);
|
||||
|
||||
// 加载常规统计数据
|
||||
const loadRoutineData = async () => {
|
||||
if (!props.xkId) {
|
||||
console.error('缺少xkId参数');
|
||||
return;
|
||||
}
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
ensureDateRange();
|
||||
const [startDate, endDate] = dateRange.value;
|
||||
if (!startDate || !endDate) {
|
||||
uni.showToast({
|
||||
title: '请选择时间范围',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
console.log('加载俱乐部常规统计数据,xkId:', props.xkId, '范围:', startDate, '-', endDate);
|
||||
|
||||
const response = await get('/api/xkkc/statistics/clubRoutineDashboard', {
|
||||
xkId: props.xkId,
|
||||
startDate,
|
||||
endDate
|
||||
});
|
||||
|
||||
if (response.resultCode === 1 && response.result) {
|
||||
const dashboard = response.result;
|
||||
|
||||
// 1. 更新点名情况统计
|
||||
if (dashboard.attendanceStats && dashboard.attendanceStats.length > 0) {
|
||||
attendanceData.value = dashboard.attendanceStats.map((item: any) => ({
|
||||
weekName: item.studyTime,
|
||||
metrics: [
|
||||
{
|
||||
label: '已点名',
|
||||
value: item.attendedCourseCount || 0,
|
||||
display: `${item.attendedCourseCount || 0}课程`
|
||||
},
|
||||
{
|
||||
label: '未点名',
|
||||
value: item.unattendedCourseCount || 0,
|
||||
display: `${item.unattendedCourseCount || 0}课程`
|
||||
}
|
||||
]
|
||||
}));
|
||||
} else {
|
||||
attendanceData.value = [];
|
||||
}
|
||||
|
||||
// 2. 更新缺勤情况统计
|
||||
if (dashboard.absenceStats && dashboard.absenceStats.length > 0) {
|
||||
absenceData.value = dashboard.absenceStats.map((item: any) => ({
|
||||
weekName: item.studyTime,
|
||||
metrics: [
|
||||
{ label: '实到', value: item.actualCount || 0, display: `${item.actualCount || 0}人` },
|
||||
{ label: '请假', value: item.leaveCount || 0, display: `${item.leaveCount || 0}人` },
|
||||
{ label: '缺勤', value: item.absentCount || 0, display: `${item.absentCount || 0}人` },
|
||||
{ label: '迟到', value: item.lateCount || 0, display: `${item.lateCount || 0}人` }
|
||||
]
|
||||
}));
|
||||
} else {
|
||||
absenceData.value = [];
|
||||
}
|
||||
|
||||
// 3. 更新巡查情况统计
|
||||
if (dashboard.inspectionStats && dashboard.inspectionStats.length > 0) {
|
||||
inspectionData.value = dashboard.inspectionStats.map((item: any) => ({
|
||||
weekName: item.studyTime,
|
||||
metrics: [
|
||||
{
|
||||
label: '已巡查',
|
||||
value: item.inspectedCourseCount || 0,
|
||||
display: `${item.inspectedCourseCount || 0}课程`
|
||||
},
|
||||
{
|
||||
label: '未巡查',
|
||||
value: item.uninspectedCourseCount || 0,
|
||||
display: `${item.uninspectedCourseCount || 0}课程`
|
||||
}
|
||||
]
|
||||
}));
|
||||
} else {
|
||||
inspectionData.value = [];
|
||||
}
|
||||
|
||||
} else {
|
||||
console.error('获取常规统计数据失败:', response);
|
||||
uni.showToast({
|
||||
title: response.message || '数据加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载常规统计数据异常:', error);
|
||||
uni.showToast({
|
||||
title: '网络异常,请稍后重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
resetToCurrentWeek(false);
|
||||
if (props.xkId) {
|
||||
loadRoutineData();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听xkId变化,重新加载数据
|
||||
watch(() => props.xkId, (newVal) => {
|
||||
if (newVal) {
|
||||
loadRoutineData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.routine-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.routine-scroll {
|
||||
flex: 1;
|
||||
padding: 15px 0 20px 0;
|
||||
background-color: #f8f9fa;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin: 0 15px 15px;
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
flex: none;
|
||||
min-width: 80px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-button.primary {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
margin: 0 15px 15px;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #fafbfc;
|
||||
margin: -15px -15px 15px -15px;
|
||||
padding: 15px;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2933;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #3B82F6;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 周期网格布局
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f8faff;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: #eef2ff;
|
||||
font-weight: 600;
|
||||
color: #1f2933;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
flex: 1;
|
||||
padding: 12px 10px;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
text-align: center;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.table-cell-week {
|
||||
flex: 1.2;
|
||||
font-weight: 600;
|
||||
color: #1f2933;
|
||||
}
|
||||
|
||||
.empty-placeholder {
|
||||
width: 100%;
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
background: #f9fafb;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
// 加载遮罩层
|
||||
.loading-mask {
|
||||
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: 9999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
102
src/pages/statistics/zb/index.vue
Normal file
102
src/pages/statistics/zb/index.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<view class="zb-statistics-container">
|
||||
<!-- Tab 切换 -->
|
||||
<view class="tab-container">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'overview' }"
|
||||
@click="switchTab('overview')"
|
||||
>
|
||||
<text class="tab-text">总览</text>
|
||||
<view v-if="activeTab === 'overview'" class="tab-indicator"></view>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'report' }"
|
||||
@click="switchTab('report')"
|
||||
>
|
||||
<text class="tab-text">报告</text>
|
||||
<view v-if="activeTab === 'report'" class="tab-indicator"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-container">
|
||||
<OverviewComponent
|
||||
v-if="activeTab === 'overview'"
|
||||
/>
|
||||
<ReportComponent
|
||||
v-if="activeTab === 'report'"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import OverviewComponent from './overview.vue';
|
||||
import ReportComponent from './report.vue';
|
||||
|
||||
const activeTab = ref<'overview' | 'report'>('overview');
|
||||
|
||||
// 切换 Tab
|
||||
const switchTab = (tab: 'overview' | 'report') => {
|
||||
activeTab.value = tab;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.zb-statistics-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 15px;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
background: #667eea;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
683
src/pages/statistics/zb/overview.vue
Normal file
683
src/pages/statistics/zb/overview.vue
Normal file
@ -0,0 +1,683 @@
|
||||
<template>
|
||||
<view class="overview-container">
|
||||
<!-- 周次选择栏 -->
|
||||
<view class="week-selector" @click="openWeekPicker">
|
||||
<view class="week-selector-content">
|
||||
<text class="selector-label">统计周次:</text>
|
||||
<text class="selector-value">{{ selectedWeeksText }}</text>
|
||||
</view>
|
||||
<uni-icons type="bottom" size="14" color="#fff"></uni-icons>
|
||||
</view>
|
||||
|
||||
<!-- 加载遮罩层 -->
|
||||
<view v-if="loading" class="loading-mask">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">数据加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="overview-scroll"
|
||||
@scroll="handleScroll"
|
||||
:scroll-top="scrollTop"
|
||||
:enable-back-to-top="true"
|
||||
>
|
||||
<!-- 巡查情况透视表 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">巡查情况</text>
|
||||
<text class="section-subtitle">按日期统计各人员巡查情况</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!pivotData.rosterList || pivotData.rosterList.length === 0" class="empty-state">
|
||||
<text class="empty-icon">📋</text>
|
||||
<text class="empty-text">暂无巡查数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 透视表 -->
|
||||
<scroll-view v-else scroll-x class="table-scroll-x">
|
||||
<view class="pivot-table">
|
||||
<!-- 表头 -->
|
||||
<view class="table-header">
|
||||
<view class="name-column header-cell">姓名</view>
|
||||
<view class="scroll-column header-cell">值班位置</view>
|
||||
<view class="scroll-column header-cell">值班行政</view>
|
||||
<view class="scroll-column header-cell">值班区域</view>
|
||||
<view
|
||||
v-for="date in pivotData.dateList"
|
||||
:key="date"
|
||||
class="date-column header-cell"
|
||||
>
|
||||
{{ formatDateShort(date) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据行 -->
|
||||
<view
|
||||
v-for="(row, index) in pivotData.rosterList"
|
||||
:key="index"
|
||||
class="table-row"
|
||||
:class="{ 'row-odd': index % 2 === 1 }"
|
||||
>
|
||||
<view class="name-column data-cell">{{ row.jsxm }}</view>
|
||||
<view class="scroll-column data-cell">{{ row.zbwz }}</view>
|
||||
<view class="scroll-column data-cell">{{ row.zbjs || '-' }}</view>
|
||||
<view class="scroll-column data-cell">{{ row.zbqy }}</view>
|
||||
<view
|
||||
v-for="date in pivotData.dateList"
|
||||
:key="date"
|
||||
class="date-column data-cell"
|
||||
:class="{ 'cell-checked': row.inspectionMap[date] }"
|
||||
>
|
||||
<text class="check-icon">{{ row.inspectionMap[date] ? '✅' : '❌' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 周选择弹窗 -->
|
||||
<uni-popup ref="weekPopup" type="bottom" @change="popupChange">
|
||||
<view class="week-picker-popup">
|
||||
<view class="popup-header">
|
||||
<text class="title">选择周次</text>
|
||||
<view class="header-actions">
|
||||
<text class="action-btn close-btn" @click="closeWeekPicker">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="week-list">
|
||||
<scroll-view
|
||||
scroll-y
|
||||
style="max-height: 60vh"
|
||||
:scroll-into-view="scrollIntoViewId"
|
||||
>
|
||||
<view
|
||||
v-for="(zc, index) in zcList"
|
||||
:key="index"
|
||||
:id="'week-item-' + index"
|
||||
:class="['week-item', { selected: isWeekSelected(zc) }]"
|
||||
@click="toggleWeek(zc)"
|
||||
>
|
||||
<view class="week-info">
|
||||
<text class="week-name">{{ zc.mc }}</text>
|
||||
<text v-if="zc.dnDjz === dnDjz" class="current-tag">当前周</text>
|
||||
</view>
|
||||
<view v-if="isWeekSelected(zc)" class="check-icon">✓</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import { get } from '@/utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import { useCommonStore } from '@/store/modules/common';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(isoWeek);
|
||||
|
||||
const { getDqPk } = useCommonStore();
|
||||
|
||||
// 滚动优化
|
||||
const scrollTop = ref(0);
|
||||
const lastScrollTime = ref(0);
|
||||
const loading = ref(false);
|
||||
|
||||
// 周次相关
|
||||
const zcList = ref<any[]>([]);
|
||||
const selectedWeeks = ref<any[]>([]);
|
||||
const tempSelectedWeeks = ref<any[]>([]); // 临时选择,用于弹窗
|
||||
const dnDjz = ref(dayjs().isoWeek()); // 当年的第几周
|
||||
const weekPopup = ref<any>(null);
|
||||
const scrollIntoViewId = ref(''); // 滚动到指定周次的 ID
|
||||
|
||||
// 透视表数据
|
||||
const pivotData = ref<any>({
|
||||
dateList: [],
|
||||
rosterList: []
|
||||
});
|
||||
|
||||
// 选中的周次显示文本(单选模式)
|
||||
const selectedWeeksText = computed(() => {
|
||||
if (selectedWeeks.value.length === 0) {
|
||||
return '请选择周次';
|
||||
}
|
||||
|
||||
// 单选模式:直接显示选中的周次名称
|
||||
return selectedWeeks.value[0].mc;
|
||||
});
|
||||
|
||||
// 滚动事件处理(节流优化)
|
||||
const handleScroll = (e: any) => {
|
||||
const now = Date.now();
|
||||
if (now - lastScrollTime.value < 100) {
|
||||
return;
|
||||
}
|
||||
lastScrollTime.value = now;
|
||||
};
|
||||
|
||||
// 格式化日期(简短格式,如:11-03)
|
||||
const formatDateShort = (date: string) => {
|
||||
if (!date) return '';
|
||||
return date.substring(5); // "2025-11-03" -> "11-03"
|
||||
};
|
||||
|
||||
// 打开周次选择弹窗
|
||||
const openWeekPicker = async () => {
|
||||
if (!zcList.value || zcList.value.length === 0) {
|
||||
uni.showToast({
|
||||
title: '暂无周次数据',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 复制当前选择到临时选择
|
||||
tempSelectedWeeks.value = [...selectedWeeks.value];
|
||||
|
||||
await nextTick();
|
||||
if (weekPopup.value) {
|
||||
weekPopup.value.open('bottom');
|
||||
}
|
||||
|
||||
// 延迟滚动,等待弹窗完全打开后再滚动
|
||||
setTimeout(() => {
|
||||
// 查找当前周的索引(用于滚动)
|
||||
let currentWeekIndex = zcList.value.findIndex((item: any) => item.dnDjz === dnDjz.value);
|
||||
|
||||
if (currentWeekIndex === -1) {
|
||||
// 如果没找到,尝试找到第一个选中的周次
|
||||
if (selectedWeeks.value.length > 0) {
|
||||
currentWeekIndex = zcList.value.findIndex((item: any) => item.djz === selectedWeeks.value[0].djz);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentWeekIndex !== -1) {
|
||||
// 设置要滚动到的元素 ID
|
||||
scrollIntoViewId.value = 'week-item-' + currentWeekIndex;
|
||||
console.log('弹窗已打开,滚动到当前周:', scrollIntoViewId.value, '索引:', currentWeekIndex);
|
||||
}
|
||||
}, 300); // 延迟300ms,等待弹窗动画完成
|
||||
};
|
||||
|
||||
// 关闭周次选择弹窗
|
||||
const closeWeekPicker = () => {
|
||||
if (weekPopup.value) {
|
||||
weekPopup.value.close();
|
||||
}
|
||||
};
|
||||
|
||||
// 弹窗状态变化
|
||||
const popupChange = (e: { show: boolean }) => {
|
||||
if (!e.show) {
|
||||
// 弹窗关闭时,如果没有确认,恢复原来的选择
|
||||
tempSelectedWeeks.value = [...selectedWeeks.value];
|
||||
// 重置滚动位置
|
||||
scrollIntoViewId.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 判断周次是否被选中
|
||||
const isWeekSelected = (zc: any) => {
|
||||
return tempSelectedWeeks.value.some(w => w.djz === zc.djz);
|
||||
};
|
||||
|
||||
// 选择周次(单选模式,选中后自动关闭弹窗)
|
||||
const toggleWeek = (zc: any) => {
|
||||
// 单选模式:直接设置为选中的周次
|
||||
selectedWeeks.value = [zc];
|
||||
tempSelectedWeeks.value = [zc];
|
||||
|
||||
console.log('选择周次:', zc.mc);
|
||||
|
||||
// 关闭弹窗
|
||||
closeWeekPicker();
|
||||
|
||||
// 加载数据
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
if (!selectedWeeks.value || selectedWeeks.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 将选中的周次转换为日期范围
|
||||
const startDate = selectedWeeks.value[0].drList[0].rq; // 第一周的第一天
|
||||
const lastWeek = selectedWeeks.value[selectedWeeks.value.length - 1];
|
||||
const endDate = lastWeek.drList[lastWeek.drList.length - 1].rq; // 最后一周的最后一天
|
||||
const weeks = selectedWeeks.value.map(w => w.djz).join(',');
|
||||
|
||||
console.log('加载巡查情况透视表数据,周次:', selectedWeeks.value.map(w => w.mc).join(', '));
|
||||
console.log('日期范围:', startDate, '至', endDate);
|
||||
console.log('周次参数:', weeks);
|
||||
|
||||
// 加载透视表数据
|
||||
const response = await get('/api/pbZb/statistics/inspectionPivot', {
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
weeks: weeks
|
||||
});
|
||||
|
||||
if (response.resultCode === 1 && response.result) {
|
||||
pivotData.value = response.result;
|
||||
console.log('透视表数据加载成功,人员数:', pivotData.value.rosterList.length, '日期数:', pivotData.value.dateList.length);
|
||||
} else {
|
||||
pivotData.value = {
|
||||
dateList: [],
|
||||
rosterList: []
|
||||
};
|
||||
console.error('获取透视表数据失败:', response);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载巡查情况透视表数据异常:', error);
|
||||
uni.showToast({
|
||||
title: '网络异常,请稍后重试',
|
||||
icon: 'none'
|
||||
});
|
||||
pivotData.value = {
|
||||
dateList: [],
|
||||
rosterList: []
|
||||
};
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载周次列表
|
||||
const loadWeekList = async () => {
|
||||
try {
|
||||
const res = await getDqPk();
|
||||
const result = res.result;
|
||||
|
||||
zcList.value = result.zcList || [];
|
||||
dnDjz.value = dayjs().isoWeek();
|
||||
|
||||
console.log('周次列表加载成功:', zcList.value.length);
|
||||
console.log('当前ISO周数:', dnDjz.value);
|
||||
|
||||
// 默认选中当前周
|
||||
let currentWeek = zcList.value.find((item: any) => item.dnDjz === dnDjz.value);
|
||||
|
||||
if (!currentWeek) {
|
||||
console.log('根据ISO周数未找到当前周,使用后端返回的当前周次');
|
||||
const dqZc = result.zc;
|
||||
currentWeek = zcList.value.find((item: any) => item.djz == dqZc);
|
||||
}
|
||||
|
||||
if (!currentWeek && zcList.value.length > 0) {
|
||||
console.log('未找到当前周,使用第一个周次');
|
||||
currentWeek = zcList.value[0];
|
||||
}
|
||||
|
||||
if (currentWeek) {
|
||||
selectedWeeks.value = [currentWeek];
|
||||
tempSelectedWeeks.value = [currentWeek];
|
||||
loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载周次列表失败:', error);
|
||||
uni.showToast({
|
||||
title: '加载周次失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadWeekList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.overview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.week-selector {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 15px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.week-selector-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.selector-label {
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.selector-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.overview-scroll {
|
||||
flex: 1;
|
||||
padding: 0 0 20px 0;
|
||||
background-color: #f8f9fa;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
margin: 0 15px 15px;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #fafbfc;
|
||||
margin: -15px -15px 15px -15px;
|
||||
padding: 15px;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2933;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
// 透视表容器
|
||||
.table-scroll-x {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.pivot-table {
|
||||
display: table;
|
||||
min-width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-header,
|
||||
.table-row {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.header-cell,
|
||||
.data-cell {
|
||||
display: table-cell;
|
||||
padding: 10px 8px;
|
||||
text-align: center;
|
||||
border: 1px solid #e5e7eb;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-cell {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.data-cell {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 姓名列固定
|
||||
.name-column {
|
||||
min-width: 80px;
|
||||
width: 80px;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background: #fff;
|
||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.header-cell.name-column {
|
||||
z-index: 3;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
// 其他可滚动列
|
||||
.scroll-column {
|
||||
min-width: 90px;
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
// 日期列
|
||||
.date-column {
|
||||
min-width: 70px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.row-odd .data-cell {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.row-odd .name-column {
|
||||
background: #f9fafb;
|
||||
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.cell-checked {
|
||||
background: #f0fdf4 !important;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
// 加载遮罩层
|
||||
.loading-mask {
|
||||
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: 9999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top-color: #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 周选择弹窗样式 */
|
||||
.week-picker-popup {
|
||||
background-color: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding-bottom: 20px;
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
|
||||
.action-btn {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
|
||||
&.primary {
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.week-list {
|
||||
padding: 10px 0;
|
||||
|
||||
.week-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&.selected {
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
|
||||
.week-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.week-name {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.current-tag {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background-color: #667eea;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 18px;
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
395
src/pages/statistics/zb/pdfPreview.vue
Normal file
395
src/pages/statistics/zb/pdfPreview.vue
Normal file
@ -0,0 +1,395 @@
|
||||
<template>
|
||||
<view class="pdf-preview-container">
|
||||
<!-- PDF 内容区域(全屏显示) -->
|
||||
<view class="pdf-content" v-if="!loading && !error && pdfViewerUrl">
|
||||
<!-- 使用 web-view 加载 PDF.js 预览页 -->
|
||||
<web-view :src="pdfViewerUrl" @load="handleWebViewLoad" @error="handleWebViewError"></web-view>
|
||||
</view>
|
||||
|
||||
<!-- 调试信息(仅开发环境) -->
|
||||
<!-- <view v-if="!loading && !error && !pdfViewerUrl" class="debug-info">
|
||||
<text>调试:pdfViewerUrl为空</text>
|
||||
</view> -->
|
||||
|
||||
<!-- 加载中(全屏) -->
|
||||
<view v-if="loading" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">正在生成并加载PDF...</text>
|
||||
<text class="loading-hint">数据量较大,请耐心等待(最长20分钟)</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误提示(全屏) -->
|
||||
<view v-if="error" class="error-container">
|
||||
<text class="error-icon">⚠️</text>
|
||||
<text class="error-text">{{ errorMessage }}</text>
|
||||
<view class="retry-btn" @click="retryLoad">
|
||||
<text class="retry-text">重试</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { BASE_URL, AUTH_KEY } from '@/config';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const pdfViewerUrl = ref('');
|
||||
const pdfBlobUrl = ref(''); // 存储下载后的 blob URL
|
||||
|
||||
// 保存参数供重试使用
|
||||
const savedParams = ref({
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
weeks: ''
|
||||
});
|
||||
|
||||
// 获取页面参数
|
||||
onMounted(() => {
|
||||
let startDate = '';
|
||||
let endDate = '';
|
||||
let weeks = '';
|
||||
|
||||
// #ifdef H5
|
||||
// H5环境:从 sessionStorage 获取参数
|
||||
try {
|
||||
const paramsJson = sessionStorage.getItem('pdfPreviewParams');
|
||||
console.log('从 sessionStorage 读取参数:', paramsJson);
|
||||
|
||||
if (paramsJson) {
|
||||
const params = JSON.parse(paramsJson);
|
||||
startDate = params.startDate || '';
|
||||
endDate = params.endDate || '';
|
||||
weeks = params.weeks || '';
|
||||
|
||||
// 清除 sessionStorage,避免影响后续使用
|
||||
sessionStorage.removeItem('pdfPreviewParams');
|
||||
|
||||
console.log('H5环境,解析参数成功:', { startDate, endDate, weeks });
|
||||
} else {
|
||||
console.warn('sessionStorage 中没有找到参数,尝试从 URL 获取');
|
||||
// 备用方案:尝试从 URL 获取
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
startDate = urlParams.get('startDate') || '';
|
||||
endDate = urlParams.get('endDate') || '';
|
||||
weeks = urlParams.get('weeks') || '';
|
||||
console.log('从URL获取参数:', { startDate, endDate, weeks });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('解析参数失败:', err);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
// APP/小程序环境:从页面选项获取
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1] as any;
|
||||
const options = currentPage.options || {};
|
||||
startDate = options.startDate || '';
|
||||
endDate = options.endDate || '';
|
||||
weeks = options.weeks || '';
|
||||
console.log('APP环境,从页面选项获取参数:', { startDate, endDate, weeks });
|
||||
// #endif
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
error.value = true;
|
||||
errorMessage.value = '缺少必要参数: startDate=' + startDate + ', endDate=' + endDate;
|
||||
loading.value = false;
|
||||
console.error('参数缺失,无法加载PDF');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存参数供重试使用
|
||||
savedParams.value = {
|
||||
startDate,
|
||||
endDate,
|
||||
weeks
|
||||
};
|
||||
|
||||
// 构建 PDF.js 查看器 URL(异步执行)
|
||||
buildPdfViewerUrl(startDate, endDate, weeks).catch(err => {
|
||||
console.error('构建PDF查看器失败:', err);
|
||||
});
|
||||
});
|
||||
|
||||
// 构建 PDF 查看器 URL(先下载后预览,避免大文件超时)
|
||||
const buildPdfViewerUrl = async (startDate: string, endDate: string, weeks?: string) => {
|
||||
try {
|
||||
// PDF API URL
|
||||
const pdfApiUrl = `${BASE_URL}/api/pbZb/statistics/generateReportPdf?startDate=${startDate}&endDate=${endDate}${weeks ? '&weeks=' + weeks : ''}`;
|
||||
|
||||
console.log('开始下载PDF数据用于预览...');
|
||||
|
||||
// 设置20分钟超时保护
|
||||
let downloadTimeout: any = null;
|
||||
downloadTimeout = setTimeout(() => {
|
||||
if (loading.value) {
|
||||
console.warn('PDF下载超时(20分钟)');
|
||||
error.value = true;
|
||||
errorMessage.value = 'PDF加载超时,数据量较大,请稍后重试或尝试下载功能';
|
||||
loading.value = false;
|
||||
}
|
||||
}, 1200000); // 20分钟超时
|
||||
|
||||
// #ifdef H5
|
||||
// H5环境:使用fetch下载PDF到blob,然后传给PDF.js
|
||||
try {
|
||||
const response = await fetch(pdfApiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
[AUTH_KEY]: userStore.getToken
|
||||
}
|
||||
});
|
||||
|
||||
console.log('fetch响应状态:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`服务器返回错误: ${response.status}`);
|
||||
}
|
||||
|
||||
// 获取PDF blob数据
|
||||
console.log('开始接收PDF数据流...');
|
||||
const blob = await response.blob();
|
||||
console.log('PDF数据接收成功,大小:', (blob.size / 1024 / 1024).toFixed(2), 'MB');
|
||||
|
||||
// 清除超时定时器
|
||||
clearTimeout(downloadTimeout);
|
||||
|
||||
// 创建blob URL
|
||||
pdfBlobUrl.value = URL.createObjectURL(blob);
|
||||
console.log('Blob URL创建成功:', pdfBlobUrl.value);
|
||||
|
||||
// 使用项目中的 PDF.js 查看器,传入 blob URL
|
||||
const viewerPath = '/static/system/pdfReader/index.html';
|
||||
// 添加 #pagemode=none 来默认关闭左侧栏
|
||||
pdfViewerUrl.value = `${viewerPath}?file=${encodeURIComponent(pdfBlobUrl.value)}#pagemode=none`;
|
||||
|
||||
console.log('PDF查看器URL已生成,准备渲染');
|
||||
|
||||
// 等待 Vue 更新 DOM,然后关闭 loading
|
||||
await nextTick();
|
||||
console.log('Vue DOM 已更新,pdfViewerUrl 已应用到模板');
|
||||
|
||||
// 再延迟一点,确保 web-view 组件已挂载
|
||||
setTimeout(() => {
|
||||
console.log('准备关闭loading,状态检查:');
|
||||
console.log('- loading:', loading.value);
|
||||
console.log('- error:', error.value);
|
||||
console.log('- pdfViewerUrl:', pdfViewerUrl.value);
|
||||
|
||||
loading.value = false;
|
||||
|
||||
console.log('loading已设置为false,web-view应该显示');
|
||||
console.log('条件检查: !loading && !error && pdfViewerUrl =', !loading.value && !error.value && !!pdfViewerUrl.value);
|
||||
}, 500);
|
||||
|
||||
} catch (fetchError: any) {
|
||||
// 清除超时定时器
|
||||
clearTimeout(downloadTimeout);
|
||||
|
||||
console.error('H5环境下载PDF失败:', fetchError);
|
||||
error.value = true;
|
||||
errorMessage.value = '加载失败: ' + (fetchError?.message || '网络异常');
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
// APP/小程序环境:直接传递API URL给PDF.js(不需要预下载)
|
||||
const separator = pdfApiUrl.includes('?') ? '&' : '?';
|
||||
const pdfUrlWithToken = `${pdfApiUrl}${separator}${AUTH_KEY}=${encodeURIComponent(userStore.getToken)}`;
|
||||
|
||||
const viewerPath = '/static/system/pdfReader/index.html';
|
||||
pdfViewerUrl.value = `${viewerPath}?file=${encodeURIComponent(pdfUrlWithToken)}#pagemode=none`;
|
||||
|
||||
console.log('APP环境:PDF查看器URL已生成');
|
||||
|
||||
// 清除超时定时器
|
||||
clearTimeout(downloadTimeout);
|
||||
|
||||
// APP环境依赖 web-view 的 @load 事件来关闭 loading
|
||||
// #endif
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('构建PDF查看器URL失败:', err);
|
||||
error.value = true;
|
||||
errorMessage.value = '加载失败: ' + (err?.message || '未知错误');
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件销毁前清理资源
|
||||
onBeforeUnmount(() => {
|
||||
// 清理 blob URL,避免内存泄漏
|
||||
if (pdfBlobUrl.value) {
|
||||
URL.revokeObjectURL(pdfBlobUrl.value);
|
||||
console.log('Blob URL已释放');
|
||||
}
|
||||
});
|
||||
|
||||
// web-view 加载完成
|
||||
const handleWebViewLoad = (e: any) => {
|
||||
console.log('Web-view加载完成(PDF.js查看器已就绪):', e);
|
||||
|
||||
// #ifndef H5
|
||||
// APP环境下,在这里关闭loading(因为APP环境不走fetch下载流程)
|
||||
if (loading.value) {
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
console.log('APP环境:PDF.js查看器加载完成');
|
||||
}, 500);
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
// H5环境下,loading 已经在 blob 创建后关闭,这里仅记录日志
|
||||
console.log('H5环境:web-view已加载,PDF内容应该正常显示');
|
||||
// #endif
|
||||
};
|
||||
|
||||
// web-view 加载失败
|
||||
const handleWebViewError = (e: any) => {
|
||||
console.error('Web-view加载失败:', e);
|
||||
loading.value = false;
|
||||
error.value = true;
|
||||
errorMessage.value = 'PDF加载失败,请检查网络连接或重试';
|
||||
};
|
||||
|
||||
// 下载 PDF(已移除下载按钮,保留函数以防需要)
|
||||
// const downloadPdf = () => {
|
||||
// uni.showToast({
|
||||
// title: '请使用浏览器菜单下载',
|
||||
// icon: 'none',
|
||||
// duration: 2000
|
||||
// });
|
||||
// };
|
||||
|
||||
// 重试加载
|
||||
const retryLoad = async () => {
|
||||
error.value = false;
|
||||
errorMessage.value = '';
|
||||
loading.value = true;
|
||||
|
||||
if (!savedParams.value.startDate || !savedParams.value.endDate) {
|
||||
error.value = true;
|
||||
errorMessage.value = '缺少必要参数';
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await buildPdfViewerUrl(savedParams.value.startDate, savedParams.value.endDate, savedParams.value.weeks);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pdf-preview-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #525659;
|
||||
}
|
||||
|
||||
// PDF 内容(全屏显示)
|
||||
.pdf-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
// 确保触摸事件传递
|
||||
touch-action: manipulation;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
// 加载中(全屏显示)
|
||||
.loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #525659;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
// 错误提示(全屏显示)
|
||||
.error-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: #525659;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
padding: 10px 30px;
|
||||
background: #667eea;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.retry-text {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
1309
src/pages/statistics/zb/report.vue
Normal file
1309
src/pages/statistics/zb/report.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -78,9 +78,71 @@
|
||||
</view>
|
||||
|
||||
<!-- 接龙详情 -->
|
||||
<view class="details-card" v-if="activeTab === 'details'">
|
||||
<text class="card-title">接龙详情</text>
|
||||
<!-- 这里可以添加接龙详情内容 -->
|
||||
<view class="details-card" v-if="activeTab === 'details' && jlDetail">
|
||||
<!-- 封面 -->
|
||||
<view v-if="jlDetail.jlfm" class="detail-cover">
|
||||
<image :src="imagUrl(jlDetail.jlfm)" mode="aspectFill" class="cover-image"></image>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="detail-section">
|
||||
<text class="detail-title">{{ jlDetail.jlmc }}</text>
|
||||
|
||||
<view class="detail-meta">
|
||||
<view class="meta-row">
|
||||
<text class="meta-label">接龙类型:</text>
|
||||
<text class="meta-value">{{ jlDetail.jlType === 'select' ? '选择题接龙' : '普通接龙' }}</text>
|
||||
</view>
|
||||
<view class="meta-row">
|
||||
<text class="meta-label">发布人:</text>
|
||||
<text class="meta-value">{{ jlDetail.jsxm }}</text>
|
||||
</view>
|
||||
<view class="meta-row">
|
||||
<text class="meta-label">开始时间:</text>
|
||||
<text class="meta-value">{{ jlDetail.jlkstime }}</text>
|
||||
</view>
|
||||
<view class="meta-row">
|
||||
<text class="meta-label">结束时间:</text>
|
||||
<text class="meta-value">{{ jlDetail.jljstime }}</text>
|
||||
</view>
|
||||
<view class="meta-row">
|
||||
<text class="meta-label">是否签名:</text>
|
||||
<text class="meta-value">{{ jlDetail.mdqz === '1' ? '启用' : '不启用' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 接龙描述 -->
|
||||
<view class="detail-section" v-if="jlDetail.jlms">
|
||||
<text class="section-title">接龙描述</text>
|
||||
<rich-text :nodes="jlDetail.jlms" class="rich-content"></rich-text>
|
||||
</view>
|
||||
|
||||
<!-- 选项配置(选择题接龙) -->
|
||||
<view class="detail-section" v-if="jlDetail.jlType === 'select' && optionsList.length > 0">
|
||||
<text class="section-title">选项配置</text>
|
||||
<view class="options-list">
|
||||
<view v-for="(option, index) in optionsList" :key="index" class="option-item-detail">
|
||||
<text class="option-key">{{ option.key }}.</text>
|
||||
<text class="option-value">{{ option.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 回执说明 -->
|
||||
<view class="detail-section" v-if="jlDetail.mdqz === '1' && jlDetail.mdhz">
|
||||
<text class="section-title">回执说明</text>
|
||||
<text class="section-content">{{ jlDetail.mdhz }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 附件 -->
|
||||
<view class="detail-section" v-if="jlDetail.jlfj">
|
||||
<text class="section-title">附件</text>
|
||||
<view class="attachment-item" @click="previewAttachment(jlDetail.jlfj)">
|
||||
<uni-icons type="paperplane" size="16" color="#1890ff"></uni-icons>
|
||||
<text class="attachment-name">{{ getFileName(jlDetail.jlfj) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@ -89,7 +151,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getJlStatisticsApi } from '@/api/base/jlApi'
|
||||
import { getJlStatisticsApi, getByJlIdApi } from '@/api/base/jlApi'
|
||||
import { imagUrl } from '@/utils'
|
||||
|
||||
interface TotalStats {
|
||||
yjl: number; // 已接龙
|
||||
@ -112,10 +175,16 @@ const activeTab = ref('completion')
|
||||
const jlId = ref('')
|
||||
const totalStats = ref<TotalStats | null>(null)
|
||||
const gradeStats = ref<GradeStats[]>([])
|
||||
const jlDetail = ref<any>(null) // 接龙详情
|
||||
const optionsList = ref<any[]>([]) // 选项列表
|
||||
|
||||
// 切换标签页
|
||||
const switchTab = (tab: string) => {
|
||||
activeTab.value = tab
|
||||
// 切换到接龙详情时加载详情数据
|
||||
if (tab === 'details' && !jlDetail.value) {
|
||||
loadJlDetail()
|
||||
}
|
||||
}
|
||||
|
||||
// 计算完成率
|
||||
@ -148,6 +217,78 @@ const goToPersonnelList = (type: string, title: string, njId?: string, njmc?: st
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const getFileName = (filePath: string) => {
|
||||
if (!filePath) return ''
|
||||
const parts = filePath.split('/')
|
||||
return parts[parts.length - 1] || filePath
|
||||
}
|
||||
|
||||
// 预览附件
|
||||
const previewAttachment = (filePath: string) => {
|
||||
if (!filePath) {
|
||||
uni.showToast({ title: '附件链接无效', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const fullUrl = filePath.startsWith('http') ? filePath : imagUrl(filePath)
|
||||
const fileExtension = filePath.split('.').pop()?.toLowerCase()
|
||||
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExtension || '')
|
||||
|
||||
if (isImage) {
|
||||
uni.previewImage({
|
||||
urls: [fullUrl],
|
||||
current: fullUrl
|
||||
})
|
||||
} else {
|
||||
uni.downloadFile({
|
||||
url: fullUrl,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
uni.openDocument({
|
||||
filePath: res.tempFilePath,
|
||||
fail: () => {
|
||||
uni.showToast({ title: '无法打开该文件类型', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载接龙详情
|
||||
const loadJlDetail = async () => {
|
||||
try {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
|
||||
const result = await getByJlIdApi({ jlId: jlId.value })
|
||||
|
||||
if (result && Array.isArray(result) && result.length > 0) {
|
||||
jlDetail.value = result[0]
|
||||
|
||||
// 解析选项配置
|
||||
if (jlDetail.value.jlType === 'select' && jlDetail.value.jlOptions) {
|
||||
try {
|
||||
optionsList.value = JSON.parse(jlDetail.value.jlOptions)
|
||||
optionsList.value.sort((a, b) => (a.sort || 0) - (b.sort || 0))
|
||||
} catch (e) {
|
||||
console.error('解析选项配置失败:', e)
|
||||
optionsList.value = []
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载接龙详情失败:', error)
|
||||
uni.showToast({
|
||||
title: '加载详情失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStatisticsData = async () => {
|
||||
try {
|
||||
@ -182,9 +323,13 @@ const loadStatisticsData = async () => {
|
||||
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.jlId) {
|
||||
if (options && options.jlId) {
|
||||
jlId.value = options.jlId
|
||||
loadStatisticsData()
|
||||
// 如果默认标签是详情,也加载详情数据
|
||||
if (activeTab.value === 'details') {
|
||||
loadJlDetail()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -405,6 +550,132 @@ onLoad((options) => {
|
||||
&.danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #faad14;
|
||||
}
|
||||
}
|
||||
|
||||
/* 接龙详情样式 */
|
||||
.details-card {
|
||||
.detail-cover {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
.meta-row {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
|
||||
.meta-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rich-content {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
.option-item-detail {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
|
||||
.option-key {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.option-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
.attachment-name {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -115,12 +115,26 @@
|
||||
</view>
|
||||
|
||||
<view class="info-card list-item-card">
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="jlTypeOptions"
|
||||
@change="handleJlTypeChange"
|
||||
>
|
||||
<view class="list-item-row">
|
||||
<text class="list-label">接龙类型</text>
|
||||
<view class="list-value">
|
||||
<text>{{ jlTypeText }}</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="signatureOptions"
|
||||
@change="handleSignatureChange"
|
||||
>
|
||||
<view class="list-item-row">
|
||||
<view class="list-item-row no-border">
|
||||
<text class="list-label">按名单签字</text>
|
||||
<view class="list-value">
|
||||
<text>{{ signatureStatusText }}</text>
|
||||
@ -130,6 +144,48 @@
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 选项配置 -->
|
||||
<view class="info-card" v-if="formData.jlType === 'select'">
|
||||
<view class="form-item options-config-section">
|
||||
<view class="options-header">
|
||||
<text class="form-label">选项配置</text>
|
||||
<button class="add-option-btn" size="mini" @click="addOption">
|
||||
<uni-icons type="plusempty" size="14" color="#409eff"></uni-icons>
|
||||
<text>添加选项</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="optionsList.length === 0" class="empty-options">
|
||||
<text>暂无选项,请点击"添加选项"</text>
|
||||
</view>
|
||||
|
||||
<view v-for="(option, index) in optionsList" :key="index" class="option-item-mobile">
|
||||
<view class="option-header">
|
||||
<view class="option-key-sort">
|
||||
<text class="option-key-label">{{ option.key }}.</text>
|
||||
<text class="option-sort-label">序号: {{ option.sort }}</text>
|
||||
</view>
|
||||
<uni-icons
|
||||
type="closeempty"
|
||||
size="18"
|
||||
color="#f56c6c"
|
||||
@click="removeOption(index)"
|
||||
class="delete-icon"
|
||||
></uni-icons>
|
||||
</view>
|
||||
<uni-easyinput
|
||||
type="textarea"
|
||||
autoHeight
|
||||
v-model="option.value"
|
||||
placeholder="请输入选项内容"
|
||||
:inputBorder="false"
|
||||
class="option-input"
|
||||
@input="updateOptionsJson"
|
||||
></uni-easyinput>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 回执说明 -->
|
||||
<view class="info-card" v-if="formData.signatureRequired">
|
||||
<view class="form-item">
|
||||
@ -281,6 +337,15 @@ interface FormData {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
mdhz: string; // 回执说明
|
||||
jlType: string; // 接龙类型
|
||||
jlOptions: string; // 选项配置JSON
|
||||
}
|
||||
|
||||
// 选项类型
|
||||
interface JlOption {
|
||||
key: string;
|
||||
value: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
const noticeId = ref<string | null>(null);
|
||||
@ -306,13 +371,24 @@ const formData = reactive<FormData>({
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
mdhz: "", // 新增回执说明字段
|
||||
jlType: "normal", // 接龙类型,默认为普通接龙
|
||||
jlOptions: "", // 选项配置JSON
|
||||
});
|
||||
|
||||
// 选项列表
|
||||
const optionsList = ref<JlOption[]>([]);
|
||||
|
||||
const signatureOptions = ["启用" , "不启用"];
|
||||
const signatureStatusText = computed(() => {
|
||||
return formData.signatureRequired ? "启用" : "不启用";
|
||||
});
|
||||
|
||||
// 接龙类型选项
|
||||
const jlTypeOptions = ["普通接龙", "选择题接龙"];
|
||||
const jlTypeText = computed(() => {
|
||||
return formData.jlType === 'select' ? "选择题接龙" : "普通接龙";
|
||||
});
|
||||
|
||||
// 树形数据
|
||||
const treeData = ref([]);
|
||||
const treeRef = ref();
|
||||
@ -402,8 +478,13 @@ const resetForm = () => {
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
mdhz: "", // 重置回执说明
|
||||
jlType: "normal", // 重置接龙类型
|
||||
jlOptions: "", // 重置选项配置
|
||||
});
|
||||
|
||||
// 重置选项列表
|
||||
optionsList.value = [];
|
||||
|
||||
// 重置发布状态
|
||||
publishedJlId.value = null;
|
||||
isPublishing.value = false;
|
||||
@ -444,6 +525,19 @@ const loadJlData = async (jlId: string) => {
|
||||
formData.endTime = jlData.jljstime || "";
|
||||
formData.signatureRequired = jlData.mdqz === "1";
|
||||
formData.mdhz = jlData.mdhz || ""; // 加载回执说明
|
||||
formData.jlType = jlData.jlType || "normal"; // 加载接龙类型
|
||||
formData.jlOptions = jlData.jlOptions || ""; // 加载选项配置
|
||||
|
||||
// 解析选项配置
|
||||
if (formData.jlType === 'select' && formData.jlOptions) {
|
||||
try {
|
||||
optionsList.value = JSON.parse(formData.jlOptions);
|
||||
optionsList.value.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
||||
} catch (e) {
|
||||
console.error('解析选项配置失败:', e);
|
||||
optionsList.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 处理附件
|
||||
if (jlData.jlfj) {
|
||||
@ -802,6 +896,18 @@ const removeStudent = (index: number) => {
|
||||
uni.showToast({ title: "已移除学生", icon: "success" });
|
||||
};
|
||||
|
||||
// 接龙类型切换
|
||||
const handleJlTypeChange = (e: any) => {
|
||||
const index = e.detail.value;
|
||||
formData.jlType = index === 0 ? "normal" : "select";
|
||||
|
||||
// 切换到普通接龙时,清空选项
|
||||
if (formData.jlType === 'normal') {
|
||||
optionsList.value = [];
|
||||
formData.jlOptions = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignatureChange = (e: any) => {
|
||||
const index = e.detail.value;
|
||||
formData.signatureRequired = index == 0; // 修正:索引0是"启用",索引1是"不启用"
|
||||
@ -816,6 +922,37 @@ const handleSignatureChange = (e: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 添加选项
|
||||
const addOption = () => {
|
||||
const newKey = String.fromCharCode(65 + optionsList.value.length); // A, B, C...
|
||||
optionsList.value.push({
|
||||
key: newKey,
|
||||
value: '',
|
||||
sort: optionsList.value.length + 1
|
||||
});
|
||||
updateOptionsJson();
|
||||
};
|
||||
|
||||
// 删除选项
|
||||
const removeOption = (index: number) => {
|
||||
optionsList.value.splice(index, 1);
|
||||
// 重新生成选项标识和排序
|
||||
optionsList.value.forEach((option, idx) => {
|
||||
option.key = String.fromCharCode(65 + idx);
|
||||
option.sort = idx + 1;
|
||||
});
|
||||
updateOptionsJson();
|
||||
};
|
||||
|
||||
// 更新JSON字段
|
||||
const updateOptionsJson = () => {
|
||||
if (optionsList.value.length > 0) {
|
||||
formData.jlOptions = JSON.stringify(optionsList.value);
|
||||
} else {
|
||||
formData.jlOptions = "";
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
// 标题校验
|
||||
if (!formData.title || !formData.title.trim()) {
|
||||
@ -832,6 +969,20 @@ const validateForm = () => {
|
||||
uni.showToast({ title: "请选择年级与班级", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
// 选择题接龙必须配置选项
|
||||
if (formData.jlType === 'select') {
|
||||
if (optionsList.value.length === 0) {
|
||||
uni.showToast({ title: "选择题接龙至少需要添加一个选项", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
// 检查选项内容是否为空
|
||||
for (let i = 0; i < optionsList.value.length; i++) {
|
||||
if (!optionsList.value[i].value.trim()) {
|
||||
uni.showToast({ title: `选项${optionsList.value[i].key}的内容不能为空`, icon: "none" });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 开始时间不能为空
|
||||
if (!formData.startTime) {
|
||||
uni.showToast({ title: "请选择开始时间", icon: "none" });
|
||||
@ -893,6 +1044,12 @@ const buildJlDto = (status: string) => {
|
||||
console.log("targetNjmcIds.join(','):", formData.targetNjmcIds.join(","));
|
||||
console.log("==========================");
|
||||
|
||||
// 处理选项配置
|
||||
let jlOptions = "";
|
||||
if (formData.jlType === 'select' && optionsList.value.length > 0) {
|
||||
jlOptions = JSON.stringify(optionsList.value);
|
||||
}
|
||||
|
||||
return {
|
||||
id: formData.id || "", // 接龙ID,新增时为空
|
||||
jlmc: formData.title.trim(), // 接龙名称
|
||||
@ -903,6 +1060,8 @@ const buildJlDto = (status: string) => {
|
||||
jljstime: formatDate(formData.endTime), // 接龙结束时间,格式化为 yyyy-MM-dd HH:mm:ss
|
||||
mdqz: formData.signatureRequired ? "1" : "0", // 按名单签字:1启用,0不启用
|
||||
mdhz: formData.mdhz || "", // 回执说明
|
||||
jlType: formData.jlType, // 接龙类型
|
||||
jlOptions: jlOptions, // 选项配置JSON
|
||||
njId: formData.targetNjIds.join(","), // 关联年级ID列表
|
||||
njmcId: formData.targetNjmcIds.join(","), // 关联年级名称ID列表
|
||||
bjId: formData.targetBjIds.join(","), // 关联班级ID列表
|
||||
@ -1657,4 +1816,109 @@ watch(() => formData.title, (newTitle) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 选项配置区域 */
|
||||
.options-config-section {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: none !important;
|
||||
|
||||
.options-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.form-label {
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.add-option-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 12px;
|
||||
height: 28px;
|
||||
line-height: 20px;
|
||||
background-color: #ecf5ff;
|
||||
color: #409eff;
|
||||
border: 1px solid #b3d8ff;
|
||||
border-radius: 14px;
|
||||
font-size: 13px;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
text {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #d9ecff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-options {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.option-item-mobile {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.option-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.option-key-sort {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.option-key-label {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.option-sort-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.option-input {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.option-input ::v-deep .uni-easyinput__content-textarea {
|
||||
font-size: 14px !important;
|
||||
color: #303133 !important;
|
||||
padding: 8px !important;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
line-height: 1.5;
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -152,6 +152,7 @@ const loadTreeData = async () => {
|
||||
key: item.key,
|
||||
title: item.title,
|
||||
njmcId: item.njmcId,
|
||||
bc: item.bc,
|
||||
children: item.children ? convertTreeData(item.children) : [],
|
||||
}));
|
||||
};
|
||||
@ -176,12 +177,33 @@ const onTreeConfirm = (selectedItems: any[]) => {
|
||||
|
||||
// 如果选择的是班级(有parents表示是班级)
|
||||
if (selectedItem.parents && selectedItem.parents.length > 0) {
|
||||
const parent = selectedItem.parents[0]; // 年级信息
|
||||
const nj = parent; // 年级信息
|
||||
const bj = selectedItem; // 班级信息
|
||||
const parent = selectedItem.parents[0]; // 年级信息(只包含key和title)
|
||||
|
||||
curNj.value = nj;
|
||||
curBj.value = bj;
|
||||
// 从 treeData 中查找完整的年级数据
|
||||
const fullNj = treeData.value.find((item: any) => item.key === parent.key);
|
||||
|
||||
if (fullNj) {
|
||||
curNj.value = {
|
||||
key: fullNj.key,
|
||||
title: fullNj.title,
|
||||
njmcId: fullNj.njmcId || '' // 从完整数据中获取年级名称ID
|
||||
};
|
||||
curBj.value = {
|
||||
key: selectedItem.key,
|
||||
title: selectedItem.title,
|
||||
bc: `${fullNj.title}${selectedItem.title}` // 组合年级和班级名称,如"一年级3班"
|
||||
};
|
||||
|
||||
console.log('选择班级信息:', {
|
||||
curNj: curNj.value,
|
||||
curBj: curBj.value
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '获取年级信息失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 如果选择的是年级,清空班级选择
|
||||
curNj.value = selectedItem;
|
||||
@ -338,12 +360,23 @@ async function submit() {
|
||||
return;
|
||||
}
|
||||
|
||||
const submitData = {
|
||||
itemList: itemList,
|
||||
bjId: curBj.value.key,
|
||||
njId: curNj.value.key,
|
||||
njmcId: curNj.value.njmcId || '',
|
||||
bc: curBj.value.bc || curBj.value.title || ''
|
||||
};
|
||||
|
||||
console.log('提交数据:', submitData);
|
||||
|
||||
try {
|
||||
await evaluationSaveApi({
|
||||
itemList: itemList,
|
||||
bjId: curBj.value.key
|
||||
});
|
||||
await evaluationSaveApi(submitData);
|
||||
showToast({title: "操作成功"});
|
||||
|
||||
// 触发量化考核列表刷新事件
|
||||
uni.$emit('refreshEvaluationList');
|
||||
|
||||
// 重置选择
|
||||
curNj.value = null;
|
||||
curBj.value = null;
|
||||
|
||||
@ -2,14 +2,14 @@
|
||||
<BasicLayout>
|
||||
<view class="form-container">
|
||||
<!-- 用途 -->
|
||||
<view class="form-item">
|
||||
<view class="form-item form-item-row">
|
||||
<view class="label required">用途</view>
|
||||
<view class="radio-group">
|
||||
<view
|
||||
v-for="item in purposeOptions"
|
||||
:key="item.value"
|
||||
class="radio-item"
|
||||
@click="!isViewMode && (form.purpose = item.value)"
|
||||
@click="form.purpose = item.value"
|
||||
>
|
||||
<view class="radio" :class="{ active: form.purpose === item.value }">
|
||||
<view class="dot" v-if="form.purpose === item.value"></view>
|
||||
@ -20,27 +20,25 @@
|
||||
</view>
|
||||
|
||||
<!-- 年级班级 -->
|
||||
<view class="form-item">
|
||||
<view class="form-item form-item-row">
|
||||
<view class="label">年级班级</view>
|
||||
<view class="selector" @click="!isViewMode && openClassPicker()">
|
||||
<view class="selector" @click="openClassPicker()">
|
||||
<text :class="{ placeholder: !classText }">{{ classText || "请选择年级班级" }}</text>
|
||||
<uni-icons v-if="!isViewMode" type="right" size="16" color="#999"></uni-icons>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 学生 -->
|
||||
<view class="form-item">
|
||||
<view class="form-item form-item-row">
|
||||
<view class="label">学生</view>
|
||||
<BasicXsPicker
|
||||
v-if="!isViewMode"
|
||||
v-model="studentInfo"
|
||||
:njId="classInfo.njId"
|
||||
:bjId="classInfo.bjId"
|
||||
:placeholder="classInfo.bjId ? '请选择学生' : '请先选择年级班级'"
|
||||
:disabled="!classInfo.bjId"
|
||||
/>
|
||||
<view v-else class="selector">
|
||||
<text>{{ form.studentName || '未选择' }}</text>
|
||||
<view class="picker-wrapper">
|
||||
<BasicXsPicker
|
||||
v-model="studentInfo"
|
||||
:njId="classInfo.njId"
|
||||
:bjId="classInfo.bjId"
|
||||
:placeholder="classInfo.bjId ? '请选择学生' : '请先选择年级班级'"
|
||||
:disabled="!classInfo.bjId"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -51,20 +49,19 @@
|
||||
v-model="form.content"
|
||||
class="textarea"
|
||||
placeholder="请输入表扬内容"
|
||||
:disabled="isViewMode"
|
||||
:maxlength="500"
|
||||
></textarea>
|
||||
</view>
|
||||
|
||||
<!-- 附件类型 -->
|
||||
<view class="form-item">
|
||||
<view class="form-item form-item-row">
|
||||
<view class="label">附件类型</view>
|
||||
<view class="radio-group">
|
||||
<view
|
||||
v-for="item in fileTypeOptions"
|
||||
:key="item.value"
|
||||
class="radio-item"
|
||||
@click="!isViewMode && changeFileType(item.value)"
|
||||
@click="changeFileType(item.value)"
|
||||
>
|
||||
<view class="radio" :class="{ active: form.scfj === item.value }">
|
||||
<view class="dot" v-if="form.scfj === item.value"></view>
|
||||
@ -84,13 +81,12 @@
|
||||
:maxVideoCount="1"
|
||||
v-model:imageList="imageList"
|
||||
v-model:videoList="videoList"
|
||||
:disabled="isViewMode"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<template #bottom>
|
||||
<view v-if="!isViewMode" class="bottom-bar">
|
||||
<view class="bottom-bar">
|
||||
<u-button text="提交" type="primary" @click="handleSubmit"/>
|
||||
</view>
|
||||
</template>
|
||||
@ -125,6 +121,7 @@ const { getData } = useDataStore();
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
id: '', // 用于判断是新增还是修改
|
||||
purpose: '表扬栏',
|
||||
content: '',
|
||||
scfj: '照片',
|
||||
@ -151,9 +148,6 @@ const videoList = ref<VideoItem[]>([]);
|
||||
const treeData = ref<any[]>([]);
|
||||
const treeRef = ref();
|
||||
|
||||
// 是否查看模式
|
||||
const isViewMode = ref(false);
|
||||
|
||||
// 选项配置
|
||||
const purposeOptions = [
|
||||
{ value: '表扬栏', text: '表扬栏' },
|
||||
@ -246,16 +240,15 @@ const changeFileType = (type: string) => {
|
||||
form.value.scfj = type;
|
||||
};
|
||||
|
||||
// 初始化表单数据(查看模式)
|
||||
// 初始化表单数据(编辑模式回显)
|
||||
const initFormData = () => {
|
||||
if (!getData._show) {
|
||||
isViewMode.value = false;
|
||||
if (!getData || !getData.id) {
|
||||
// 没有数据,新增模式
|
||||
return;
|
||||
}
|
||||
|
||||
isViewMode.value = true;
|
||||
|
||||
// 基本信息
|
||||
// 有数据,编辑模式,回显数据
|
||||
form.value.id = getData.id || '';
|
||||
form.value.purpose = getData.purpose || '表扬栏';
|
||||
form.value.content = getData.content || '';
|
||||
form.value.scfj = getData.scfj || '照片';
|
||||
@ -343,6 +336,11 @@ const handleSubmit = async () => {
|
||||
pic: fileUrls.join(',')
|
||||
};
|
||||
|
||||
// 如果有 id,表示是修改操作,添加 id 到提交数据
|
||||
if (form.value.id) {
|
||||
submitData.id = form.value.id;
|
||||
}
|
||||
|
||||
// 添加班级信息(可选)
|
||||
if (classInfo.value.njId) {
|
||||
submitData.njId = classInfo.value.njId;
|
||||
@ -359,7 +357,11 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
await readyToGoSaveApi(submitData);
|
||||
showToast({ title: '操作成功' });
|
||||
showToast({ title: form.value.id ? '修改成功' : '新增成功' });
|
||||
|
||||
// 触发事件通知首页刷新随手拍列表
|
||||
uni.$emit('refreshSspList');
|
||||
|
||||
navigateBack({ delta: 1 });
|
||||
} catch (error) {
|
||||
showToast({ title: '提交失败', icon: 'error' });
|
||||
@ -383,6 +385,24 @@ onMounted(() => {
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
&.form-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
|
||||
.label {
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.selector,
|
||||
.picker-wrapper,
|
||||
.radio-group {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
@ -451,7 +471,6 @@ onMounted(() => {
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
|
||||
&.placeholder {
|
||||
color: #999;
|
||||
|
||||
@ -1,25 +1,47 @@
|
||||
<template>
|
||||
<view class="p-15">
|
||||
<view class="details-container">
|
||||
<BasicListLayout @register="register">
|
||||
<template v-slot="{data,index}">
|
||||
<view class="white-bg-color r-md p-15 mb-15 flex-row items-center">
|
||||
<view class="flex-1">
|
||||
<view class="font-w-500 flex-row">
|
||||
<view class="mr-5">{{ data.fullName }}</view>
|
||||
<view class="mr-5">{{ data.remark }}</view>
|
||||
<view v-if="data.roleName=='系统管理员'">{{ data.roleName }}{{ data.userName }}</view>
|
||||
<view class="evaluation-card">
|
||||
<!-- 头部信息 -->
|
||||
<view class="card-header">
|
||||
<view class="header-left">
|
||||
<view class="class-tag">{{ data.fullName }}</view>
|
||||
<view class="user-name" v-if="data.userName">{{ data.userName }}</view>
|
||||
</view>
|
||||
<template v-for="(arr,key) in data.items">
|
||||
<view class="font-14 font-w-500 mt-15">{{ key }}</view>
|
||||
<view class="font-14 mt-6" v-for="(item,index) in arr">
|
||||
{{ item.inspectStandard }},{{
|
||||
item.scoreType == 2 ? '扣' : '加'
|
||||
}}{{ Math.abs(item.score) }}分;
|
||||
</view>
|
||||
|
||||
<!-- 评价项目列表 -->
|
||||
<view class="items-container">
|
||||
<template v-for="(arr,key) in data.items" :key="key">
|
||||
<view class="item-category">
|
||||
<view class="category-title">
|
||||
<view class="category-left">
|
||||
<view class="category-icon"></view>
|
||||
<text>{{ key }}</text>
|
||||
</view>
|
||||
<view class="date-badge">{{ data.createdTime.split(" ")[0] }}</view>
|
||||
</view>
|
||||
<view class="category-items">
|
||||
<view
|
||||
class="item-row"
|
||||
v-for="(item,idx) in arr"
|
||||
:key="idx"
|
||||
:class="item.scoreType == 2 ? 'deduct' : 'add'"
|
||||
>
|
||||
<view class="item-content">
|
||||
<view class="item-index">{{ idx + 1 }}</view>
|
||||
<text class="item-text">{{ item.inspectStandard }}</text>
|
||||
</view>
|
||||
<view class="score-badge" :class="item.scoreType == 2 ? 'deduct-badge' : 'add-badge'">
|
||||
<text class="score-symbol">{{ item.scoreType == 2 ? '-' : '+' }}</text>
|
||||
<text class="score-value">{{ Math.abs(item.score) }}</text>
|
||||
<text class="score-unit">分</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view style="text-align: right">
|
||||
<view>{{ data.createdTime.split(" ")[0] }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@ -55,5 +77,235 @@ if (getData.value.bjId) {
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.details-container {
|
||||
padding: 20rpx;
|
||||
background: linear-gradient(to bottom, #f5f7fa 0%, #ffffff 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.evaluation-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片头部
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 24rpx 16rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.class-tag {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 6rpx 16rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
padding: 6rpx 16rpx;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(79, 172, 254, 0.3);
|
||||
}
|
||||
|
||||
.remark-text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-badge {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
padding: 6rpx 12rpx;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 检查人员信息
|
||||
.inspector-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
background: #fafbfc;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.inspector-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// 评价项目容器
|
||||
.items-container {
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.item-category {
|
||||
padding: 16rpx 24rpx;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.category-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 6rpx;
|
||||
height: 28rpx;
|
||||
background: linear-gradient(to bottom, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
|
||||
.category-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
// 评价项
|
||||
.item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx;
|
||||
background: #fafbfc;
|
||||
border-radius: 12rpx;
|
||||
border-left: 4rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.deduct {
|
||||
border-left-color: #ff4d4f;
|
||||
background: linear-gradient(to right, #fff1f0 0%, #fafbfc 100%);
|
||||
}
|
||||
|
||||
&.add {
|
||||
border-left-color: #52c41a;
|
||||
background: linear-gradient(to right, #f6ffed 0%, #fafbfc 100%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateX(4rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.item-index {
|
||||
flex-shrink: 0;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #e6e8eb;
|
||||
color: #666;
|
||||
border-radius: 50%;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// 分数徽章
|
||||
.score-badge {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 2rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
|
||||
&.deduct-badge {
|
||||
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
|
||||
}
|
||||
|
||||
&.add-badge {
|
||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.score-symbol {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.score-unit {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -28,18 +28,13 @@
|
||||
<view class="flex-1 tab-content-container">
|
||||
<!-- 量化考核 Tab -->
|
||||
<view v-show="current==0" class="tab-content">
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="loading-box">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="evaluationList.length === 0" class="empty-box">
|
||||
<view v-if="!loading && evaluationList.length === 0" class="empty-box">
|
||||
<text>暂无数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<view v-else>
|
||||
<view v-else-if="!loading">
|
||||
<view
|
||||
v-for="(data, index) in evaluationList"
|
||||
:key="index"
|
||||
@ -69,18 +64,13 @@
|
||||
|
||||
<!-- 随手拍 Tab -->
|
||||
<view v-show="current==1" class="tab-content">
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="sspLoading" class="loading-box">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="sspList.length === 0" class="empty-box">
|
||||
<view v-if="!sspLoading && sspList.length === 0" class="empty-box">
|
||||
<text>暂无数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<view v-else>
|
||||
<view v-else-if="!sspLoading">
|
||||
<view
|
||||
v-for="(data, index) in sspList"
|
||||
:key="index"
|
||||
@ -140,6 +130,14 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载遮罩层 -->
|
||||
<view v-if="loading || sspLoading" class="loading-overlay">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -154,7 +152,7 @@ import {
|
||||
readyToGoFindPageApi
|
||||
} from "@/api/base/assesment";
|
||||
import {imagUrl} from "@/utils";
|
||||
import {onShow} from "@dcloudio/uni-app";
|
||||
import {onShow, onUnload} from "@dcloudio/uni-app";
|
||||
import {useUserStore} from "@/store/modules/user";
|
||||
import Template from "@/components/BasicQrcode/_template/template.vue";
|
||||
import {useDataStore} from "@/store/modules/data";
|
||||
@ -239,6 +237,28 @@ async function loadSspData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理随手拍列表刷新
|
||||
const handleRefreshSspList = () => {
|
||||
// 如果当前在随手拍tab,立即刷新
|
||||
if (current.value === 1) {
|
||||
loadSspData();
|
||||
} else {
|
||||
// 如果不在随手拍tab,标记为未加载,下次切换时会重新加载
|
||||
tabLoaded.value.tab1 = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理量化考核列表刷新
|
||||
const handleRefreshEvaluationList = () => {
|
||||
// 如果当前在量化考核tab,立即刷新
|
||||
if (current.value === 0) {
|
||||
loadEvaluationData();
|
||||
} else {
|
||||
// 如果不在量化考核tab,标记为未加载,下次切换时会重新加载
|
||||
tabLoaded.value.tab0 = false;
|
||||
}
|
||||
};
|
||||
|
||||
onShow(async () => {
|
||||
let res = await inspectItemFindAllsApi();
|
||||
inspectItems.value = res.result;
|
||||
@ -255,8 +275,23 @@ onShow(async () => {
|
||||
} else {
|
||||
thirdId.value = "";
|
||||
}
|
||||
|
||||
// 先移除可能存在的旧监听,避免重复注册
|
||||
uni.$off('refreshSspList', handleRefreshSspList);
|
||||
uni.$off('refreshEvaluationList', handleRefreshEvaluationList);
|
||||
|
||||
// 监听随手拍刷新事件
|
||||
uni.$on('refreshSspList', handleRefreshSspList);
|
||||
// 监听量化考核刷新事件
|
||||
uni.$on('refreshEvaluationList', handleRefreshEvaluationList);
|
||||
})
|
||||
|
||||
// 页面卸载时移除事件监听
|
||||
onUnload(() => {
|
||||
uni.$off('refreshSspList', handleRefreshSspList);
|
||||
uni.$off('refreshEvaluationList', handleRefreshEvaluationList);
|
||||
});
|
||||
|
||||
|
||||
const [register, lhkh] = useLayout({
|
||||
api: evaluationFindPageSummaryApi,
|
||||
@ -452,4 +487,50 @@ function tabsChange(index: any) {
|
||||
width: 45rpx;
|
||||
height: 45rpx;
|
||||
}
|
||||
|
||||
/* 加载遮罩层 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
padding: 60rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border: 6rpx solid #f3f3f3;
|
||||
border-top: 6rpx solid #4F46E5;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 30rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,18 +2,13 @@
|
||||
<!-- <view :style="'width:'+height+'px;height:'+width+'px;transform-origin:'+(height/2)+'px '+(height/2)+'px'"-->
|
||||
<!-- style="transform: rotate(270deg);" class="p-15 flex-col"> -->
|
||||
<view class="flex-col wh-full">
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="flex-col-center wh-full">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="!dataLiat || dataLiat.length === 0" class="flex-col-center wh-full">
|
||||
<view v-if="!loading && (!dataLiat || dataLiat.length === 0)" class="flex-col-center wh-full">
|
||||
<text>暂无数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 数据展示 -->
|
||||
<view v-else class="page-container">
|
||||
<view v-else-if="!loading" class="page-container">
|
||||
<!-- 年级选项卡 -->
|
||||
<view class="grade-tabs">
|
||||
<view
|
||||
@ -74,6 +69,14 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载遮罩层 -->
|
||||
<view v-if="loading" class="loading-overlay">
|
||||
<view class="loading-content">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -101,7 +104,7 @@ function getGradeName(citem: any) {
|
||||
// 获取检查项基础分:baseScore * 5
|
||||
function getBaseScore(ditem: any) {
|
||||
const inspectItem = head.value.find(item => item.id === ditem.id);
|
||||
if (inspectItem && inspectItem.baseScore) {
|
||||
if (inspectItem && typeof inspectItem.baseScore === 'number') {
|
||||
return inspectItem.baseScore * 5;
|
||||
}
|
||||
return 0;
|
||||
@ -112,11 +115,13 @@ function calculateItemScore(bitem: any, ditem: any) {
|
||||
if (bitem.itemScoreMap && bitem.itemScoreMap[ditem.id]) {
|
||||
// 找到对应的检查项获取baseScore
|
||||
const inspectItem = head.value.find(item => item.id === ditem.id);
|
||||
if (inspectItem && inspectItem.baseScore) {
|
||||
|
||||
if (inspectItem && typeof inspectItem.baseScore === 'number') {
|
||||
const baseScore = inspectItem.baseScore * 5; // 基础分
|
||||
const deduction = bitem.itemScoreMap[ditem.id]; // 扣分项(负数)
|
||||
const finalScore = baseScore + deduction;
|
||||
// 得分 = baseScore * 5 - 扣分项
|
||||
return baseScore + deduction; // deduction是负数,所以用加法
|
||||
return finalScore; // deduction是负数,所以用加法
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
@ -128,7 +133,7 @@ function calculateTotalScore(bitem: any) {
|
||||
|
||||
// 遍历所有检查项(从head中获取)
|
||||
head.value.forEach(item => {
|
||||
if (item.id && item.baseScore) {
|
||||
if (item.id && typeof item.baseScore === 'number') {
|
||||
const baseScore = item.baseScore * 5; // 基础分
|
||||
let deduction = 0; // 扣分项
|
||||
|
||||
@ -469,4 +474,50 @@ getWeekData()
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 加载遮罩层 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
padding: 60rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border: 6rpx solid #f3f3f3;
|
||||
border-top: 6rpx solid #4F46E5;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 30rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
416
src/pages/view/routine/jc/components/completedList.vue
Normal file
416
src/pages/view/routine/jc/components/completedList.vue
Normal file
@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<view class="completed-list-content">
|
||||
<!-- 已点名列表 -->
|
||||
<view class="course-list" v-if="xkkcList && xkkcList.length > 0">
|
||||
<view
|
||||
v-for="(xkkc, index) in xkkcList"
|
||||
:key="xkkc.id || index"
|
||||
class="course-item"
|
||||
@click="goToDetail(xkkc)"
|
||||
>
|
||||
<!-- 状态标识 -->
|
||||
<view class="course-status status-done">
|
||||
已点名
|
||||
</view>
|
||||
<view class="course-name">{{ getCourseDisplayName(xkkc) }}</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">楼层:</view>
|
||||
<view class="info-data">{{ xkkc.xcdd || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">点名时间:</view>
|
||||
<view class="info-data">{{ getInspectionTime(xkkc) }}</view>
|
||||
</view>
|
||||
<view class="separator-line"></view>
|
||||
<view class="course-btn-group">
|
||||
<view class="recheck-btn" @click.stop="goToReDm(xkkc)">重新点名</view>
|
||||
<view class="detail-btn" @click.stop="goToDetail(xkkc)">查看详情</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 暂无数据提示 -->
|
||||
<view v-else-if="!loading" class="empty-course-list">
|
||||
<view class="empty-icon">
|
||||
<u-icon name="checkmark-circle" size="50" color="#67c23a"></u-icon>
|
||||
</view>
|
||||
<view class="empty-text">暂无已点名班级</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view v-if="loading" class="loading-overlay">
|
||||
<view class="loading-content">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getJcDmCourseListApi } from '@/api/base/pbApi'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useDataStore } from '@/store/modules/data'
|
||||
import dayjs from "dayjs"
|
||||
|
||||
const { getJs } = useUserStore()
|
||||
const dataStore = useDataStore()
|
||||
|
||||
// 已点名列表数据
|
||||
const xkkcList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 星期名称列表
|
||||
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||
|
||||
// 检查当前时间是否符合课程时间安排
|
||||
const isCurrentTimeMatch = (xkkc: any) => {
|
||||
const now = dayjs();
|
||||
let wDay = now.day();
|
||||
if (wDay === 0) {
|
||||
wDay = 7; // 将周日从0改为7
|
||||
}
|
||||
let mDay = now.date(); // 当前日期(1-31)
|
||||
|
||||
// 判断周期类型
|
||||
switch (xkkc.skzqlx) {
|
||||
case '每天':
|
||||
return true; // 每天都可以巡查
|
||||
case '每周':
|
||||
if (!xkkc.skzq) return false;
|
||||
const daysOfWeek = xkkc.skzq.split(',').map(Number);
|
||||
return daysOfWeek.includes(wDay);
|
||||
case '每月':
|
||||
if (!xkkc.skzq) return false;
|
||||
const daysOfMonth = xkkc.skzq.split(',').map(Number);
|
||||
return daysOfMonth.includes(mDay);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取课程显示名称(只显示年级和班级信息)
|
||||
const getCourseDisplayName = (xkkc: any) => {
|
||||
let gradeClassInfo = '';
|
||||
|
||||
if (xkkc.gradeName) {
|
||||
gradeClassInfo = xkkc.gradeName;
|
||||
}
|
||||
|
||||
if (xkkc.bjmc && xkkc.bjmc !== '暂无') {
|
||||
if (gradeClassInfo) {
|
||||
gradeClassInfo += `-${xkkc.bjmc}`;
|
||||
} else {
|
||||
gradeClassInfo = xkkc.bjmc;
|
||||
}
|
||||
}
|
||||
|
||||
return gradeClassInfo || '暂无信息';
|
||||
};
|
||||
|
||||
// 获取巡查时间显示文本
|
||||
const getInspectionTime = (xkkc: any) => {
|
||||
if (!xkkc.skzqlx || !xkkc.skzq) {
|
||||
return '暂无';
|
||||
}
|
||||
|
||||
switch (xkkc.skzqlx) {
|
||||
case '每天':
|
||||
return '每天';
|
||||
case '每周':
|
||||
if (!xkkc.skzq) return '每周';
|
||||
const daysOfWeek = xkkc.skzq.split(',').map(Number);
|
||||
const weekDays = daysOfWeek.map((day: number) => wdNameList[day - 1]).join('、');
|
||||
return `每周 ${weekDays}`;
|
||||
case '每月':
|
||||
if (!xkkc.skzq) return '每月';
|
||||
const daysOfMonth = xkkc.skzq.split(',').map(Number);
|
||||
const monthDays = daysOfMonth.map((day: number) => day + '号').join('、');
|
||||
return `每月 ${monthDays}`;
|
||||
default:
|
||||
return '暂无';
|
||||
}
|
||||
};
|
||||
|
||||
// 重新点名
|
||||
const goToReDm = (xkkc: any) => {
|
||||
dataStore.setData(xkkc);
|
||||
|
||||
uni.navigateTo({
|
||||
url: '/pages/view/routine/jc/dm'
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到详情页面
|
||||
const goToDetail = (xkkc: any) => {
|
||||
if (!xkkc.jcDmId) {
|
||||
uni.showToast({
|
||||
title: '未找到点名记录',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dataStore.setData({
|
||||
id: xkkc.jcDmId
|
||||
});
|
||||
|
||||
uni.navigateTo({
|
||||
url: '/pages/view/routine/jc/detail'
|
||||
});
|
||||
};
|
||||
|
||||
// 加载已点名列表
|
||||
const loadCompletedList = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const pbId = '1183821685246267392'; // 固定的 pbId
|
||||
|
||||
const res = await getJcDmCourseListApi({
|
||||
jsId: getJs.id,
|
||||
pbId: pbId
|
||||
});
|
||||
|
||||
if (res && res.resultCode == 1) {
|
||||
const list = res.result || [];
|
||||
|
||||
// 映射数据
|
||||
let mappedList = list.map((item: any) => ({
|
||||
id: item.jcDmId || item.id, // 使用点名记录ID作为主ID
|
||||
jcDmId: item.jcDmId || '', // 点名记录ID
|
||||
pbNjBjId: item.id || '', // 排班年级班级记录ID
|
||||
pbId: pbId,
|
||||
pbLxId: item.pbLxId || '1183783237328179200', // 排班类型ID
|
||||
kcmc: item.kmmc || '基础点名',
|
||||
gradeName: item.bc && item.njmc ? `${item.bc}(${item.njmc})` : (item.njmc || '暂无'),
|
||||
bjmc: item.bjmc || '暂无',
|
||||
njId: item.njId || '',
|
||||
njmcId: item.njmcId || '',
|
||||
bjId: item.bjId || '',
|
||||
skzqlx: item.skzqlx || '',
|
||||
skzq: item.skzq || '',
|
||||
sfxc: item.sfxc || '否',
|
||||
xcstime: item.xcstime || '',
|
||||
xcjstime: item.xcjstime || '',
|
||||
xcdd: item.xcdd || ''
|
||||
}));
|
||||
|
||||
// 过滤出当前时间符合的课程,且只显示已点名的(sfxc === '是')
|
||||
xkkcList.value = mappedList.filter((xkkc: any) =>
|
||||
isCurrentTimeMatch(xkkc) && xkkc.sfxc === '是'
|
||||
);
|
||||
} else {
|
||||
xkkcList.value = [];
|
||||
uni.showToast({
|
||||
title: (res as any).resultMessage || '获取已点名列表失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载已点名列表失败:', error);
|
||||
xkkcList.value = [];
|
||||
uni.showToast({
|
||||
title: '加载已点名列表失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadCompletedList();
|
||||
|
||||
// 监听刷新事件
|
||||
uni.$on('refreshDmList', () => {
|
||||
loadCompletedList();
|
||||
});
|
||||
});
|
||||
|
||||
// 组件卸载前移除事件监听
|
||||
onBeforeUnmount(() => {
|
||||
uni.$off('refreshDmList');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.completed-list-content {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 课程列表样式 */
|
||||
.course-list {
|
||||
.course-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
&:active {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
|
||||
border-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.course-status {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
|
||||
&.status-done {
|
||||
background: linear-gradient(135deg, #67c23a, #85ce61);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(103, 194, 58, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.course-name {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.4;
|
||||
padding-right: 80px;
|
||||
}
|
||||
|
||||
.course-info-item {
|
||||
display: flex;
|
||||
margin-bottom: 14px;
|
||||
font-size: 13px;
|
||||
align-items: flex-start;
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
flex: 0 0 auto;
|
||||
font-weight: 500;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.info-data {
|
||||
flex: 1 0 1px;
|
||||
color: #333;
|
||||
font-weight: 400;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.separator-line {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, #e8e8e8, transparent);
|
||||
margin: 18px 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.course-btn-group {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
|
||||
.recheck-btn {
|
||||
display: inline-block;
|
||||
color: #ff6b35;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, rgba(255, 107, 53, 0.1), rgba(255, 107, 53, 0.05));
|
||||
border: 1px solid #ff6b35;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.15);
|
||||
|
||||
&:active {
|
||||
background: linear-gradient(135deg, rgba(255, 107, 53, 0.2), rgba(255, 107, 53, 0.15));
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
display: inline-block;
|
||||
color: #67c23a;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, rgba(103, 194, 58, 0.1), rgba(103, 194, 58, 0.05));
|
||||
border: 1px solid #67c23a;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(103, 194, 58, 0.15);
|
||||
|
||||
&:active {
|
||||
background: linear-gradient(135deg, rgba(103, 194, 58, 0.2), rgba(103, 194, 58, 0.15));
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(103, 194, 58, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 暂无数据样式 */
|
||||
.empty-course-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 25px;
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
|
||||
.loading-content {
|
||||
background-color: #fff;
|
||||
padding: 40rpx;
|
||||
border-radius: 16rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
384
src/pages/view/routine/jc/components/pendingList.vue
Normal file
384
src/pages/view/routine/jc/components/pendingList.vue
Normal file
@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<view class="pending-list-content">
|
||||
<!-- 待点名列表 -->
|
||||
<view class="course-list" v-if="xkkcList && xkkcList.length > 0">
|
||||
<view
|
||||
v-for="(xkkc, index) in xkkcList"
|
||||
:key="xkkc.id || index"
|
||||
class="course-item"
|
||||
@click="goToDm(xkkc)"
|
||||
>
|
||||
<!-- 巡查状态标识 -->
|
||||
<view class="course-status" :class="xkkc.sfxc === '是' ? 'status-done' : 'status-pending'">
|
||||
{{ xkkc.sfxc === '是' ? '已点名' : '待点名' }}
|
||||
</view>
|
||||
<view class="course-name">{{ getCourseDisplayName(xkkc) }}</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">楼层:</view>
|
||||
<view class="info-data">{{ xkkc.xcdd || '暂无' }}</view>
|
||||
</view>
|
||||
<view class="course-info-item">
|
||||
<view class="info-label">点名时间:</view>
|
||||
<view class="info-data">{{ getInspectionTime(xkkc) }}</view>
|
||||
</view>
|
||||
<view class="separator-line"></view>
|
||||
<view class="course-btn-group">
|
||||
<view class="select-btn" @click.stop="goToDm(xkkc)">开始点名</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 暂无数据提示 -->
|
||||
<view v-else-if="!loading" class="empty-course-list">
|
||||
<view class="empty-icon">
|
||||
<u-icon name="list" size="50" color="#C8C9CC"></u-icon>
|
||||
</view>
|
||||
<view class="empty-text">暂无待点名班级</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view v-if="loading" class="loading-overlay">
|
||||
<view class="loading-content">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getJcDmCourseListApi } from '@/api/base/pbApi'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useDataStore } from '@/store/modules/data'
|
||||
import dayjs from "dayjs"
|
||||
|
||||
const { getJs } = useUserStore()
|
||||
const dataStore = useDataStore()
|
||||
|
||||
// 待点名列表数据
|
||||
const xkkcList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 星期名称列表
|
||||
const wdNameList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||
|
||||
// 检查当前时间是否符合课程时间安排
|
||||
const isCurrentTimeMatch = (xkkc: any) => {
|
||||
const now = dayjs();
|
||||
let wDay = now.day();
|
||||
if (wDay === 0) {
|
||||
wDay = 7; // 将周日从0改为7
|
||||
}
|
||||
let mDay = now.date(); // 当前日期(1-31)
|
||||
|
||||
// 判断周期类型
|
||||
switch (xkkc.skzqlx) {
|
||||
case '每天':
|
||||
return true; // 每天都可以巡查
|
||||
case '每周':
|
||||
if (!xkkc.skzq) return false;
|
||||
const daysOfWeek = xkkc.skzq.split(',').map(Number);
|
||||
return daysOfWeek.includes(wDay);
|
||||
case '每月':
|
||||
if (!xkkc.skzq) return false;
|
||||
const daysOfMonth = xkkc.skzq.split(',').map(Number);
|
||||
return daysOfMonth.includes(mDay);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取课程显示名称(只显示年级和班级信息)
|
||||
const getCourseDisplayName = (xkkc: any) => {
|
||||
let gradeClassInfo = '';
|
||||
|
||||
if (xkkc.gradeName) {
|
||||
gradeClassInfo = xkkc.gradeName;
|
||||
}
|
||||
|
||||
if (xkkc.bjmc && xkkc.bjmc !== '暂无') {
|
||||
if (gradeClassInfo) {
|
||||
gradeClassInfo += `-${xkkc.bjmc}`;
|
||||
} else {
|
||||
gradeClassInfo = xkkc.bjmc;
|
||||
}
|
||||
}
|
||||
|
||||
return gradeClassInfo || '暂无信息';
|
||||
};
|
||||
|
||||
// 获取巡查时间显示文本
|
||||
const getInspectionTime = (xkkc: any) => {
|
||||
if (!xkkc.skzqlx || !xkkc.skzq) {
|
||||
return '暂无';
|
||||
}
|
||||
|
||||
switch (xkkc.skzqlx) {
|
||||
case '每天':
|
||||
return '每天';
|
||||
case '每周':
|
||||
if (!xkkc.skzq) return '每周';
|
||||
const daysOfWeek = xkkc.skzq.split(',').map(Number);
|
||||
const weekDays = daysOfWeek.map((day: number) => wdNameList[day - 1]).join('、');
|
||||
return `每周 ${weekDays}`;
|
||||
case '每月':
|
||||
if (!xkkc.skzq) return '每月';
|
||||
const daysOfMonth = xkkc.skzq.split(',').map(Number);
|
||||
const monthDays = daysOfMonth.map((day: number) => day + '号').join('、');
|
||||
return `每月 ${monthDays}`;
|
||||
default:
|
||||
return '暂无';
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到点名页面
|
||||
const goToDm = (xkkc: any) => {
|
||||
// 将选中的班级信息存储到 dataStore
|
||||
dataStore.setData(xkkc);
|
||||
|
||||
// 跳转到点名页面(直接跳转到 dm 组件)
|
||||
uni.navigateTo({
|
||||
url: '/pages/view/routine/jc/dm'
|
||||
});
|
||||
};
|
||||
|
||||
// 加载待点名列表
|
||||
const loadPendingList = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const pbId = '1183821685246267392'; // 固定的 pbId
|
||||
|
||||
const res = await getJcDmCourseListApi({
|
||||
jsId: getJs.id,
|
||||
pbId: pbId
|
||||
});
|
||||
|
||||
if (res && res.resultCode == 1) {
|
||||
const list = res.result || [];
|
||||
|
||||
// 映射数据
|
||||
let mappedList = list.map((item: any) => ({
|
||||
id: item.id,
|
||||
jcDmId: item.jcDmId || '', // 点名记录ID
|
||||
pbId: pbId,
|
||||
pbLxId: item.pbLxId || '1183783237328179200', // 排班类型ID
|
||||
kcmc: item.kmmc || '基础点名',
|
||||
gradeName: item.bc && item.njmc ? `${item.bc}(${item.njmc})` : (item.njmc || '暂无'),
|
||||
bjmc: item.bjmc || '暂无',
|
||||
njId: item.njId || '',
|
||||
njmcId: item.njmcId || '',
|
||||
bjId: item.bjId || '',
|
||||
skzqlx: item.skzqlx || '',
|
||||
skzq: item.skzq || '',
|
||||
sfxc: item.sfxc || '否',
|
||||
xcstime: item.xcstime || '',
|
||||
xcjstime: item.xcjstime || '',
|
||||
xcdd: item.xcdd || ''
|
||||
}));
|
||||
|
||||
// 过滤出当前时间符合的课程,且只显示待点名的(sfxc === '否')
|
||||
xkkcList.value = mappedList.filter((xkkc: any) =>
|
||||
isCurrentTimeMatch(xkkc) && xkkc.sfxc === '否'
|
||||
);
|
||||
} else {
|
||||
xkkcList.value = [];
|
||||
uni.showToast({
|
||||
title: (res as any).resultMessage || '获取待点名列表失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载待点名列表失败:', error);
|
||||
xkkcList.value = [];
|
||||
uni.showToast({
|
||||
title: '加载待点名列表失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadPendingList();
|
||||
|
||||
// 监听刷新事件
|
||||
uni.$on('refreshDmList', () => {
|
||||
loadPendingList();
|
||||
});
|
||||
});
|
||||
|
||||
// 组件卸载前移除事件监听
|
||||
onBeforeUnmount(() => {
|
||||
uni.$off('refreshDmList');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pending-list-content {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 课程列表样式 */
|
||||
.course-list {
|
||||
.course-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
&:active {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
|
||||
border-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.course-status {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
|
||||
&.status-done {
|
||||
background: linear-gradient(135deg, #67c23a, #85ce61);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(103, 194, 58, 0.3);
|
||||
}
|
||||
|
||||
&.status-pending {
|
||||
background: linear-gradient(135deg, #e6a23c, #f0c78a);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(230, 162, 60, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.course-name {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.4;
|
||||
padding-right: 80px;
|
||||
}
|
||||
|
||||
.course-info-item {
|
||||
display: flex;
|
||||
margin-bottom: 14px;
|
||||
font-size: 13px;
|
||||
align-items: flex-start;
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
flex: 0 0 auto;
|
||||
font-weight: 500;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.info-data {
|
||||
flex: 1 0 1px;
|
||||
color: #333;
|
||||
font-weight: 400;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.separator-line {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, #e8e8e8, transparent);
|
||||
margin: 18px 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.course-btn-group {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
|
||||
.select-btn {
|
||||
display: inline-block;
|
||||
color: #667eea;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(102, 126, 234, 0.05));
|
||||
border: 1px solid #667eea;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
|
||||
|
||||
&:active {
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(102, 126, 234, 0.15));
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 暂无数据样式 */
|
||||
.empty-course-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
|
||||
.empty-icon {
|
||||
margin-bottom: 25px;
|
||||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
|
||||
.loading-content {
|
||||
background-color: #fff;
|
||||
padding: 40rpx;
|
||||
border-radius: 16rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
<template>
|
||||
<view class="start-dm-content">
|
||||
<!-- 班级选择器 -->
|
||||
<!-- 班级信息显示 -->
|
||||
<view class="section">
|
||||
<text class="section-title">选择班级</text>
|
||||
<view class="class-selector" @click="showClassTree">
|
||||
<text :class="{ placeholder: !selectedClassText }">{{ selectedClassText || "请选择班级" }}</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
<text class="section-title">点名班级</text>
|
||||
<view class="class-display">
|
||||
<text class="class-text">{{ selectedClassText || "未选择班级" }}</text>
|
||||
</view>
|
||||
<!-- 班级选择提示 -->
|
||||
<view v-if="!curBj" class="class-tip">
|
||||
<text class="tip-icon">ℹ️</text>
|
||||
<text class="tip-text">请先选择班级</text>
|
||||
<!-- 楼层信息显示 -->
|
||||
<view v-if="selectedXkkc && selectedXkkc.xcdd" class="floor-info">
|
||||
<text class="floor-label">楼层:</text>
|
||||
<text class="floor-value">{{ selectedXkkc.xcdd }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -77,17 +76,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import BasicTree from '@/components/BasicTree/Tree.vue'
|
||||
import DmJs from './dmJs.vue'
|
||||
import DmXs from './dmXs.vue'
|
||||
import DmJs from './components/dmJs.vue'
|
||||
import DmXs from './components/dmXs.vue'
|
||||
import { ImageVideoUpload, type ImageItem, type VideoItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload'
|
||||
import { submitJcDmDataApi } from '@/api/base/jcApi'
|
||||
import { findAllNjBjTree } from '@/api/base/server'
|
||||
import { attachmentUpload } from '@/api/system/upload'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useDataStore } from '@/store/modules/data'
|
||||
import { useDebounce } from "@/utils/debounce";
|
||||
import { useDebounce } from "@/utils/debounce"
|
||||
|
||||
const { getJs } = useUserStore()
|
||||
const { getJcBz } = useDataStore();
|
||||
const dataStore = useDataStore();
|
||||
const { getJcBz } = dataStore;
|
||||
|
||||
// 替换 isSubmitting 状态为 useDebounce
|
||||
const { isProcessing: isSubmitting, debounce } = useDebounce(2000);
|
||||
@ -109,6 +110,9 @@ const isLoading = ref(false);
|
||||
const curNj = ref<any>(null);
|
||||
const curBj = ref<any>(null);
|
||||
|
||||
// 从上一页接收的待巡查项
|
||||
const selectedXkkc = ref<any>(null);
|
||||
|
||||
// 计算属性:显示选中的班级文本
|
||||
const selectedClassText = computed(() => {
|
||||
if (curBj.value && curNj.value) {
|
||||
@ -220,6 +224,8 @@ const tjDm = debounce(async () => {
|
||||
njId: curNj.value.key,
|
||||
bjmc: curBj.value.title,
|
||||
njmc: curNj.value.title,
|
||||
njmcId: curNj.value.njmcId || '', // 年级名称ID
|
||||
pbLxId: selectedXkkc.value?.pbLxId || '1183783237328179200', // 排班类型ID
|
||||
jsId: getJs.id || '', // 点名教师ID
|
||||
pcRs: dmJsList.length,
|
||||
zrs: dmXsList.length,
|
||||
@ -263,13 +269,25 @@ const tjDm = debounce(async () => {
|
||||
const response = await submitJcDmDataApi(dmData);
|
||||
uni.hideLoading();
|
||||
if (response.result) {
|
||||
// 重置表单
|
||||
curNj.value = null
|
||||
curBj.value = null
|
||||
imageList.value = []
|
||||
videoList.value = []
|
||||
// 返回上一页
|
||||
uni.navigateBack()
|
||||
// 发送刷新事件
|
||||
uni.$emit('refreshDmList');
|
||||
|
||||
uni.showToast({
|
||||
title: '提交成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
});
|
||||
|
||||
// 延迟返回,确保刷新事件被接收
|
||||
setTimeout(() => {
|
||||
// 重置表单
|
||||
curNj.value = null
|
||||
curBj.value = null
|
||||
imageList.value = []
|
||||
videoList.value = []
|
||||
// 返回上一页
|
||||
uni.navigateBack()
|
||||
}, 500);
|
||||
} else {
|
||||
throw new Error(response.message || '提交失败')
|
||||
}
|
||||
@ -282,9 +300,50 @@ const tjDm = debounce(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 从上一页传递的数据中自动设置班级
|
||||
const loadFromPreviousPage = () => {
|
||||
// 从 dataStore 中获取传递的数据
|
||||
const xkkcData = dataStore.getData;
|
||||
|
||||
if (xkkcData && xkkcData.njId && xkkcData.bjId) {
|
||||
selectedXkkc.value = xkkcData;
|
||||
|
||||
// 从树形数据中找到对应的年级和班级
|
||||
const findNjBj = (items: any[]): any => {
|
||||
for (const item of items) {
|
||||
if (item.key === xkkcData.njId) {
|
||||
// 找到年级
|
||||
curNj.value = item;
|
||||
// 在年级的子节点中找班级
|
||||
if (item.children) {
|
||||
for (const child of item.children) {
|
||||
if (child.key === xkkcData.bjId) {
|
||||
curBj.value = child;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// 递归查找
|
||||
if (item.children && item.children.length > 0) {
|
||||
if (findNjBj(item.children)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
findNjBj(treeData.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadTreeData();
|
||||
onMounted(async () => {
|
||||
await loadTreeData();
|
||||
// 从上一页加载数据并自动设置班级
|
||||
loadFromPreviousPage();
|
||||
});
|
||||
|
||||
</script>
|
||||
@ -293,13 +352,15 @@ onMounted(() => {
|
||||
.start-dm-content {
|
||||
padding: 20rpx;
|
||||
padding-bottom: 120rpx; /* 为固定底部按钮留出空间 */
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
margin: 0 20rpx 20rpx 20rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
@ -313,47 +374,42 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.class-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
/* 班级信息显示 */
|
||||
.class-display {
|
||||
padding: 20rpx 30rpx;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border: 1rpx solid #e9ecef;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12rpx;
|
||||
margin-top: 20rpx;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
|
||||
&.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
|
||||
.class-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.class-tip {
|
||||
/* 楼层信息显示 */
|
||||
.floor-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 20rpx;
|
||||
padding: 15rpx 20rpx;
|
||||
background-color: #fffbe6;
|
||||
border: 1rpx solid #ffe58f;
|
||||
background-color: #f0f7ff;
|
||||
border: 1rpx solid #d4e6ff;
|
||||
border-radius: 12rpx;
|
||||
color: #faad14;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
|
||||
.tip-icon {
|
||||
.floor-label {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
margin-right: 10rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.floor-value {
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,4 +460,5 @@ onMounted(() => {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -4,25 +4,25 @@
|
||||
<view class="tab-container">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'start' }"
|
||||
@click="switchTab('start')"
|
||||
:class="{ active: activeTab === 'pending' }"
|
||||
@click="switchTab('pending')"
|
||||
>
|
||||
开始点名
|
||||
待点名
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'record' }"
|
||||
@click="switchTab('record')"
|
||||
:class="{ active: activeTab === 'completed' }"
|
||||
@click="switchTab('completed')"
|
||||
>
|
||||
点名记录
|
||||
已点名
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 开始点名内容 -->
|
||||
<DmComponent v-if="activeTab === 'start'" />
|
||||
<!-- 待点名内容 -->
|
||||
<PendingListComponent v-if="activeTab === 'pending'" />
|
||||
|
||||
<!-- 点名记录内容 -->
|
||||
<DmListComponent v-if="activeTab === 'record'" />
|
||||
<!-- 已点名记录内容 -->
|
||||
<CompletedListComponent v-if="activeTab === 'completed'" />
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view v-if="loading" class="loading-overlay">
|
||||
@ -35,11 +35,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DmComponent from './components/dm.vue'
|
||||
import DmListComponent from './components/dmList.vue'
|
||||
import PendingListComponent from './components/pendingList.vue'
|
||||
import CompletedListComponent from './components/completedList.vue'
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('start')
|
||||
const activeTab = ref('pending')
|
||||
const loading = ref(false)
|
||||
|
||||
// 方法
|
||||
|
||||
@ -57,18 +57,18 @@
|
||||
<text style="margin-right: 8px;">{{ idx + 1 }}、{{ xm.xcMc }}</text>
|
||||
<view style="display: flex; align-items: center;">
|
||||
<u-icon
|
||||
:name="xm.xcJg === 'A' ? 'checkmark-circle-fill' : 'close-circle-fill'"
|
||||
:color="xm.xcJg === 'A' ? '#67c23a' : '#f56c6c'"
|
||||
:name="xm.xcJg === 'A' ? 'close-circle-fill' : 'checkmark-circle-fill'"
|
||||
:color="xm.xcJg === 'A' ? '#f56c6c' : '#67c23a'"
|
||||
size="18"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view style="font-size: 12px; color: #666;">
|
||||
<text v-if="xm.xcJg === 'A'" style="color: #67c23a;">
|
||||
优点
|
||||
<text v-if="xm.xcJg === 'A'" style="color: #f56c6c;">
|
||||
有
|
||||
</text>
|
||||
<text v-else style="color: #f56c6c;">
|
||||
缺点
|
||||
<text v-else style="color: #67c23a;">
|
||||
无
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -136,8 +136,10 @@ const loadPbList = async (isRefresh = false) => {
|
||||
// 根据实际API响应结构调整数据获取逻辑
|
||||
const list = res.rows || res.result?.rows || res.result?.list || [];
|
||||
|
||||
// ✅ 过滤数据:只保留课程巡查(A)和课业辅导巡查(B),排除值周巡查(C)
|
||||
const filteredList = list.filter((item: any) => item.xclx === 'A' || item.xclx === 'B');
|
||||
// ✅ 过滤数据:只保留课程巡查(A)和课业辅导巡查(B),排除值周巡查(C)和指定排课类型
|
||||
const filteredList = list.filter((item: any) =>
|
||||
(item.xclx === 'A' || item.xclx === 'B') && item.pbLxId !== '1183783237328179200'
|
||||
);
|
||||
|
||||
console.log('原始数据数量:', list.length, '过滤后数据数量:', filteredList.length);
|
||||
console.log('过滤掉的值周巡查(C)数量:', list.length - filteredList.length);
|
||||
|
||||
@ -55,18 +55,18 @@
|
||||
<text style="margin-right: 8px;">{{ idx + 1 }}、{{ xm.xcMc }}</text>
|
||||
<view style="display: flex; align-items: center;">
|
||||
<u-icon
|
||||
:name="xm.xcJg === 'A' ? 'checkmark-circle-fill' : 'close-circle-fill'"
|
||||
:color="xm.xcJg === 'A' ? '#67c23a' : '#f56c6c'"
|
||||
:name="xm.xcJg === 'A' ? 'close-circle-fill' : 'checkmark-circle-fill'"
|
||||
:color="xm.xcJg === 'A' ? '#f56c6c' : '#67c23a'"
|
||||
size="18"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view style="font-size: 12px; color: #666;">
|
||||
<text v-if="xm.xcJg === 'A'" style="color: #67c23a;">
|
||||
优点
|
||||
<text v-if="xm.xcJg === 'A'" style="color: #f56c6c;">
|
||||
有
|
||||
</text>
|
||||
<text v-else style="color: #f56c6c;">
|
||||
缺点
|
||||
<text v-else style="color: #67c23a;">
|
||||
无
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -86,45 +86,61 @@
|
||||
<text class="item-text"
|
||||
>{{ index + 1 }}、{{ item.xcMc }}</text
|
||||
>
|
||||
<!-- 分值和结果同一行 -->
|
||||
<!-- 分值和操作按钮同一行 -->
|
||||
<view class="item-score-result">
|
||||
<text class="item-deduction mr-20">
|
||||
分值:{{ item.xmFz }}分
|
||||
</text>
|
||||
<view class="item-result">
|
||||
<radio-group
|
||||
:name="'result_' + item.id"
|
||||
class="item-radio-group"
|
||||
@change="onCheckItemChange($event, item)"
|
||||
>
|
||||
<label class="item-radio-label mr-10">
|
||||
<radio
|
||||
:value="'A'"
|
||||
:checked="item.xcJg === 'A'"
|
||||
color="#52c41a"
|
||||
class="item-radio"
|
||||
/>
|
||||
<text class="ml-2">优点</text>
|
||||
</label>
|
||||
<label class="item-radio-label">
|
||||
<radio
|
||||
:value="'B'"
|
||||
:checked="item.xcJg === 'B'"
|
||||
color="#ff4d4f"
|
||||
class="item-radio"
|
||||
/>
|
||||
<text class="ml-2">缺点</text>
|
||||
</label>
|
||||
</radio-group>
|
||||
<view class="item-action-btns">
|
||||
<button class="action-btn advantage-btn" @click="addComment(item, 'A')">
|
||||
<u-icon name="plus" size="12" color="#2e7d32"></u-icon>
|
||||
<text class="btn-text">优点</text>
|
||||
</button>
|
||||
<button class="action-btn disadvantage-btn" @click="addComment(item, 'B')">
|
||||
<u-icon name="plus" size="12" color="#c62828"></u-icon>
|
||||
<text class="btn-text">缺点</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 显示输入的评价内容 -->
|
||||
<view v-if="item.xcPj" class="item-comment" @click="editComment(item)">
|
||||
<view class="comment-header">
|
||||
<text class="comment-label">{{ item.xcJg === 'A' ? '优点' : '缺点' }}:</text>
|
||||
<text class="comment-edit-hint">点击编辑</text>
|
||||
|
||||
<!-- 显示已添加的优点 -->
|
||||
<view v-if="item.advantages && item.advantages.length > 0" class="comment-list">
|
||||
<view class="comment-list-title">优点:</view>
|
||||
<view
|
||||
v-for="(adv, advIndex) in item.advantages"
|
||||
:key="advIndex"
|
||||
class="comment-item advantage-item"
|
||||
>
|
||||
<text class="comment-text">{{ advIndex + 1 }}. {{ adv }}</text>
|
||||
<view class="item-actions">
|
||||
<view class="edit-btn" @click.stop="editComment(item, 'A', advIndex)">
|
||||
<u-icon name="edit-pen" size="14" color="#1976d2"></u-icon>
|
||||
</view>
|
||||
<view class="delete-btn" @click.stop="deleteComment(item, 'A', advIndex)">
|
||||
<u-icon name="close-circle-fill" size="16" color="#999"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 显示已添加的缺点 -->
|
||||
<view v-if="item.disadvantages && item.disadvantages.length > 0" class="comment-list">
|
||||
<view class="comment-list-title">缺点:</view>
|
||||
<view
|
||||
v-for="(dis, disIndex) in item.disadvantages"
|
||||
:key="disIndex"
|
||||
class="comment-item disadvantage-item"
|
||||
>
|
||||
<text class="comment-text">{{ disIndex + 1 }}. {{ dis }}</text>
|
||||
<view class="item-actions">
|
||||
<view class="edit-btn" @click.stop="editComment(item, 'B', disIndex)">
|
||||
<u-icon name="edit-pen" size="14" color="#1976d2"></u-icon>
|
||||
</view>
|
||||
<view class="delete-btn" @click.stop="deleteComment(item, 'B', disIndex)">
|
||||
<u-icon name="close-circle-fill" size="16" color="#999"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="comment-text">{{ item.xcPj }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -298,6 +314,7 @@ const inputContent = ref('');
|
||||
const currentInputLabel = ref('');
|
||||
const currentInputValue = ref('');
|
||||
const currentItem = ref<any>(null);
|
||||
const currentEditIndex = ref<number>(-1); // 当前编辑的索引,-1表示新增
|
||||
|
||||
// 格式化时间(只显示日期,不显示时和分)
|
||||
const formatTime = (timestamp: string) => {
|
||||
@ -378,8 +395,8 @@ const loadCheckItems = async () => {
|
||||
checkItems.value = (res.result || []).map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
xcJg: '', // 巡查结果:A-优点,B-缺点
|
||||
xcPj: '', // 巡查评价内容
|
||||
advantages: [], // 优点列表
|
||||
disadvantages: [], // 缺点列表
|
||||
};
|
||||
});
|
||||
console.log('巡查项目列表:', checkItems.value);
|
||||
@ -392,28 +409,48 @@ const loadCheckItems = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onCheckItemChange = (e: any, item: any) => {
|
||||
const value = e.detail.value; // 'A' 或 'B'
|
||||
const label = value === 'A' ? '优点' : '缺点';
|
||||
// 添加优点或缺点
|
||||
const addComment = (item: any, type: string) => {
|
||||
const label = type === 'A' ? '优点' : '缺点';
|
||||
|
||||
// 打开自定义输入弹窗
|
||||
currentItem.value = item;
|
||||
currentInputValue.value = value;
|
||||
currentInputValue.value = type;
|
||||
currentInputLabel.value = label;
|
||||
inputContent.value = item.xcPj || '';
|
||||
currentEditIndex.value = -1; // -1表示新增
|
||||
inputContent.value = '';
|
||||
showInputModal.value = true;
|
||||
};
|
||||
|
||||
// 编辑优点或缺点
|
||||
const editComment = (item: any, type: string, index: number) => {
|
||||
const label = type === 'A' ? '优点' : '缺点';
|
||||
const content = type === 'A' ? item.advantages[index] : item.disadvantages[index];
|
||||
|
||||
currentItem.value = item;
|
||||
currentInputValue.value = type;
|
||||
currentInputLabel.value = label;
|
||||
currentEditIndex.value = index; // 记录编辑的索引
|
||||
inputContent.value = content;
|
||||
showInputModal.value = true;
|
||||
};
|
||||
|
||||
// 删除优点或缺点
|
||||
const deleteComment = (item: any, type: string, index: number) => {
|
||||
if (type === 'A') {
|
||||
item.advantages.splice(index, 1);
|
||||
} else {
|
||||
item.disadvantages.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭输入弹窗
|
||||
const closeInputModal = () => {
|
||||
showInputModal.value = false;
|
||||
// 如果用户之前没有输入内容,清空选择
|
||||
if (currentItem.value && !currentItem.value.xcPj) {
|
||||
currentItem.value.xcJg = '';
|
||||
currentItem.value.xcPj = '';
|
||||
}
|
||||
inputContent.value = '';
|
||||
currentItem.value = null;
|
||||
currentInputValue.value = '';
|
||||
currentInputLabel.value = '';
|
||||
currentEditIndex.value = -1;
|
||||
};
|
||||
|
||||
// 确认输入
|
||||
@ -430,25 +467,38 @@ const confirmInput = () => {
|
||||
}
|
||||
|
||||
if (currentItem.value) {
|
||||
currentItem.value.xcJg = currentInputValue.value;
|
||||
currentItem.value.xcPj = value;
|
||||
// 判断是新增还是编辑
|
||||
if (currentEditIndex.value >= 0) {
|
||||
// 编辑模式:更新指定索引的内容
|
||||
if (currentInputValue.value === 'A') {
|
||||
currentItem.value.advantages[currentEditIndex.value] = value;
|
||||
} else {
|
||||
currentItem.value.disadvantages[currentEditIndex.value] = value;
|
||||
}
|
||||
} else {
|
||||
// 新增模式:添加到对应的数组
|
||||
if (currentInputValue.value === 'A') {
|
||||
if (!currentItem.value.advantages) {
|
||||
currentItem.value.advantages = [];
|
||||
}
|
||||
currentItem.value.advantages.push(value);
|
||||
} else {
|
||||
if (!currentItem.value.disadvantages) {
|
||||
currentItem.value.disadvantages = [];
|
||||
}
|
||||
currentItem.value.disadvantages.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showInputModal.value = false;
|
||||
inputContent.value = '';
|
||||
currentItem.value = null;
|
||||
currentInputValue.value = '';
|
||||
currentInputLabel.value = '';
|
||||
currentEditIndex.value = -1;
|
||||
};
|
||||
|
||||
// 编辑评价内容
|
||||
const editComment = (item: any) => {
|
||||
const label = item.xcJg === 'A' ? '优点' : '缺点';
|
||||
|
||||
currentItem.value = item;
|
||||
currentInputValue.value = item.xcJg;
|
||||
currentInputLabel.value = label;
|
||||
inputContent.value = item.xcPj || '';
|
||||
showInputModal.value = true;
|
||||
};
|
||||
|
||||
// 上传成功回调
|
||||
const onImageUploadSuccess = (image: ImageItem, index: number) => {
|
||||
@ -516,42 +566,42 @@ const submit = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否选择了年级班级
|
||||
if (!curBj.value || !curNj.value) {
|
||||
uni.showToast({
|
||||
title: "请选择巡查的年级班级",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证巡查项目是否已选择
|
||||
const hasCheckedItems = checkItems.value.some(item => item.xcJg && item.xcPj);
|
||||
if (!hasCheckedItems) {
|
||||
uni.showToast({
|
||||
title: "请至少选择一个巡查项目并填写评价",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 巡查项目可以为空,不强制要求填写
|
||||
// 允许用户只提交年级班级信息和图片/视频
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
// 构建巡查项目列表(只提交已选择的项目)
|
||||
const zbXcXmList = checkItems.value
|
||||
.filter((item: any) => item.xcJg && item.xcPj)
|
||||
.map((item: any) => {
|
||||
return {
|
||||
xcXmId: item.id,
|
||||
xcJg: item.xcJg, // A-优点,B-缺点
|
||||
xcPj: item.xcPj, // 评价内容
|
||||
xmFz: item.xmFz, // 项目分值
|
||||
xcMc: item.xcMc, // 巡查名称
|
||||
};
|
||||
});
|
||||
// 构建巡查项目列表(将优点和缺点分别提交)
|
||||
const zbXcXmList: any[] = [];
|
||||
|
||||
checkItems.value.forEach((item: any) => {
|
||||
// 添加所有优点
|
||||
if (item.advantages && item.advantages.length > 0) {
|
||||
item.advantages.forEach((adv: string) => {
|
||||
zbXcXmList.push({
|
||||
xcXmId: item.id,
|
||||
xcJg: 'A', // 优点
|
||||
xcPj: adv, // 评价内容
|
||||
xmFz: item.xmFz, // 项目分值
|
||||
xcMc: item.xcMc, // 巡查名称
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 添加所有缺点
|
||||
if (item.disadvantages && item.disadvantages.length > 0) {
|
||||
item.disadvantages.forEach((dis: string) => {
|
||||
zbXcXmList.push({
|
||||
xcXmId: item.id,
|
||||
xcJg: 'B', // 缺点
|
||||
xcPj: dis, // 评价内容
|
||||
xmFz: item.xmFz, // 项目分值
|
||||
xcMc: item.xcMc, // 巡查名称
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const submitData: any = {
|
||||
jsId: js.value.id,
|
||||
@ -561,10 +611,10 @@ const submit = async () => {
|
||||
xctime: now.format("YYYY-MM-DD HH:mm:ss"),
|
||||
zp: getImageUrls(),
|
||||
sp: getVideoUrls(),
|
||||
njId: curNj.value.key, // 年级ID
|
||||
njmcId: curNj.value.njmcId || curNj.value.key, // 年级名称ID
|
||||
bjId: curBj.value.key, // 班级ID
|
||||
bc: selectedClassText.value, // 别称(如:三年级1班)
|
||||
njId: curNj.value?.key || '', // 年级ID(可选)
|
||||
njmcId: curNj.value?.njmcId || curNj.value?.key || '', // 年级名称ID(可选)
|
||||
bjId: curBj.value?.key || '', // 班级ID(可选)
|
||||
bc: selectedClassText.value || '', // 别称(如:三年级1班)
|
||||
zbXcXmList: zbXcXmList,
|
||||
};
|
||||
|
||||
@ -759,11 +809,12 @@ onMounted(async () => {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
.item-score-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.item-score-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
@ -791,43 +842,113 @@ onMounted(async () => {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.item-comment {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #4080ff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
.item-action-btns {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
&:active {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
.action-btn {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
transition: all 0.3s;
|
||||
|
||||
.btn-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.advantage-btn {
|
||||
background-color: #f0f9ff;
|
||||
color: #2e7d32;
|
||||
border: 1px solid #81c784;
|
||||
|
||||
&:active {
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
}
|
||||
|
||||
&.disadvantage-btn {
|
||||
background-color: #fff5f5;
|
||||
color: #c62828;
|
||||
border: 1px solid #e57373;
|
||||
|
||||
&:active {
|
||||
background-color: #ffebee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
margin-top: 10px;
|
||||
|
||||
.comment-label {
|
||||
.comment-list-title {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.comment-edit-hint {
|
||||
font-size: 12px;
|
||||
color: #4080ff;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
.comment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 6px;
|
||||
|
||||
&.advantage-item {
|
||||
background-color: #e8f5e9;
|
||||
border-left: 3px solid #4caf50;
|
||||
}
|
||||
|
||||
&.disadvantage-item {
|
||||
background-color: #ffebee;
|
||||
border-left: 3px solid #f44336;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,40 +47,47 @@
|
||||
<text class="item-label">值周位置:</text>
|
||||
<text class="item-value">{{ data.zbwz || '暂无' }}</text>
|
||||
</view>
|
||||
<view class="content-item" v-if="data.bc">
|
||||
<text class="item-label">巡查班级:</text>
|
||||
<text class="item-value">{{ data.bc }}</text>
|
||||
</view>
|
||||
|
||||
<view class="content-item flex-col">
|
||||
<text class="item-label" style="flex: 0 0 25px">巡查项目:</text>
|
||||
<view class="item-value" style="width: 100%">
|
||||
<template v-if="data.zbXcXmList && data.zbXcXmList.length > 0">
|
||||
<view
|
||||
v-for="(xm, idx) in data.zbXcXmList"
|
||||
:key="xm.xcXmId"
|
||||
style="margin-bottom: 4px"
|
||||
v-for="(project, pIdx) in groupProjectsByXcXmId(data.zbXcXmList)"
|
||||
:key="project.xcXmId"
|
||||
class="project-group"
|
||||
>
|
||||
<view>
|
||||
<view style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;">
|
||||
<view style="display: flex; align-items: center; flex: 1;">
|
||||
<text style="margin-right: 8px;">{{ idx + 1 }}、{{ xm.xcMc }}</text>
|
||||
<view style="display: flex; align-items: center;">
|
||||
<u-icon
|
||||
:name="xm.xcJg === 'A' ? 'checkmark-circle-fill' : 'close-circle-fill'"
|
||||
:color="xm.xcJg === 'A' ? '#67c23a' : '#f56c6c'"
|
||||
size="18"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view style="font-size: 12px; color: #666;">
|
||||
<text v-if="xm.xcJg === 'A'" style="color: #67c23a;">
|
||||
优点
|
||||
</text>
|
||||
<text v-else style="color: #f56c6c;">
|
||||
缺点
|
||||
</text>
|
||||
</view>
|
||||
<!-- 项目名称和分值 -->
|
||||
<view class="project-header">
|
||||
<text class="project-name">{{ pIdx + 1 }}、{{ project.xcMc }}</text>
|
||||
<text class="project-score">分值:{{ project.xmFz }}分</text>
|
||||
</view>
|
||||
|
||||
<!-- 优点列表 -->
|
||||
<view v-if="project.advantages && project.advantages.length > 0" class="comment-section">
|
||||
<view class="comment-section-title">优点:</view>
|
||||
<view
|
||||
v-for="(adv, advIdx) in project.advantages"
|
||||
:key="advIdx"
|
||||
class="comment-item advantage-item"
|
||||
>
|
||||
<text class="comment-text">{{ advIdx + 1 }}. {{ adv }}</text>
|
||||
</view>
|
||||
<view v-if="xm.xcPj" style="font-size: 12px; color: #666; margin-top: 4px; padding-left: 20px; line-height: 1.5;">
|
||||
<text style="color: #999;">评价:</text>
|
||||
<text style="color: #666;">{{ xm.xcPj }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 缺点列表 -->
|
||||
<view v-if="project.disadvantages && project.disadvantages.length > 0" class="comment-section">
|
||||
<view class="comment-section-title">缺点:</view>
|
||||
<view
|
||||
v-for="(dis, disIdx) in project.disadvantages"
|
||||
:key="disIdx"
|
||||
class="comment-item disadvantage-item"
|
||||
>
|
||||
<text class="comment-text">{{ disIdx + 1 }}. {{ dis }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -241,6 +248,41 @@ const getVideoArray = (str: string) => {
|
||||
return str.split(",").map((item) => item.trim());
|
||||
};
|
||||
|
||||
// 按 xcXmId 分组巡查项目
|
||||
const groupProjectsByXcXmId = (zbXcXmList: any[]) => {
|
||||
const groupedProjects: any[] = [];
|
||||
const projectMap = new Map();
|
||||
|
||||
zbXcXmList.forEach((xm: any) => {
|
||||
if (!projectMap.has(xm.xcXmId)) {
|
||||
// 新项目
|
||||
projectMap.set(xm.xcXmId, {
|
||||
xcXmId: xm.xcXmId,
|
||||
xcMc: xm.xcMc,
|
||||
xmFz: xm.xmFz,
|
||||
advantages: [],
|
||||
disadvantages: []
|
||||
});
|
||||
}
|
||||
|
||||
const project = projectMap.get(xm.xcXmId);
|
||||
|
||||
// 根据 xcJg 分类添加到优点或缺点
|
||||
if (xm.xcJg === 'A') {
|
||||
project.advantages.push(xm.xcPj);
|
||||
} else if (xm.xcJg === 'B') {
|
||||
project.disadvantages.push(xm.xcPj);
|
||||
}
|
||||
});
|
||||
|
||||
// 转换为数组
|
||||
projectMap.forEach((value) => {
|
||||
groupedProjects.push(value);
|
||||
});
|
||||
|
||||
return groupedProjects;
|
||||
};
|
||||
|
||||
// 页面加载时重新加载巡查记录
|
||||
onMounted(() => {
|
||||
reload();
|
||||
@ -361,6 +403,75 @@ onMounted(() => {
|
||||
|
||||
.item-value {
|
||||
color: #4a5568;
|
||||
|
||||
.project-group {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e8e8e8;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed #e0e0e0;
|
||||
|
||||
.project-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.project-score {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-section {
|
||||
margin-top: 8px;
|
||||
|
||||
.comment-section-title {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.advantage-item {
|
||||
background-color: #e8f5e9;
|
||||
border-left: 3px solid #4caf50;
|
||||
}
|
||||
|
||||
&.disadvantage-item {
|
||||
background-color: #ffebee;
|
||||
border-left: 3px solid #f44336;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/static/base/home/jlb.png
Normal file
BIN
src/static/base/home/jlb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/static/base/home/zbtj.png
Normal file
BIN
src/static/base/home/zbtj.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
@ -23,7 +23,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
<html dir="ltr" mozdisallowselectionprint moznomarginboxes>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes">
|
||||
<meta name="google" content="notranslate">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>pdf</title>
|
||||
@ -337,5 +337,168 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
</div>
|
||||
<!-- outerContainer -->
|
||||
<div id="printContainer"></div>
|
||||
|
||||
<!-- 强制启用触摸缩放(针对微信WebView) - 优化版 -->
|
||||
<script>
|
||||
(function() {
|
||||
// 强制设置viewport允许缩放
|
||||
var viewport = document.querySelector('meta[name="viewport"]');
|
||||
if (viewport) {
|
||||
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=10.0, minimum-scale=0.1, user-scalable=yes');
|
||||
}
|
||||
|
||||
// 禁用微信WebView的默认触摸行为
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
if (e.touches.length > 1) {
|
||||
e.preventDefault(); // 阻止微信默认的双指事件
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// 添加双指缩放支持(优化性能版)
|
||||
var viewerContainer = document.getElementById('viewerContainer');
|
||||
var viewer = document.getElementById('viewer');
|
||||
|
||||
if (viewerContainer) {
|
||||
var lastDistance = 0;
|
||||
var initialScale = 1;
|
||||
var currentScale = 1;
|
||||
var baseScale = 1; // PDF.js 当前的实际缩放
|
||||
var tempScale = 1; // 临时的 CSS transform 缩放
|
||||
var isScaling = false;
|
||||
var scaleTimeout = null;
|
||||
var centerX = 0, centerY = 0;
|
||||
|
||||
// 获取 PDF.js 当前缩放
|
||||
function getCurrentPDFScale() {
|
||||
if (window.PDFViewerApplication && window.PDFViewerApplication.pdfViewer) {
|
||||
return window.PDFViewerApplication.pdfViewer.currentScale || 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 应用 CSS transform 进行即时缩放(流畅)
|
||||
function applyTransformScale(scale, originX, originY) {
|
||||
if (viewer) {
|
||||
viewer.style.transformOrigin = originX + 'px ' + originY + 'px';
|
||||
viewer.style.transform = 'scale(' + scale + ')';
|
||||
viewer.style.transition = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 清除 CSS transform,使用 PDF.js API 进行最终缩放
|
||||
function applyFinalScale(targetScale) {
|
||||
if (viewer) {
|
||||
viewer.style.transform = '';
|
||||
viewer.style.transformOrigin = '';
|
||||
viewer.style.transition = '';
|
||||
}
|
||||
|
||||
if (window.PDFViewerApplication && window.PDFViewerApplication.pdfViewer) {
|
||||
// 使用 PDF.js 的缩放 API
|
||||
window.PDFViewerApplication.pdfViewer.currentScaleValue = targetScale;
|
||||
console.log('最终缩放:', Math.round(targetScale * 100) + '%');
|
||||
}
|
||||
}
|
||||
|
||||
viewerContainer.addEventListener('touchstart', function(e) {
|
||||
if (e.touches.length === 2) {
|
||||
isScaling = true;
|
||||
|
||||
// 计算两指中点作为缩放中心
|
||||
centerX = (e.touches[0].pageX + e.touches[1].pageX) / 2;
|
||||
centerY = (e.touches[0].pageY + e.touches[1].pageY) / 2;
|
||||
|
||||
lastDistance = Math.hypot(
|
||||
e.touches[0].pageX - e.touches[1].pageX,
|
||||
e.touches[0].pageY - e.touches[1].pageY
|
||||
);
|
||||
|
||||
// 获取当前 PDF.js 的实际缩放
|
||||
baseScale = getCurrentPDFScale();
|
||||
initialScale = baseScale;
|
||||
tempScale = 1; // 重置临时缩放
|
||||
|
||||
// 清除之前的延迟操作
|
||||
if (scaleTimeout) {
|
||||
clearTimeout(scaleTimeout);
|
||||
scaleTimeout = null;
|
||||
}
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
viewerContainer.addEventListener('touchmove', function(e) {
|
||||
if (e.touches.length === 2 && isScaling) {
|
||||
e.preventDefault();
|
||||
|
||||
var distance = Math.hypot(
|
||||
e.touches[0].pageX - e.touches[1].pageX,
|
||||
e.touches[0].pageY - e.touches[1].pageY
|
||||
);
|
||||
|
||||
if (lastDistance > 0) {
|
||||
// 计算相对于初始距离的缩放比例
|
||||
var scaleRatio = distance / lastDistance;
|
||||
tempScale = scaleRatio;
|
||||
currentScale = initialScale * scaleRatio;
|
||||
|
||||
// 限制缩放范围
|
||||
currentScale = Math.max(0.5, Math.min(currentScale, 5));
|
||||
tempScale = currentScale / baseScale;
|
||||
|
||||
// 使用 CSS transform 进行即时缩放(非常流畅)
|
||||
applyTransformScale(tempScale, centerX, centerY);
|
||||
}
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
viewerContainer.addEventListener('touchend', function(e) {
|
||||
if (e.touches.length < 2 && isScaling) {
|
||||
isScaling = false;
|
||||
lastDistance = 0;
|
||||
|
||||
// 延迟应用最终缩放,避免手势结束时卡顿
|
||||
scaleTimeout = setTimeout(function() {
|
||||
if (currentScale !== baseScale) {
|
||||
applyFinalScale(currentScale);
|
||||
baseScale = currentScale;
|
||||
}
|
||||
}, 50); // 50ms 延迟,让手势结束动画更流畅
|
||||
}
|
||||
});
|
||||
|
||||
// 手势取消时也要处理
|
||||
viewerContainer.addEventListener('touchcancel', function(e) {
|
||||
if (isScaling) {
|
||||
isScaling = false;
|
||||
lastDistance = 0;
|
||||
|
||||
// 恢复原始状态
|
||||
if (viewer) {
|
||||
viewer.style.transform = '';
|
||||
viewer.style.transformOrigin = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听页面加载完成
|
||||
window.addEventListener('load', function() {
|
||||
console.log('PDF查看器已加载,优化版手势缩放已启用');
|
||||
|
||||
// 强制设置CSS样式
|
||||
if (viewerContainer) {
|
||||
viewerContainer.style.touchAction = 'pan-x pan-y pinch-zoom';
|
||||
viewerContainer.style.webkitOverflowScrolling = 'touch';
|
||||
}
|
||||
|
||||
if (viewer) {
|
||||
viewer.style.willChange = 'transform';
|
||||
viewer.style.webkitTransform = 'translateZ(0)'; // 启用硬件加速
|
||||
}
|
||||
|
||||
document.body.style.touchAction = 'manipulation';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -110,7 +110,7 @@ export function get<T = any>(
|
||||
method: "GET",
|
||||
dataType: "json",
|
||||
responseType: "text",
|
||||
timeout: 30000, // 设置30秒超时
|
||||
timeout: 1200000, // 设置20分钟超时(PDF生成需要较长时间)
|
||||
},
|
||||
options
|
||||
)
|
||||
@ -159,7 +159,7 @@ export function post<T = any>(
|
||||
method: "POST",
|
||||
dataType: "json",
|
||||
responseType: "text",
|
||||
timeout: 300000, // 设置5分钟超时
|
||||
timeout: 1200000, // 设置20分钟超时(PDF生成需要较长时间)
|
||||
},
|
||||
options
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user