学习进度统计
This commit is contained in:
@@ -57,6 +57,19 @@ export const learningTaskApi = {
|
||||
const response = await api.post<TaskVO>(`${this.learningTaskPrefix}/page`, {pageParam, filter});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 首页学习进度统计(按标签)
|
||||
* @param startTime 开始时间,格式:yyyy-MM-dd HH:mm:ss
|
||||
* @param endTime 结束时间,格式:yyyy-MM-dd HH:mm:ss
|
||||
*/
|
||||
async getMyStatistics(startTime: string, endTime: string): Promise<ResultDomain<any>> {
|
||||
const response = await api.get<any>(`${this.learningTaskPrefix}/statistics`, {
|
||||
startTime,
|
||||
endTime,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
/**
|
||||
* 创建学习任务
|
||||
* @param task 任务数据
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<div class="summary-content">
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">总学习人数</div>
|
||||
<div class="summary-value">{{ taskInfo.totalTaskNum || 0 }}</div>
|
||||
<div class="summary-value">{{ totalPerson }}</div>
|
||||
</div>
|
||||
<div class="summary-divider"></div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">已完成人数</div>
|
||||
<div class="summary-value completed">{{ taskInfo.completedTaskNum || 0 }}</div>
|
||||
<div class="summary-value completed">{{ completedPerson }}</div>
|
||||
</div>
|
||||
<div class="summary-divider"></div>
|
||||
<div class="summary-item">
|
||||
@@ -138,6 +138,7 @@ import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import { learningTaskApi } from '@/apis/study/learning-task';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { TaskVO } from '@/types';
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
@@ -146,7 +147,7 @@ const props = defineProps<{
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const taskInfo = ref<any>({});
|
||||
const taskInfo = ref<TaskVO>();
|
||||
const durationDistribution = ref<any[]>([]);
|
||||
const progressDistribution = ref<any[]>([]);
|
||||
const completionRanking = ref<any[]>([]);
|
||||
@@ -158,12 +159,12 @@ const progressChartRef = ref<HTMLElement>();
|
||||
let durationChart: ECharts | null = null;
|
||||
let progressChart: ECharts | null = null;
|
||||
|
||||
const totalPerson = computed(() => taskInfo.value?.taskUsers?.length || 0);
|
||||
const completedPerson = computed(() => taskInfo.value?.taskUsers?.filter((user: any) => user.status === 2).length || 0);
|
||||
// 计算完成率
|
||||
const completionRate = computed(() => {
|
||||
const total = taskInfo.value.totalTaskNum || 0;
|
||||
const completed = taskInfo.value.completedTaskNum || 0;
|
||||
if (total === 0) return 0;
|
||||
return ((completed / total) * 100).toFixed(1);
|
||||
if (totalPerson.value === 0) return 0;
|
||||
return ((completedPerson.value / totalPerson.value) * 100).toFixed(1);
|
||||
});
|
||||
|
||||
// 获取排名样式类
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<ArticleShow
|
||||
:resource-id="articleId"
|
||||
:task-id="taskId"
|
||||
:show-back-button="true"
|
||||
back-button-text="返回"
|
||||
@back="handleBack"
|
||||
@@ -20,6 +21,7 @@ const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const articleId = computed(() => route.query.articleId as string || '');
|
||||
const taskId = computed(() => route.query.taskId as string || '');
|
||||
|
||||
// 返回上一页
|
||||
function handleBack() {
|
||||
|
||||
@@ -113,6 +113,7 @@ interface Props {
|
||||
width?: string; // Dialog 宽度
|
||||
articleData?: Resource; // 文章数据(Dialog 模式使用)
|
||||
resourceID?: string; // 资源ID(路由模式使用)
|
||||
taskId?: string; // 任务ID(路由模式使用)
|
||||
showEditButton?: boolean; // 是否显示编辑按钮
|
||||
showBackButton?: boolean; // 是否显示返回按钮(路由模式)
|
||||
backButtonText?: string; // 返回按钮文本
|
||||
@@ -181,7 +182,7 @@ onMounted(() => {
|
||||
// 路由模式下,从路由参数加载文章
|
||||
if (!props.asDialog) {
|
||||
const articleId = route.query.articleId as string;
|
||||
const taskId = route.query.taskId as string;
|
||||
const taskId = props.taskId || (route.query.taskId as string);
|
||||
|
||||
// 如果传入了 articleData,则不需要从路由加载
|
||||
if (props.articleData && Object.keys(props.articleData).length > 0) {
|
||||
@@ -325,15 +326,6 @@ async function loadLearningRecord(resourceID: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成学习记录的taskId(当没有真实taskId时)
|
||||
function generateTaskId(resourceID: string, userID: string): string {
|
||||
// 使用简短格式,确保不超过50字符:SA_{resourceID的hash}_{userID的hash}
|
||||
// SA = Self-study Article
|
||||
const resourceHash = hashString(resourceID).substring(0, 16);
|
||||
const userHash = hashString(userID).substring(0, 16);
|
||||
return `SA_${resourceHash}_${userHash}`; // 长度:3 + 16 + 1 + 16 = 36字符
|
||||
}
|
||||
|
||||
// 简单的字符串哈希函数
|
||||
function hashString(str: string): string {
|
||||
let hash = 0;
|
||||
@@ -351,9 +343,9 @@ async function createLearningRecord(resourceID: string) {
|
||||
if (!userInfo.value?.id) return;
|
||||
|
||||
try {
|
||||
const taskId = route.query.taskId as string;
|
||||
const taskId = props.taskId || (route.query.taskId as string);
|
||||
// 如果没有taskId,生成一个自学任务ID
|
||||
const effectiveTaskId = taskId || generateTaskId(resourceID, userInfo.value.id!);
|
||||
const effectiveTaskId = taskId
|
||||
|
||||
const res = await learningRecordApi.createRecord({
|
||||
userID: userInfo.value.id,
|
||||
@@ -445,7 +437,7 @@ async function markArticleComplete() {
|
||||
|
||||
try {
|
||||
// 使用learningRecord中保存的taskID(可能是真实任务ID或生成的自学ID)
|
||||
const taskId = learningRecord.value.taskID || (route.query.taskId as string);
|
||||
const taskId = learningRecord.value.taskID || props.taskId || (route.query.taskId as string);
|
||||
|
||||
await learningRecordApi.markComplete({
|
||||
id: learningRecord.value.id,
|
||||
@@ -547,11 +539,19 @@ async function createHistoryRecord(resourceID: string) {
|
||||
if (!userInfo.value?.id) return;
|
||||
|
||||
try {
|
||||
const res = await learningHistoryApi.recordResourceView(
|
||||
userInfo.value.id,
|
||||
resourceID,
|
||||
0 // 初始时长为0
|
||||
);
|
||||
const taskId = props.taskId || (route.query.taskId as string);
|
||||
|
||||
// 直接创建学习历史对象,包含 taskID
|
||||
const historyData: TbLearningHistory = {
|
||||
userID: userInfo.value.id,
|
||||
resourceType: 1, // 1资源/新闻
|
||||
resourceID: resourceID,
|
||||
duration: 0,
|
||||
deviceType: 'web',
|
||||
taskID: taskId || undefined // 如果没有 taskId,传 undefined
|
||||
};
|
||||
|
||||
const res = await learningHistoryApi.recordLearningHistory(historyData);
|
||||
|
||||
if (res.success && res.data) {
|
||||
learningHistory.value = res.data;
|
||||
@@ -597,7 +597,8 @@ async function saveHistoryRecord() {
|
||||
const updatedHistory: TbLearningHistory = {
|
||||
...learningHistory.value,
|
||||
duration: (learningHistory.value.duration || 0) + duration,
|
||||
endTime: new Date().toISOString()
|
||||
endTime: new Date().toISOString(),
|
||||
taskID: learningHistory.value.taskID // 保持原有的 taskID
|
||||
};
|
||||
|
||||
// 调用API更新学习历史
|
||||
@@ -654,7 +655,7 @@ function handleBack() {
|
||||
}
|
||||
stopLearningTimer();
|
||||
|
||||
const taskId = route.query.taskId as string;
|
||||
const taskId = props.taskId || (route.query.taskId as string);
|
||||
// 如果有 taskId,返回任务详情
|
||||
if (taskId) {
|
||||
router.push({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="progress-header">
|
||||
<div class="header-left">
|
||||
<h3 class="progress-title">学习进度</h3>
|
||||
<p class="update-time">更新时间:2025-09-25 18:30:00</p>
|
||||
<p class="update-time">更新时间:{{ updateTime || '-' }}</p>
|
||||
</div>
|
||||
<div class="tab-buttons">
|
||||
<button v-for="(tab, index) in tabs" :key="index" :class="['tab-btn', { active: activeTab === index }]"
|
||||
@@ -21,38 +21,51 @@
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import dayjs from 'dayjs';
|
||||
import { learningTaskApi } from '@/apis/study/learning-task';
|
||||
|
||||
const tabs = ref(['今日', '近一周', '近一月']);
|
||||
const activeTab = ref(0);
|
||||
const chartRef = ref<HTMLElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
|
||||
// 模拟不同时间段的数据
|
||||
const chartDataMap = {
|
||||
0: [
|
||||
{ name: '党史', value: 43, highlight: false },
|
||||
{ name: '理论', value: 58, highlight: false },
|
||||
{ name: '政策', value: 34, highlight: false },
|
||||
{ name: '思政', value: 20, highlight: false },
|
||||
{ name: '文化', value: 52, highlight: true }
|
||||
],
|
||||
1: [
|
||||
{ name: '党史', value: 68, highlight: true },
|
||||
{ name: '理论', value: 52, highlight: false },
|
||||
{ name: '政策', value: 45, highlight: false },
|
||||
{ name: '思政', value: 38, highlight: false },
|
||||
{ name: '文化', value: 41, highlight: false }
|
||||
],
|
||||
2: [
|
||||
{ name: '党史', value: 85, highlight: true },
|
||||
{ name: '理论', value: 72, highlight: false },
|
||||
{ name: '政策', value: 58, highlight: false },
|
||||
{ name: '思政', value: 65, highlight: false },
|
||||
{ name: '文化', value: 43, highlight: false }
|
||||
]
|
||||
};
|
||||
// 后端返回的数据结构映射为图表数据
|
||||
const chartData = ref<{ name: string; value: number; highlight: boolean }[]>([]);
|
||||
|
||||
const chartData = computed(() => chartDataMap[activeTab.value as keyof typeof chartDataMap]);
|
||||
const updateTime = ref('');
|
||||
|
||||
async function loadStatistics() {
|
||||
// 根据当前 tab 计算时间范围
|
||||
const now = dayjs();
|
||||
let start: dayjs.Dayjs;
|
||||
|
||||
if (activeTab.value === 0) {
|
||||
// 今日:从今天 00:00:00 到现在
|
||||
start = now.startOf('day');
|
||||
} else if (activeTab.value === 1) {
|
||||
// 近一周:过去 7 天
|
||||
start = now.subtract(6, 'day').startOf('day');
|
||||
} else {
|
||||
// 近一月:过去 30 天
|
||||
start = now.subtract(29, 'day').startOf('day');
|
||||
}
|
||||
|
||||
const startTime = start.format('YYYY-MM-DD HH:mm:ss');
|
||||
const endTime = now.format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
const res = await learningTaskApi.getMyStatistics(startTime, endTime);
|
||||
if (res.code === 200 && res.data) {
|
||||
const list = (res.data.tagProgressList || []) as { tagName: string; avgProgress: number }[];
|
||||
chartData.value = list.map(item => ({
|
||||
name: item.tagName,
|
||||
value: Number(item.avgProgress ?? 0),
|
||||
highlight: false,
|
||||
}));
|
||||
updateTime.value = now.format('YYYY-MM-DD HH:mm:ss');
|
||||
} else {
|
||||
chartData.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 图表配置
|
||||
function getChartOption(): EChartsOption {
|
||||
@@ -155,8 +168,9 @@ function handleResize() {
|
||||
}
|
||||
|
||||
// 处理标签切换
|
||||
function handleTabChange(index: number) {
|
||||
async function handleTabChange(index: number) {
|
||||
activeTab.value = index;
|
||||
await loadStatistics();
|
||||
if (chartInstance) {
|
||||
chartInstance.setOption(chartOption.value, true);
|
||||
}
|
||||
@@ -169,8 +183,12 @@ watch(() => chartOption.value, () => {
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
initChart();
|
||||
await loadStatistics();
|
||||
if (chartInstance) {
|
||||
chartInstance.setOption(chartOption.value, true);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -62,13 +62,14 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">任务分类标签</label>
|
||||
<label class="form-label required">任务分类标签</label>
|
||||
<select
|
||||
v-model="selectedTagID"
|
||||
class="form-input form-select"
|
||||
@change="handleTagChange"
|
||||
@blur="validateTag"
|
||||
>
|
||||
<option value="">请选择分类标签(可选)</option>
|
||||
<option value="">请选择分类标签</option>
|
||||
<option
|
||||
v-for="tag in availableTags"
|
||||
:key="tag.tagID"
|
||||
@@ -77,6 +78,7 @@
|
||||
{{ tag.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="errors.tag" class="error-msg">{{ errors.tag }}</span>
|
||||
<div v-if="selectedTag" class="selected-tag-preview">
|
||||
<div class="tag-badge" :style="{ backgroundColor: selectedTag.color || '#409eff' }">
|
||||
{{ selectedTag.name }}
|
||||
@@ -350,7 +352,8 @@ const taskData = ref<TaskVO>({
|
||||
const errors = ref({
|
||||
name: '',
|
||||
startTime: '',
|
||||
endTime: ''
|
||||
endTime: '',
|
||||
tag: ''
|
||||
});
|
||||
|
||||
// 选中的数据
|
||||
@@ -732,6 +735,7 @@ function handleTagChange() {
|
||||
} else {
|
||||
selectedTag.value = null;
|
||||
}
|
||||
validateTag();
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
@@ -744,6 +748,15 @@ function validateTaskName() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateTag() {
|
||||
if (!selectedTagID.value) {
|
||||
errors.value.tag = '请选择任务分类标签';
|
||||
return false;
|
||||
}
|
||||
errors.value.tag = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateTime() {
|
||||
if (!taskData.value.learningTask.startTime) {
|
||||
errors.value.startTime = '请选择开始时间';
|
||||
@@ -765,8 +778,9 @@ function validateTime() {
|
||||
function validateForm() {
|
||||
const isNameValid = validateTaskName();
|
||||
const isTimeValid = validateTime();
|
||||
const isTagValid = validateTag();
|
||||
|
||||
if (!isNameValid || !isTimeValid) {
|
||||
if (!isNameValid || !isTimeValid || !isTagValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user