..
This commit is contained in:
@@ -209,7 +209,7 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
// 前往学习历史
|
||||
goToHistory() {
|
||||
pjaxReplace('/platform/h5/edu/history');
|
||||
pjaxReplace('/platform/h5/edu/studyhis/');
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<%
|
||||
layout('/platform/zhghh5/layout/layout.html',{
|
||||
title: '学习历史',
|
||||
keywords: '学习历史,视频学习,在线教育',
|
||||
description: '查看学习历史记录和进度统计'
|
||||
}){
|
||||
%>
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.history-container {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 15px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
@@ -27,32 +25,38 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1989fa;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
margin: 15px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.history-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
@@ -60,21 +64,25 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.course-category {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
@@ -82,16 +90,19 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
border-radius: 8px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
text-align: right;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #28a745;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
@@ -100,14 +111,17 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
overflow: hidden;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #28a745, #20c997);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.video-list {
|
||||
padding: 0 20px 15px;
|
||||
}
|
||||
|
||||
.video-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -115,9 +129,11 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.video-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -130,30 +146,37 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.video-icon.completed {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
}
|
||||
|
||||
.video-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video-duration {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.video-progress {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.watch-time {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
@@ -163,19 +186,19 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
|
||||
<div id="app" class="history-container">
|
||||
<van-nav-bar
|
||||
title="学习历史"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
|
||||
title="学习历史"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
></van-nav-bar>
|
||||
|
||||
<van-tabs v-model="activeTab" @change="onTabChange">
|
||||
<van-tab title="全部" name="all"></van-tab>
|
||||
<van-tab title="已完成" name="completed"></van-tab>
|
||||
<van-tab title="学习中" name="in_progress"></van-tab>
|
||||
<van-tab title="最近观看" name="recent"></van-tab>
|
||||
</van-tabs>
|
||||
|
||||
|
||||
<!-- 学习统计 -->
|
||||
<div v-if="showStats && stats" class="stats-card">
|
||||
<div class="stats-title">
|
||||
@@ -197,16 +220,16 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 学习历史列表 -->
|
||||
<van-loading v-if="loading" type="spinner" color="#1989fa" style="margin: 40px auto; display: block;">
|
||||
加载中...
|
||||
</van-loading>
|
||||
|
||||
|
||||
<van-empty v-else-if="historyList.length === 0" description="暂无学习记录">
|
||||
<van-button round type="primary" @click="loadData">刷新</van-button>
|
||||
</van-empty>
|
||||
|
||||
|
||||
<div v-else>
|
||||
<div v-for="course in historyList" :key="course.courseId" class="history-item">
|
||||
<div class="history-header" @click="viewCourse(course.courseId)">
|
||||
@@ -224,16 +247,16 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="course.videos && course.videos.length > 0" class="video-list">
|
||||
<div
|
||||
v-for="video in course.videos"
|
||||
:key="video.videoId"
|
||||
class="video-item"
|
||||
@click="playVideo(video.videoId, course.courseId, video.videoTitle)"
|
||||
<div
|
||||
v-for="video in course.videos"
|
||||
:key="video.videoId"
|
||||
class="video-item"
|
||||
@click="playVideo(video.videoId, course.courseId, video.videoTitle)"
|
||||
>
|
||||
<div class="video-icon" :class="{ completed: video.isCompleted }">
|
||||
<van-icon :name="video.isCompleted ? 'success' : 'play-circle-o'" />
|
||||
<van-icon :name="video.isCompleted ? 'success' : 'play-circle-o'"/>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<div class="video-title">{{ video.videoTitle || '未命名视频' }}</div>
|
||||
@@ -254,132 +277,134 @@ layout('/platform/zhghh5/layout/layout.html',{
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'all',
|
||||
loading: false,
|
||||
showStats: false,
|
||||
stats: null,
|
||||
historyList: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadData();
|
||||
},
|
||||
methods: {
|
||||
onClickLeft() {
|
||||
window.location.href = '${base}/platform/h5/edu/courses';
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'all',
|
||||
loading: false,
|
||||
showStats: false,
|
||||
stats: null,
|
||||
historyList: []
|
||||
};
|
||||
},
|
||||
|
||||
onTabChange(name) {
|
||||
this.activeTab = name;
|
||||
mounted() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 如果是全部标签,先加载统计信息
|
||||
if (this.activeTab === 'all') {
|
||||
await this.loadStats();
|
||||
this.showStats = true;
|
||||
methods: {
|
||||
onClickLeft() {
|
||||
window.location.href = '/platform/h5/edu/courses';
|
||||
},
|
||||
|
||||
onTabChange(name) {
|
||||
this.activeTab = name;
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 如果是全部标签,先加载统计信息
|
||||
if (this.activeTab === 'all') {
|
||||
await this.loadStats();
|
||||
this.showStats = true;
|
||||
} else {
|
||||
this.showStats = false;
|
||||
}
|
||||
|
||||
// 加载学习历史
|
||||
await this.loadHistory();
|
||||
} catch (error) {
|
||||
this.$toast('加载失败,请重试');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await this.$axios.post('/platform/h5/edu/study/stats');
|
||||
if (response.data.code === 0) {
|
||||
this.stats = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadHistory() {
|
||||
try {
|
||||
const response = await this.$axios.post('/platform/h5/edu/study/history', {
|
||||
params: {filter: this.activeTab}
|
||||
});
|
||||
if (response.data.code === 0) {
|
||||
this.historyList = response.data.data || [];
|
||||
} else {
|
||||
this.$toast(response.data.msg || '加载失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast('网络错误,请检查网络连接');
|
||||
}
|
||||
},
|
||||
|
||||
viewCourse(courseId) {
|
||||
window.location.href = `/platform/h5/edu/course/detail?courseId=` + courseId;
|
||||
},
|
||||
|
||||
playVideo(videoId, courseId, videoTitle) {
|
||||
pjaxReplace('/platform/h5/edu/video/play?videoId=' + videoId + '&courseId=' + courseId + '&title=' + encodeURIComponent(videoTitle))
|
||||
},
|
||||
|
||||
getProgressPercent(course) {
|
||||
if (!course.videos || course.videos.length === 0) return 0;
|
||||
const completedCount = course.videos.filter(v => v.isCompleted).length;
|
||||
return Math.round((completedCount / course.videos.length) * 100);
|
||||
},
|
||||
|
||||
getVideoProgress(video) {
|
||||
if (video.isCompleted) return '已完成';
|
||||
if (video.duration > 0 && video.watchDuration > 0) {
|
||||
const percent = Math.round((video.watchDuration / video.duration) * 100);
|
||||
return `观看` + percent + '%';
|
||||
}
|
||||
return '未开始';
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '未知';
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffTime = now - date;
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
|
||||
if (diffHours === 0) {
|
||||
const diffMinutes = Math.floor(diffTime / (1000 * 60));
|
||||
return diffMinutes <= 0 ? '刚刚' : diffMinutes + `分钟前`;
|
||||
}
|
||||
return diffHours + `小时前`;
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天';
|
||||
} else if (diffDays < 7) {
|
||||
return diffDays + `天前`;
|
||||
} else {
|
||||
this.showStats = false;
|
||||
return date.getFullYear() + '-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getDate()).padStart(2, '0');
|
||||
}
|
||||
|
||||
// 加载学习历史
|
||||
await this.loadHistory();
|
||||
} catch (error) {
|
||||
this.$toast('加载失败,请重试');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
formatDuration(seconds) {
|
||||
if (!seconds) return '00:00';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return String(minutes).padStart(2, '0') + ':' + String(remainingSeconds).padStart(2, '0');
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await this.$http.get('${base}/platform/h5/edu/study/stats');
|
||||
if (response.data.code === 0) {
|
||||
this.stats = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadHistory() {
|
||||
try {
|
||||
const response = await this.$http.get('${base}/platform/h5/edu/study/history', {
|
||||
params: { filter: this.activeTab }
|
||||
});
|
||||
if (response.data.code === 0) {
|
||||
this.historyList = response.data.data || [];
|
||||
} else {
|
||||
this.$toast(response.data.msg || '加载失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast('网络错误,请检查网络连接');
|
||||
}
|
||||
},
|
||||
|
||||
viewCourse(courseId) {
|
||||
window.location.href = `${base}/platform/h5/edu/course/detail?courseId=${courseId}`;
|
||||
},
|
||||
|
||||
playVideo(videoId, courseId, videoTitle) {
|
||||
window.location.href = `${base}/platform/h5/edu/video/play?videoId=${videoId}&courseId=${courseId}&title=${encodeURIComponent(videoTitle)}`;
|
||||
},
|
||||
|
||||
getProgressPercent(course) {
|
||||
if (!course.videos || course.videos.length === 0) return 0;
|
||||
const completedCount = course.videos.filter(v => v.isCompleted).length;
|
||||
return Math.round((completedCount / course.videos.length) * 100);
|
||||
},
|
||||
|
||||
getVideoProgress(video) {
|
||||
if (video.isCompleted) return '已完成';
|
||||
if (video.duration > 0 && video.watchDuration > 0) {
|
||||
const percent = Math.round((video.watchDuration / video.duration) * 100);
|
||||
return `观看${percent}%`;
|
||||
}
|
||||
return '未开始';
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '未知';
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffTime = now - date;
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
|
||||
if (diffHours === 0) {
|
||||
const diffMinutes = Math.floor(diffTime / (1000 * 60));
|
||||
return diffMinutes <= 0 ? '刚刚' : `${diffMinutes}分钟前`;
|
||||
}
|
||||
return `${diffHours}小时前`;
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天';
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}天前`;
|
||||
} else {
|
||||
return date.getFullYear() + '-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getDate()).padStart(2, '0');
|
||||
}
|
||||
},
|
||||
|
||||
formatDuration(seconds) {
|
||||
if (!seconds) return '00:00';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return String(minutes).padStart(2, '0') + ':' + String(remainingSeconds).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<%}%>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.study-his-container {
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
background: linear-gradient(135deg, #1989fa, #1976d2);
|
||||
padding: 20px 15px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
background: white;
|
||||
padding: 0 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: inline-block;
|
||||
padding: 15px 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #1989fa;
|
||||
border-bottom-color: #1989fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.history-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 15px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.course-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.course-category {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 3px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.last-study-time {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
text-align: right;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.progress-percent {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #28a745;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 80px;
|
||||
height: 6px;
|
||||
background: #e9ecef;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #28a745, #20c997);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.video-list {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.video-list.expanded {
|
||||
max-height: 1000px;
|
||||
padding: 0 20px 15px;
|
||||
}
|
||||
|
||||
.video-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.video-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
margin: 0 -10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.video-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.video-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #1989fa, #1976d2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
margin-right: 15px;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.video-icon.completed {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
}
|
||||
|
||||
.video-icon.in-progress {
|
||||
background: linear-gradient(135deg, #ffc107, #ff9800);
|
||||
}
|
||||
|
||||
.video-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.video-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.video-duration {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.video-progress {
|
||||
color: #28a745;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.video-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
min-width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
color: #1989fa;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.expand-btn.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="study-his-container">
|
||||
<van-nav-bar
|
||||
title="学习统计"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
></van-nav-bar>
|
||||
|
||||
<!-- 学习统计概览 -->
|
||||
<div v-if="stats" class="stats-overview">
|
||||
<div class="stats-title">我的学习成果</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ stats.totalCourses || 0 }}</div>
|
||||
<div class="stat-label">学习课程</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ stats.completedVideos || 0 }}</div>
|
||||
<div class="stat-label">完成视频</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ Math.round((stats.totalWatchTime || 0) / 60) }}</div>
|
||||
<div class="stat-label">学习时长(分)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选标签 -->
|
||||
<div class="filter-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<van-loading type="spinner" color="#1989fa" size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<!-- 学习历史列表 -->
|
||||
<div v-else-if="historyList.length > 0" class="history-list">
|
||||
<div v-for="course in historyList" :key="course.courseId" class="history-card">
|
||||
<div class="course-header" @click="toggleCourseExpand(course.courseId)">
|
||||
<div class="course-info">
|
||||
<div class="course-title">{{ course.courseTitle || '未命名课程' }}</div>
|
||||
<div class="course-meta">
|
||||
<span class="course-category">{{ course.courseCategory || '未分类' }}</span>
|
||||
<span class="last-study-time">{{ formatDate(course.lastWatchTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-section">
|
||||
<div class="progress-percent">{{ getProgressPercent(course) }}%</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: getProgressPercent(course) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<van-icon
|
||||
name="arrow-down"
|
||||
class="expand-btn"
|
||||
:class="{ expanded: expandedCourses.includes(course.courseId) }"
|
||||
></van-icon>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="video-list"
|
||||
:class="{ expanded: expandedCourses.includes(course.courseId) }"
|
||||
>
|
||||
<div
|
||||
v-for="video in course.videos"
|
||||
:key="video.videoId"
|
||||
class="video-item"
|
||||
@click="playVideo(video.videoId, course.courseId, video.videoTitle)"
|
||||
>
|
||||
<div
|
||||
class="video-icon"
|
||||
:class="getVideoIconClass(video)"
|
||||
>
|
||||
<van-icon :name="getVideoIconName(video)" ></van-icon>
|
||||
</div>
|
||||
<div class="video-content">
|
||||
<div class="video-title">{{ video.videoTitle || '未命名视频' }}</div>
|
||||
<div class="video-meta">
|
||||
<span class="video-duration">{{ formatDuration(video.duration) }}</span>
|
||||
<span class="video-progress">{{ getVideoProgressText(video) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-time">
|
||||
{{ formatDate(video.lastWatchTime) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-state">
|
||||
<div class="empty-icon">📚</div>
|
||||
<div class="empty-text">暂无学习记录</div>
|
||||
<van-button round type="primary" size="small" @click="goToCourses">
|
||||
开始学习
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
stats: null,
|
||||
activeTab: 'all',
|
||||
tabs: [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'completed', label: '已完成' },
|
||||
{ key: 'in_progress', label: '学习中' },
|
||||
{ key: 'recent', label: '最近' }
|
||||
],
|
||||
historyList: [],
|
||||
expandedCourses: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadData();
|
||||
},
|
||||
methods: {
|
||||
onClickLeft() {
|
||||
window.location.href = '/platform/h5/edu/courses';
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadStats(),
|
||||
this.loadHistory()
|
||||
]);
|
||||
} catch (error) {
|
||||
this.$toast('加载失败,请重试');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
const response = await this.$axios.post('/platform/h5/edu/studyhis/stats');
|
||||
if (response.data.code === 0) {
|
||||
this.stats = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadHistory() {
|
||||
try {
|
||||
const response = await this.$axios.post('/platform/h5/edu/studyhis/history', {
|
||||
filter: this.activeTab
|
||||
});
|
||||
if (response.data.code === 0) {
|
||||
this.historyList = response.data.data || [];
|
||||
} else {
|
||||
this.$toast(response.data.msg || '加载失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast('网络错误,请检查网络连接');
|
||||
}
|
||||
},
|
||||
|
||||
switchTab(tabKey) {
|
||||
if (this.activeTab === tabKey) return;
|
||||
|
||||
this.activeTab = tabKey;
|
||||
this.expandedCourses = [];
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
toggleCourseExpand(courseId) {
|
||||
const index = this.expandedCourses.indexOf(courseId);
|
||||
if (index > -1) {
|
||||
this.expandedCourses.splice(index, 1);
|
||||
} else {
|
||||
this.expandedCourses.push(courseId);
|
||||
}
|
||||
},
|
||||
|
||||
playVideo(videoId, courseId, videoTitle) {
|
||||
const url = '/platform/h5/edu/video/play?videoId=' + videoId +
|
||||
'&courseId=' + courseId +
|
||||
'&title=' + encodeURIComponent(videoTitle || '');
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
goToCourses() {
|
||||
window.location.href = '/platform/h5/edu/courses';
|
||||
},
|
||||
|
||||
getProgressPercent(course) {
|
||||
if (!course.videos || course.videos.length === 0) return 0;
|
||||
const completedCount = course.videos.filter(v => v.isCompleted).length;
|
||||
return Math.round((completedCount / course.videos.length) * 100);
|
||||
},
|
||||
|
||||
getVideoIconClass(video) {
|
||||
if (video.isCompleted) return 'completed';
|
||||
if (video.watchDuration > 0) return 'in-progress';
|
||||
return '';
|
||||
},
|
||||
|
||||
getVideoIconName(video) {
|
||||
if (video.isCompleted) return 'success';
|
||||
if (video.watchDuration > 0) return 'pause-circle-o';
|
||||
return 'play-circle-o';
|
||||
},
|
||||
|
||||
getVideoProgressText(video) {
|
||||
if (video.isCompleted) return '已完成';
|
||||
if (video.duration > 0 && video.watchDuration > 0) {
|
||||
const percent = Math.round((video.watchDuration / video.duration) * 100);
|
||||
return '观看' + percent + '%';
|
||||
}
|
||||
return '未开始';
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '未知';
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffTime = now - date;
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
|
||||
if (diffHours === 0) {
|
||||
const diffMinutes = Math.floor(diffTime / (1000 * 60));
|
||||
return diffMinutes <= 0 ? '刚刚' : diffMinutes + '分钟前';
|
||||
}
|
||||
return diffHours + '小时前';
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天';
|
||||
} else if (diffDays < 7) {
|
||||
return diffDays + '天前';
|
||||
} else {
|
||||
return (date.getMonth() + 1) + '-' + date.getDate();
|
||||
}
|
||||
},
|
||||
|
||||
formatDuration(seconds) {
|
||||
if (!seconds) return '00:00';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return String(minutes).padStart(2, '0') + ':' + String(remainingSeconds).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="团长审核" placeholder fixed></van-nav-bar>
|
||||
<van-sticky offset-top="46px">
|
||||
<van-search
|
||||
v-model="pageForm.name"
|
||||
:show-action="false"
|
||||
:reverse-color="false"
|
||||
input-align="left"
|
||||
placeholder="请输入提案名称搜索"
|
||||
@search="doSearch"
|
||||
></van-search>
|
||||
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
|
||||
<van-dropdown-item v-model="pageForm.sessionId" :options="sessionOptions" :multiple="false"
|
||||
@change="doSearch"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
<van-tabs v-model="pageForm.approvalText"
|
||||
@change="(val)=>{this.pageForm.approval = val==='1';this.doSearch();}">
|
||||
<van-tab title="已审核" name="1"></van-tab>
|
||||
<van-tab title="未审核" name="0"></van-tab>
|
||||
</van-tabs>
|
||||
</van-sticky>
|
||||
|
||||
<table-list api="/platform/proposal/delegation/pageData" :page_form.sync="pageForm" @ready="onReady"
|
||||
ref="tableListRef"
|
||||
title="name">
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="提案编号">{{row.code}}</table-column>
|
||||
<table-column label="提案类别">{{row.typeName}}</table-column>
|
||||
<table-column label="提案人">{{row.createUserName}}</table-column>
|
||||
<table-column label="代表团">{{row.delegationName}}</table-column>
|
||||
<table-column label="当前节点">{{row.curTaskName}}</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<div class="action-btn" @click="onView(row)">
|
||||
<i class="fa fa-eye"></i>
|
||||
<span>查看</span>
|
||||
</div>
|
||||
</template>
|
||||
</table-list>
|
||||
|
||||
<proposal-info ref="proposalInfoRef"></proposal-info>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
<!--#include("../../common/info.js"){}#-->
|
||||
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
name: null,
|
||||
sessionId: null,
|
||||
approvalText: "0",
|
||||
approval: false
|
||||
},
|
||||
|
||||
sessionOptions: [],
|
||||
formData: {},
|
||||
showApprovalForm: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"proposal-info": PROPOSAL_INFO
|
||||
},
|
||||
methods: {
|
||||
onReady() {
|
||||
this.listSession()
|
||||
},
|
||||
listSession() {
|
||||
this.$axios.post("/platform/proposal/common/listSession").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.sessionOptions = [
|
||||
{
|
||||
text: "全部届次",
|
||||
value: null
|
||||
}
|
||||
].concat(res.data.map((v) => ({text: v.fullName, value: v.id})))
|
||||
if (this.sessionOptions.length > 0) {
|
||||
this.pageForm.sessionId = this.sessionOptions[0].value
|
||||
this.doSearch()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onView(row) {
|
||||
this.showApprovalForm = false
|
||||
this.$refs.proposalInfoRef.onOpen(row)
|
||||
},
|
||||
|
||||
doSearch() {
|
||||
this.$nextTick(() => {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.$refs.tableListRef.doSearch()
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -17,14 +17,16 @@ layout("/layouts/platform_h5.html"){
|
||||
<work v-if="homeTabbarActive === 'work'"></work>
|
||||
<mine v-if="homeTabbarActive === 'mine'"></mine>
|
||||
<apps v-if="homeTabbarActive === 'apps'"></apps>
|
||||
<msg v-if="homeTabbarActive === 'msg'"></msg>
|
||||
</div>
|
||||
|
||||
<div id="page-tarbar" class="page-tarbar">
|
||||
<van-tabbar v-model="homeTabbarActive" @change="homeTabbarChange">
|
||||
<van-tabbar-item name="home" icon="wap-home-o">首页</van-tabbar-item>
|
||||
<van-tabbar-item name="apps" icon="gem">应用</van-tabbar-item>
|
||||
<van-tabbar-item name="work" icon="gem">工作台</van-tabbar-item>
|
||||
<!-- <van-tabbar-item name="work" icon="gem">工作台</van-tabbar-item>-->
|
||||
<van-tabbar-item name="todo" icon="gem">待办</van-tabbar-item>
|
||||
<van-tabbar-item name="msg" icon="gem">消息</van-tabbar-item>
|
||||
<van-tabbar-item name="mine" icon="user-o">我的</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
</div>
|
||||
@@ -37,6 +39,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<!--#include("mine.js"){}#-->
|
||||
<!--#include("todo.js"){}#-->
|
||||
<!--#include("apps.js"){}#-->
|
||||
<!--#include("msg.js"){}#-->
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
@@ -45,7 +48,8 @@ layout("/layouts/platform_h5.html"){
|
||||
mine,
|
||||
work,
|
||||
todo,
|
||||
apps
|
||||
apps,
|
||||
msg
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
const msg= {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="message-center">
|
||||
<van-nav-bar title="消息中心" placeholder fixed></van-nav-bar>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<van-sticky offset-top="46px">
|
||||
<!-- <div class="filter-section">-->
|
||||
<!-- <van-row gutter="10">-->
|
||||
<!-- <van-col span="12">-->
|
||||
<!-- <van-field-->
|
||||
<!-- v-model="filters.type"-->
|
||||
<!-- is-link-->
|
||||
<!-- readonly-->
|
||||
<!-- label="消息类型"-->
|
||||
<!-- placeholder="选择消息类型"-->
|
||||
<!-- @click="showTypePicker = true"-->
|
||||
<!-- />-->
|
||||
<!-- </van-col>-->
|
||||
<!-- <van-col span="12" v-if="pageForm.isRead == 0">-->
|
||||
<!-- <van-button -->
|
||||
<!-- type="primary" -->
|
||||
<!-- size="small" -->
|
||||
<!-- :loading="loading"-->
|
||||
<!-- @click="markAllAsRead"-->
|
||||
<!-- style="margin-top: 6px; width: 100%;"-->
|
||||
<!-- >-->
|
||||
<!-- 全部已读-->
|
||||
<!-- </van-button>-->
|
||||
<!-- </van-col>-->
|
||||
<!-- </van-row>-->
|
||||
<!-- </div>-->
|
||||
<!-- 标签页 -->
|
||||
<van-tabs v-model="activeTab" @change="handleTabChange">
|
||||
<van-tab title="全部消息" name="-1">
|
||||
<template #title>
|
||||
<van-icon name="chat-o" />
|
||||
全部消息 ({{stats.total}})
|
||||
</template>
|
||||
</van-tab>
|
||||
<van-tab title="未读消息" name="0">
|
||||
<template #title>
|
||||
<van-icon name="bell" />
|
||||
未读消息 ({{stats.unread}})
|
||||
</template>
|
||||
</van-tab>
|
||||
<van-tab title="已读消息" name="1">
|
||||
<template #title>
|
||||
<van-icon name="passed" />
|
||||
已读消息 ({{stats.read}})
|
||||
</template>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</van-sticky>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list
|
||||
v-model="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
@click="viewMessage(message)"
|
||||
class="custom-message-item"
|
||||
:class="{'unread-message': !message.isRead}"
|
||||
>
|
||||
<div class="message-header">
|
||||
<div class="message-title-wrapper">
|
||||
<van-tag
|
||||
:type="message.type === 1 ? 'danger' : 'primary'"
|
||||
size="mini"
|
||||
style="margin-right: 8px;"
|
||||
>
|
||||
{{getMessageTypeName(message.type)}}
|
||||
</van-tag>
|
||||
<!-- <span class="message-title">{{message.title}}</span>-->
|
||||
<van-tag
|
||||
v-if="!message.isRead"
|
||||
type="warning"
|
||||
size="mini"
|
||||
style="margin-left: 8px;"
|
||||
>
|
||||
未读
|
||||
</van-tag>
|
||||
</div>
|
||||
<div class="message-time">{{formatTime(message.createdAt)}}</div>
|
||||
</div>
|
||||
<div class="message-content">{{getContentPreview(message.content)}}</div>
|
||||
<div class="message-arrow">
|
||||
<van-icon name="arrow" />
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 消息类型选择器 -->
|
||||
<van-popup v-model="showTypePicker" position="bottom">
|
||||
<van-picker
|
||||
:columns="typeColumns"
|
||||
@confirm="onTypeConfirm"
|
||||
@cancel="showTypePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<!-- 消息详情弹窗 -->
|
||||
<van-popup
|
||||
v-model="detailVisible"
|
||||
position="bottom"
|
||||
:style="{ height: '80%' }"
|
||||
closeable
|
||||
close-icon-position="top-right"
|
||||
>
|
||||
<div class="message-detail" v-if="currentMessage.id">
|
||||
<div class="detail-header">
|
||||
<h3>{{currentMessage.title}}</h3>
|
||||
<div class="detail-meta">
|
||||
<van-tag :type="currentMessage?.globalMessage?.type === 1 ? 'danger' : 'primary'">
|
||||
{{getMessageTypeName(currentMessage?.globalMessage?.type)}}
|
||||
</van-tag>
|
||||
<span class="detail-time">{{formatTime(currentMessage.createdAt)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-content" v-html="currentMessage?.globalMessage?.content"></div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
`,
|
||||
data(){
|
||||
return{
|
||||
activeTab: '-1',
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
messages: [],
|
||||
stats: {
|
||||
total: 0,
|
||||
unread: 0,
|
||||
read: 0
|
||||
},
|
||||
filters: {
|
||||
type: ''
|
||||
},
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
isRead: -1
|
||||
},
|
||||
detailVisible: false,
|
||||
currentMessage: {},
|
||||
showTypePicker: false,
|
||||
typeColumns: [
|
||||
{ text: '全部类型', value: '' },
|
||||
{ text: '系统公告', value: '1' },
|
||||
{ text: '消息通知', value: '2' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载统计数据
|
||||
async loadStats() {
|
||||
try {
|
||||
const resp = await this.$axios.get('/platform/v4/msg/stats')
|
||||
if (resp.code === 0) {
|
||||
this.stats = resp.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载消息列表
|
||||
async loadMessages(isRefresh = false) {
|
||||
if (isRefresh) {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.messages = []
|
||||
this.finished = false
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const resp = await this.$axios.post('/platform/v4/msg/pageData', this.pageForm)
|
||||
if (resp.code === 0) {
|
||||
const newMessages = resp.data.list || []
|
||||
if (isRefresh) {
|
||||
this.messages = newMessages
|
||||
} else {
|
||||
this.messages = [...this.messages, ...newMessages]
|
||||
}
|
||||
|
||||
this.pageForm.totalCount = resp.data.totalCount
|
||||
|
||||
// 判断是否还有更多数据
|
||||
if (this.messages.length >= this.pageForm.totalCount) {
|
||||
this.finished = true
|
||||
}
|
||||
} else {
|
||||
this.$toast(resp.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast('加载消息失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
},
|
||||
|
||||
// 查看消息详情
|
||||
async viewMessage(message) {
|
||||
try {
|
||||
const resp = await this.$axios.post(`/platform/v4/msg/detail/` + message.id)
|
||||
if (resp.code === 0) {
|
||||
this.currentMessage = resp.data
|
||||
this.detailVisible = true
|
||||
|
||||
// 如果是未读消息,标记为已读
|
||||
if (!message.isRead) {
|
||||
await this.markAsRead(message.id)
|
||||
message.isRead = true
|
||||
this.loadStats()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast('加载消息详情失败')
|
||||
}
|
||||
},
|
||||
|
||||
// 标记单条消息为已读
|
||||
async markAsRead(messageId) {
|
||||
try {
|
||||
await this.$axios.post(`/platform/v4/msg/read/` + messageId)
|
||||
} catch (error) {
|
||||
console.error('标记已读失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 全部标记为已读
|
||||
async markAllAsRead() {
|
||||
this.$dialog.confirm({
|
||||
title: '提示',
|
||||
message: '确定要将所有未读消息标记为已读吗?'
|
||||
}).then(async () => {
|
||||
this.loading = true
|
||||
try {
|
||||
const resp = await this.$axios.post('/platform/v4/msg/read/all')
|
||||
if (resp.code === 0) {
|
||||
this.$toast.success('操作成功')
|
||||
this.loadMessages(true)
|
||||
this.loadStats()
|
||||
} else {
|
||||
this.$toast(resp.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast('操作失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}).catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
},
|
||||
|
||||
// 切换标签页
|
||||
handleTabChange(name) {
|
||||
this.pageForm.isRead = parseInt(name)
|
||||
this.loadMessages(true)
|
||||
},
|
||||
|
||||
// 点击统计卡片切换标签
|
||||
switchTab(tabValue) {
|
||||
this.activeTab = tabValue.toString()
|
||||
this.pageForm.isRead = tabValue
|
||||
this.loadMessages(true)
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onRefresh() {
|
||||
this.loadMessages(true)
|
||||
this.loadStats()
|
||||
},
|
||||
|
||||
// 上拉加载更多
|
||||
onLoad() {
|
||||
if (this.finished) {
|
||||
return
|
||||
}
|
||||
this.pageForm.pageNumber++
|
||||
this.loadMessages()
|
||||
},
|
||||
|
||||
// 消息类型选择确认
|
||||
onTypeConfirm(value) {
|
||||
this.filters.type = value.value
|
||||
this.showTypePicker = false
|
||||
this.loadMessages(true)
|
||||
},
|
||||
|
||||
// 获取消息类型名称
|
||||
getMessageTypeName(type) {
|
||||
return type === 1 ? '系统公告' : '消息通知'
|
||||
},
|
||||
|
||||
// 获取内容预览
|
||||
getContentPreview(content) {
|
||||
if (!content) return ''
|
||||
// 移除HTML标签
|
||||
const text = content.replace(/<[^>]*>/g, '')
|
||||
return text.length > 60 ? text.substring(0, 30) + '...' : text
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timestamp) {
|
||||
return this.$moment(timestamp).format('MM-DD HH:mm')
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadStats()
|
||||
this.loadMessages(true)
|
||||
},
|
||||
|
||||
style: /*language=CSS*/ `
|
||||
.message-center {
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 自定义消息列表项样式 */
|
||||
/deep/ .custom-message-item {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/deep/ .custom-message-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/deep/ .custom-message-item:active {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/deep/ .message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/deep/ .message-time {
|
||||
font-size: 12px;
|
||||
color: #969799;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/deep/ .message-content {
|
||||
font-size: 14px;
|
||||
color: #646566;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
/deep/ .message-arrow {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #c8c9cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 筛选区域样式 */
|
||||
/deep/ .filter-section {
|
||||
padding: 12px 16px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/deep/ .message-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/deep/ .message-title {
|
||||
flex: 1;
|
||||
/*font-weight: 500;*/
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
/* 消息详情弹窗样式 */
|
||||
/deep/ .message-detail {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/deep/ .detail-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
/deep/ .detail-header h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
/deep/ .detail-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/deep/ .detail-time {
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
/deep/ .detail-content {
|
||||
line-height: 1.8;
|
||||
font-size: 15px;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
/deep/ .detail-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/deep/ .detail-content p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
/* 标签页样式优化 */
|
||||
/deep/ .van-tabs__nav {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/deep/ .van-tab {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/deep/ .van-tab--active {
|
||||
color: var(--color-primary, #1989fa);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/deep/ .van-tabs__line {
|
||||
background-color: var(--color-primary, #1989fa);
|
||||
}
|
||||
|
||||
/* 列表样式优化 */
|
||||
/deep/ .van-cell__title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/deep/ .van-cell__label {
|
||||
font-size: 13px;
|
||||
color: #969799;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/deep/ .van-cell__value {
|
||||
font-size: 12px;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
/deep/ .van-tag--mini {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
/deep/ .van-popup--bottom {
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
/* 下拉刷新和上拉加载样式 */
|
||||
/deep/ .van-pull-refresh__track {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
/deep/ .van-list__finished-text {
|
||||
color: #969799;
|
||||
font-size: 12px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
/deep/ .van-empty {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/deep/ .van-empty__description {
|
||||
color: #969799;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 简约动画效果 */
|
||||
|
||||
/* 基础过渡效果 */
|
||||
/deep/ .oa-message-item,
|
||||
/deep/ .oa-stat-item,
|
||||
/deep/ .oa-refresh-btn,
|
||||
/deep/ .oa-search-input {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* 页面进入动画 */
|
||||
/deep/ .page-enter {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 简化的响应式动画 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
/deep/ .oa-header {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
/deep/ .oa-header-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/deep/ .oa-stats-panel,
|
||||
/deep/ .oa-toolbar,
|
||||
/deep/ .oa-message-list {
|
||||
margin: 6px 12px;
|
||||
}
|
||||
|
||||
/deep/ .oa-stat-item {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
/deep/ .oa-stat-number {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/deep/ .oa-stat-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/deep/ .oa-message-item {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/deep/ .oa-message-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/deep/ .oa-message-content {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
@@ -5,15 +5,16 @@ const todo = {
|
||||
<!-- 标签页 -->
|
||||
<van-sticky offset-top="46px">
|
||||
<van-tabs v-model="activeTab" @change="handleTabChange" animated swipeable>
|
||||
<van-tab title="待办" name="todo"></van-tab>
|
||||
<van-tab title="已办" name="done"></van-tab>
|
||||
<van-tab title="我发起的" name="started"></van-tab>
|
||||
<van-tab :title="'待办(' + todoCount + ')'" name="todo"></van-tab>
|
||||
<van-tab :title="'已办(' + doneCount + ')'" name="done"></van-tab>
|
||||
<van-tab :title="'发起(' + startedCount + ')'" name="started"></van-tab>
|
||||
</van-tabs>
|
||||
<!-- 搜索筛选区域 -->
|
||||
<div class="filter-section">
|
||||
<div>
|
||||
<div class="search-form">
|
||||
<van-search v-model="pageForm.searchKeyword" placeholder="请输入搜索关键词"></van-search>
|
||||
<van-search v-model="pageForm.searchKeyword" placeholder="请输入搜索关键词"
|
||||
@search="doSearch"></van-search>
|
||||
</div>
|
||||
|
||||
<!-- <van-field-->
|
||||
@@ -39,63 +40,27 @@ const todo = {
|
||||
</van-sticky>
|
||||
|
||||
<!-- 任务列表区域 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list
|
||||
v-model="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="getTasks"
|
||||
>
|
||||
<div v-if="tasks.length > 0" class="task-list">
|
||||
<div
|
||||
v-for="task in tasks"
|
||||
:key="task.taskId"
|
||||
class="task-item"
|
||||
>
|
||||
<div class="task-content">
|
||||
<div class="task-title">{{task.variable?.instanceName || '无标题流程'}}</div>
|
||||
|
||||
<div class="task-info">
|
||||
<span class="task-info-label">任务节点:</span>
|
||||
<span class="task-info-value">{{task.taskName}}</span>
|
||||
</div>
|
||||
|
||||
<div class="task-info">
|
||||
<span class="task-info-label">流程分类:</span>
|
||||
<span class="task-info-value">
|
||||
{{categoryOptions.find(item => item.id === task.category)?.name || '未知分类'}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="task-info">
|
||||
<span class="task-info-label">申请人:</span>
|
||||
<span class="task-info-value">{{task.variable?.initiatorName || '未知'}}</span>
|
||||
</div>
|
||||
|
||||
<div class="task-info" v-if="activeTab === 'started'">
|
||||
<span class="task-info-label">状态:</span>
|
||||
<span class="task-info-value">
|
||||
{{processStatusMap[task.state]?.text || '未知状态'}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="task-footer">
|
||||
<van-button
|
||||
type="primary"
|
||||
size="mini"
|
||||
@click="openView(task)"
|
||||
>查看
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table-list :api="'/flow/todoCenter/'+ activeTab" :page_form.sync="pageForm"
|
||||
ref="tableListRef"
|
||||
@ready="doSearch"
|
||||
title="variable.instanceName">
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="任务节点">{{row.taskName}}</table-column>
|
||||
<table-column label="流程分类">{{categoryOptions.find(item => item.id === row.category)?.name ||
|
||||
'未知分类'}}
|
||||
</table-column>
|
||||
<table-column label="申请人">{{row.variable?.initiatorName || '未知'}}</table-column>
|
||||
<table-column label="状态" v-if="activeTab === 'started'">{{processStatusMap[row.state]?.text ||
|
||||
'未知状态'}}
|
||||
</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<div class="action-btn" @click="onView(row)">
|
||||
<i class="fa fa-eye"></i>
|
||||
<span>查看</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-section">
|
||||
<van-empty :description="getEmptyText()"></van-empty>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
</template>
|
||||
</table-list>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
@@ -147,8 +112,6 @@ const todo = {
|
||||
// 初始化数据
|
||||
async initData() {
|
||||
await Promise.all([this.getStatistics(), this.listCategory()]);
|
||||
// 初始加载任务
|
||||
// this.getTasks();
|
||||
},
|
||||
|
||||
// 获取统计数据
|
||||
@@ -181,61 +144,20 @@ const todo = {
|
||||
}
|
||||
},
|
||||
|
||||
// 获取任务列表
|
||||
async getTasks() {
|
||||
if (this.refreshing) {
|
||||
this.tasks = [];
|
||||
this.refreshing = false;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const {code,data,msg} = await this.$axios.post("/flow/todoCenter/" + this.activeTab, this.pageForm);
|
||||
if (code === 0) {
|
||||
this.tasks = this.tasks.concat(data.list || []);
|
||||
this.pageForm.totalCount = data.totalCount;
|
||||
this.loading = false;
|
||||
|
||||
// 数据全部加载完成
|
||||
if (this.tasks.length >= this.pageForm.totalCount) {
|
||||
this.finished = true;
|
||||
} else {
|
||||
this.pageForm.pageNumber++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast("获取任务列表失败");
|
||||
console.error("获取任务列表失败:", error);
|
||||
this.loading = false;
|
||||
this.refreshing = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onRefresh() {
|
||||
this.pageForm.pageNumber = 1;
|
||||
this.finished = false;
|
||||
this.getTasks();
|
||||
},
|
||||
|
||||
// 处理标签页变化
|
||||
handleTabChange(name) {
|
||||
this.activeTab = name;
|
||||
this.pageForm.pageNumber = 1;
|
||||
this.tasks = [];
|
||||
this.finished = false;
|
||||
this.getTasks();
|
||||
this.doSearch()
|
||||
this.getStatistics();
|
||||
},
|
||||
|
||||
// 搜索
|
||||
search() {
|
||||
this.pageForm.pageNumber = 1;
|
||||
this.tasks = [];
|
||||
this.finished = false;
|
||||
this.getTasks();
|
||||
this.getStatistics();
|
||||
doSearch() {
|
||||
this.$nextTick(() => {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.$refs.tableListRef.doSearch()
|
||||
})
|
||||
},
|
||||
|
||||
// 重置搜索
|
||||
@@ -243,7 +165,7 @@ const todo = {
|
||||
this.pageForm.searchKeyword = "";
|
||||
this.pageForm.category = "";
|
||||
this.categoryLabel = "";
|
||||
this.search();
|
||||
this.doSearch();
|
||||
},
|
||||
|
||||
// 分类选择确认
|
||||
@@ -254,7 +176,7 @@ const todo = {
|
||||
},
|
||||
|
||||
// 处理任务
|
||||
openView(task) {
|
||||
onView(task) {
|
||||
const {taskId, taskKey, instanceId, businessNo, formKey} = task;
|
||||
|
||||
if (!formKey) {
|
||||
@@ -278,10 +200,24 @@ const todo = {
|
||||
}
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
.task-center {
|
||||
margin: 0 auto;
|
||||
/* 标签页样式优化 */
|
||||
/deep/ .van-tabs__nav {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/deep/ .van-tab {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/deep/ .van-tab--active {
|
||||
color: var(--color-primary, #1989fa);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/deep/ .van-tabs__line {
|
||||
background-color: var(--color-primary, #1989fa);
|
||||
}
|
||||
|
||||
/deep/ .filter-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user