+
批量下载 ({{ checkedCount }}张)
@@ -93,9 +99,12 @@
-
-
图片预览区域
-
{{ currentPreview?.time }} | {{ currentPreview?.type }}
+
![]()
+
+
+ 暂无图片
+
+
{{ currentPreview?.behaviorTime }} | {{ currentPreview?.behaviorTypeName }}
@@ -106,64 +115,53 @@
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
+import { getBehaviorTypesWithStats, getBehaviorTimePeriod, getBehaviorRecords, getBehaviorTypes } from '@/api/behavior'
const filters = reactive({
- course: 'math_a',
- teacher: '',
+ courseName: '',
+ teacherName: '',
date: null
})
-const imageFilter = ref('all')
+const imageFilter = ref('')
+
+// 行为类型列表(用于筛选按钮和颜色映射)
+const behaviorTypes = ref([])
-const behaviors = ref([
- { name: '专注听讲', percent: 65, color: '#52c41a' },
- { name: '举手互动', percent: 15, color: '#1890ff' },
- { name: '低头书写', percent: 12, color: '#722ed1' },
- { name: '交谈讨论', percent: 5, color: '#faad14' },
- { name: '其他', percent: 3, color: '#bfbfbf' }
-])
+// 行为分布数据(从接口获取)
+const behaviors = ref([])
+const loading = ref(false)
const pieChartRef = ref(null)
const barChartRef = ref(null)
let pieChart = null
let barChart = null
-// 模拟图片数据
-const images = ref([
- { time: '08:15:32', type: '专注', checked: false },
- { time: '08:18:05', type: '举手', checked: false },
- { time: '08:22:41', type: '专注', checked: false },
- { time: '08:30:18', type: '低头', checked: false },
- { time: '08:35:56', type: '专注', checked: false },
- { time: '08:42:10', type: '交谈', checked: false },
- { time: '08:48:33', type: '举手', checked: false },
- { time: '08:55:20', type: '专注', checked: false }
-])
-
-const filteredImages = computed(() => {
- if (imageFilter.value === 'all') return images.value
- return images.value.filter(i => i.type === imageFilter.value)
+// 图片记录列表
+const imageRecords = ref([])
+const pagination = reactive({
+ current: 1,
+ size: 10,
+ total: 0
})
-const checkedCount = computed(() => images.value.filter(i => i.checked).length)
+const checkedCount = computed(() => imageRecords.value.filter(i => i.checked).length)
const hasChecked = computed(() => checkedCount.value > 0)
const previewVisible = ref(false)
const currentPreview = ref(null)
-const getBehaviorColor = (type) => {
- const map = { '专注': '#52c41a', '举手': '#1890ff', '低头': '#722ed1', '交谈': '#faad14' }
- return map[type] || '#bfbfbf'
-}
-
-const getBehaviorTagType = (type) => {
- const map = { '专注': 'success', '举手': '', '低头': 'info', '交谈': 'warning' }
- return map[type] || 'info'
+/** 根据行为类型 ID 获取对应颜色 */
+const getTypeColor = (behaviorTypeId) => {
+ const bt = behaviorTypes.value.find(t => t.id === behaviorTypeId)
+ return bt ? bt.color : '#bfbfbf'
}
-const getBehaviorIcon = (type) => {
- const map = { '专注': 'View', '举手': 'Pointer', '低头': 'Reading', '交谈': 'ChatDotRound' }
- return map[type] || 'QuestionFilled'
+/** 计算单个行为占比 */
+const behaviorPercent = (b) => {
+ if (!behaviors.value.length) return 0
+ const total = behaviors.value.reduce((sum, item) => sum + (item.count || 0), 0)
+ return total > 0 ? ((b.count / total) * 100).toFixed(1) : 0
}
const previewImage = (img) => {
@@ -175,21 +173,188 @@ const batchDownload = () => {
ElMessage.success(`正在下载 ${checkedCount.value} 张图片...`)
}
+/** 获取行为类型列表 */
+const fetchBehaviorTypes = async () => {
+ try {
+ const res = await getBehaviorTypes()
+ if (res && res.data) {
+ behaviorTypes.value = res.data
+ imageFilter.value = ''
+ }
+ } catch { /* 错误已在拦截器统一处理 */ }
+}
+
+/** 获取行为标记图片记录 */
+const fetchRecords = async (page = 1) => {
+ try {
+ const params = {
+ current: page,
+ size: pagination.size,
+ courseName: filters.courseName || undefined,
+ teacherName: filters.teacherName || undefined,
+ attDate: filters.date || undefined,
+ behaviorTypeId: imageFilter.value || undefined
+ }
+ const res = await getBehaviorRecords(params)
+ if (res && res.data) {
+ imageRecords.value = (res.data.records || []).map(r => ({
+ ...r,
+ checked: false
+ }))
+ pagination.current = res.data.current
+ pagination.total = res.data.total
+ pagination.size = res.data.size
+ }
+ } catch { /* 错误已在拦截器统一处理 */ }
+}
+
+/** 重置筛选条件 */
+const handleReset = () => {
+ filters.courseName = ''
+ filters.teacherName = ''
+ filters.date = null
+ imageFilter.value = ''
+ pagination.current = 1
+ fetchData()
+}
+
+/** 查询按钮 */
+const fetchData = async () => {
+ loading.value = true
+ pagination.current = 1
+ try {
+ const params = {
+ courseName: filters.courseName || undefined,
+ teacherName: filters.teacherName || undefined,
+ attDate: filters.date || undefined
+ }
+ const [res1, res2] = await Promise.all([
+ getBehaviorTypesWithStats(params),
+ getBehaviorTimePeriod(params)
+ ])
+
+ // 处理行为分布占比数据
+ if (res1 && res1.data) {
+ behaviors.value = res1.data.map(item => ({
+ id: item.id,
+ typeCode: item.typeCode,
+ name: item.typeName,
+ color: item.color,
+ count: item.count,
+ category: item.category,
+ description: item.description
+ }))
+ updatePieChart()
+ }
+
+ // 处理各行为时段分布数据
+ if (res2 && res2.data) {
+ updateBarChart(res2.data)
+ }
+
+ // 同时查询图片记录
+ await fetchRecords(1)
+ } catch {
+ // 错误已在拦截器中统一处理
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 筛选条件变化时重新查询 */
+const onFilterChange = () => {
+ pagination.current = 1
+ fetchRecords(1)
+}
+
+/** 分页切换 */
+const onPageChange = (page) => {
+ fetchRecords(page)
+}
+
+/** 更新饼图数据 */
+const updatePieChart = () => {
+ if (!pieChart) return
+ const total = behaviors.value.reduce((sum, b) => sum + (b.count || 0), 0)
+ pieChart.setOption({
+ series: [{
+ data: behaviors.value.map(b => ({
+ value: b.count,
+ name: b.name,
+ itemStyle: { color: b.color }
+ }))
+ }],
+ tooltip: {
+ trigger: 'item',
+ formatter: (params) => {
+ const pct = total > 0 ? ((params.value / total) * 100).toFixed(1) : 0
+ return `${params.name}: ${params.value} (${pct}%)`
+ }
+ }
+ })
+}
+
+/** 更新柱状图数据 */
+const updateBarChart = (data) => {
+ if (!barChart || !data.length) return
+
+ // x 轴:各时段标签
+ const xData = data.map(d => d.timeSlot)
+
+ // 收集所有行为类型名称及对应颜色(去重,按首次出现顺序)
+ const nameSet = new Set()
+ const nameList = []
+ const colorMap = {}
+ data.forEach(slot => {
+ (slot.data || []).forEach(item => {
+ if (!nameSet.has(item.name)) {
+ nameSet.add(item.name)
+ nameList.push(item.name)
+ colorMap[item.name] = item.color
+ }
+ })
+ })
+
+ // 构建 series:每个行为类型一根柱子
+ const series = nameList.map((name, idx) => ({
+ name,
+ type: 'bar',
+ stack: 'total',
+ data: data.map(slot => {
+ const found = (slot.data || []).find(item => item.name === name)
+ return found ? found.value : 0
+ }),
+ color: colorMap[name],
+ itemStyle: idx === 0 ? { borderRadius: [4, 4, 0, 0] } : {}
+ }))
+
+ barChart.setOption({
+ grid: { bottom: 70 },
+ xAxis: { data: xData },
+ legend: {
+ data: nameList,
+ bottom: 0,
+ type: 'scroll'
+ },
+ series
+ })
+}
+
const initCharts = () => {
// 饼图
if (pieChartRef.value) {
pieChart = echarts.init(pieChartRef.value)
pieChart.setOption({
- tooltip: { trigger: 'item', formatter: '{b}: {c}%' },
+ tooltip: { trigger: 'item' },
series: [{
type: 'pie',
- radius: ['55%', '80%'],
+ radius: ['45%', '80%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
label: { show: false },
emphasis: { label: { show: true, fontSize: 16, fontWeight: 'bold' } },
- data: behaviors.value.map(b => ({ value: b.percent, name: b.name, itemStyle: { color: b.color } }))
+ data: []
}]
})
}
@@ -214,6 +379,8 @@ const initCharts = () => {
onMounted(() => {
initCharts()
+ fetchBehaviorTypes()
+ fetchData()
window.addEventListener('resize', () => {
pieChart?.resize()
barChart?.resize()
@@ -268,18 +435,19 @@ onUnmounted(() => {
.legend-dot {
font-size: 12px;
- color: #525252;
+ color: #ffffff;
display: flex;
align-items: center;
gap: 4px;
-
- &::before {
- content: '';
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: inherit;
- }
+ padding: 2px 5px;
+ border-radius: 4px;
+ // &::before {
+ // content: '';
+ // width: 8px;
+ // height: 8px;
+ // border-radius: 50%;
+ // background: inherit;
+ // }
}
.image-section {
@@ -330,6 +498,13 @@ onUnmounted(() => {
align-items: center;
justify-content: center;
border-bottom: 3px solid #d9d9d9;
+ overflow: hidden;
+
+ .snapshot-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
}
.image-info {
@@ -377,6 +552,13 @@ onUnmounted(() => {
gap: 8px;
color: #bfbfbf;
font-size: 14px;
+ overflow: hidden;
+
+ .preview-img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
}
.preview-meta {
diff --git a/src/views/dashboard/components/AttendanceDetail.vue b/src/views/dashboard/components/AttendanceDetail.vue
index fb58996..5994fc5 100644
--- a/src/views/dashboard/components/AttendanceDetail.vue
+++ b/src/views/dashboard/components/AttendanceDetail.vue
@@ -80,11 +80,16 @@ const initDetailChart = () => {
}
const batchExport = () => ElMessage.success('批量导出成功')
-
-// 弹窗打开时初始化图表
+// 弹窗打开时重置 tab
watch(() => props.modelValue, (val) => {
if (val) {
detailTab.value = 'absent'
+ }
+})
+
+// 切换到出勤统计 tab 时初始化图表
+watch(detailTab, (val) => {
+ if (val === 'stats') {
nextTick(() => initDetailChart())
}
})
diff --git a/src/views/dashboard/components/AttendanceManage.vue b/src/views/dashboard/components/AttendanceManage.vue
index d42a678..5a0604e 100644
--- a/src/views/dashboard/components/AttendanceManage.vue
+++ b/src/views/dashboard/components/AttendanceManage.vue
@@ -45,7 +45,7 @@
-
+
@@ -100,7 +100,7 @@ const mapRecord = (item) => ({
courseName: item.courseName,
teacher: item.teacherName,
classroom: item.classroomName,
- time: `${item.startTime || ''}
至
${item.endTime || ''}`,
+ time: `${item.startTime || ''} 至 ${item.endTime || ''}`,
total: item.totalCount,
actual: item.actualCount,
absentCount: item.absentCount,
diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue
index 6be552c..0f2d385 100644
--- a/src/views/dashboard/index.vue
+++ b/src/views/dashboard/index.vue
@@ -57,7 +57,7 @@
-
+
diff --git a/src/views/history/index.vue b/src/views/history/index.vue
index 92daea5..b94a1cf 100644
--- a/src/views/history/index.vue
+++ b/src/views/history/index.vue
@@ -15,244 +15,196 @@
size="default"
style="width: 200px"
/>
-
-
-
+
-
-
-
-
+
- 查询
+ 查询
重置