统计调整

This commit is contained in:
hebo 2025-11-10 16:41:31 +08:00
parent c69caf1f85
commit 50ad4eb5e3
34 changed files with 7293 additions and 508 deletions

View File

@ -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);
};
/**
* -
*/

View File

@ -23,3 +23,9 @@ export const findAllZw = () => {
};

View File

@ -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;
}
}

View File

@ -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": {

View File

@ -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",
},
],
},
],

View File

@ -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;
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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使fetchPDFblobPDF.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已设置为falseweb-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 URLPDF.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
// APPloadingAPPfetch
if (loading.value) {
setTimeout(() => {
loading.value = false;
console.log('APP环境PDF.js查看器加载完成');
}, 500);
}
// #endif
// #ifdef H5
// H5loading 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>

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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", // 10
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>

View File

@ -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]; // keytitle
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;

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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; // 07
}
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, // 使IDID
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>

View 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; // 07
}
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>

View File

@ -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>

View File

@ -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)
//

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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;
}
}
}
}
}

View File

@ -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;
}
}
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -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>

View File

@ -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
)