commit
This commit is contained in:
@@ -20,7 +20,7 @@ layout("/layouts/platform_h5.html"){
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="申请人">{{row.userName}}</table-column>
|
||||
<table-column label="申请时间">{{row.submitTime}}</table-column>
|
||||
<table-column label="当前节点">{{row.taskName}}</table-column>
|
||||
<!-- <table-column label="当前节点">{{row.taskName}}</table-column>-->
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<div class="action-btn" @click="onView(row)">
|
||||
|
||||
@@ -69,10 +69,11 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
.learning-filter-card {
|
||||
flex-shrink: 0;
|
||||
margin: 14px 14px 0;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
margin: 10px 10px 0;
|
||||
padding: 4px 10px 8px;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 3px 12px rgba(31, 72, 122, .08);
|
||||
}
|
||||
.learning-list-scroll {
|
||||
min-height: 0;
|
||||
@@ -82,80 +83,81 @@ layout("/layouts/platform_h5.html"){
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-filter-group {
|
||||
display: grid;
|
||||
grid-template-columns: 112px minmax(0, 1fr);
|
||||
min-height: 72px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: #eef6ff;
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
}
|
||||
.learning-filter-group + .learning-filter-group {
|
||||
margin-top: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.learning-filter-group:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.learning-filter-category {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
color: #ffffff;
|
||||
justify-content: space-between;
|
||||
min-height: 40px;
|
||||
padding: 0 2px;
|
||||
color: #26354c;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-filter-category-text {
|
||||
min-width: 0;
|
||||
.learning-filter-category-main,
|
||||
.learning-filter-category-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.learning-filter-category-title {
|
||||
font-size: 15px;
|
||||
margin-left: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.learning-filter-category-main .van-icon {
|
||||
color: #1677ff;
|
||||
font-size: 17px;
|
||||
}
|
||||
.learning-filter-selected-count {
|
||||
color: #97a4b7;
|
||||
font-size: 11px;
|
||||
}
|
||||
.learning-filter-reset,
|
||||
.learning-filter-collapse {
|
||||
margin-left: 12px;
|
||||
color: #4d92ec;
|
||||
font-size: 11px;
|
||||
}
|
||||
.learning-filter-collapse .van-icon {
|
||||
margin-left: 2px;
|
||||
font-size: 11px;
|
||||
vertical-align: -1px;
|
||||
}
|
||||
.learning-filter-options {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
background: rgba(255, 255, 255, .55);
|
||||
}
|
||||
.learning-filter-options::-webkit-scrollbar {
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px 9px;
|
||||
padding: 0 1px 10px;
|
||||
}
|
||||
.learning-filter-item {
|
||||
flex: 0 0 72px;
|
||||
min-width: 72px;
|
||||
padding: 8px 2px 7px;
|
||||
border-left: 1px solid rgba(255, 255, 255, .72);
|
||||
flex: 1 0 calc((100% - 18px) / 3);
|
||||
min-width: 0;
|
||||
height: 22px;
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #dce5f1;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
color: #5f6b7a;
|
||||
color: #718096;
|
||||
font-size: 11px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-filter-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 auto 5px;
|
||||
border-radius: 50%;
|
||||
color: #4f87c8;
|
||||
font-size: 18px;
|
||||
background: rgba(255, 255, 255, .78);
|
||||
}
|
||||
.learning-filter-item.no-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 58px;
|
||||
color: #0b75bd;
|
||||
font-weight: 700;
|
||||
}
|
||||
.learning-filter-item.active {
|
||||
color: #0b75bd;
|
||||
font-weight: 700;
|
||||
background: rgba(255, 255, 255, .86);
|
||||
}
|
||||
.learning-filter-item.active .learning-filter-icon {
|
||||
color: #ffffff;
|
||||
background: #0b75bd;
|
||||
font-weight: 700;
|
||||
background: #287ff0;
|
||||
border-color: #287ff0;
|
||||
box-shadow: 0 2px 5px rgba(40, 127, 240, .2);
|
||||
}
|
||||
.learning-filter-name {
|
||||
overflow: hidden;
|
||||
@@ -275,28 +277,9 @@ layout("/layouts/platform_h5.html"){
|
||||
padding: 22px 0 36px;
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.learning-filter-card {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
.learning-filter-group {
|
||||
grid-template-columns: 94px minmax(0, 1fr);
|
||||
}
|
||||
.learning-filter-category {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
.learning-filter-category-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
.learning-filter-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 17px;
|
||||
}
|
||||
.learning-filter-item {
|
||||
font-size: 11px;
|
||||
}
|
||||
.learning-filter-card { margin-left: 8px; margin-right: 8px; padding-left: 8px; padding-right: 8px; }
|
||||
.learning-filter-reset, .learning-filter-collapse { margin-left: 8px; }
|
||||
.learning-filter-item { padding-left: 4px; padding-right: 4px; }
|
||||
.learning-course-cover {
|
||||
flex-basis: 120px;
|
||||
width: 120px;
|
||||
@@ -328,21 +311,24 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
<div class="learning-filter-card">
|
||||
<div class="learning-filter-group" v-for="group in filterGroups" :key="group.key">
|
||||
<div class="learning-filter-category" :style="{background: group.color}">
|
||||
<div class="learning-filter-category-text">
|
||||
<div class="learning-filter-category">
|
||||
<div class="learning-filter-category-main">
|
||||
<van-icon :name="group.icon"></van-icon>
|
||||
<div class="learning-filter-category-title">{{ group.name }}</div>
|
||||
</div>
|
||||
<div class="learning-filter-category-actions">
|
||||
<span class="learning-filter-selected-count">已选{{ selectedFilterCount(group) }}项</span>
|
||||
<span class="learning-filter-reset" @click.stop="resetGroupFilter(group)">重置</span>
|
||||
<span class="learning-filter-collapse" @click.stop="toggleFilterGroup(group.key)">{{ filterCollapsed[group.key] ? '展开' : '收起' }}<van-icon :name="filterCollapsed[group.key] ? 'arrow-down' : 'arrow-up'"></van-icon></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="learning-filter-options">
|
||||
<div v-show="!filterCollapsed[group.key]" class="learning-filter-options">
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
class="learning-filter-item"
|
||||
:class="{active: isFilterActive(item), 'no-icon': !item.icon}"
|
||||
:class="{active: isFilterActive(item)}"
|
||||
@click="selectFilter(item)">
|
||||
<div v-if="item.icon" class="learning-filter-icon">
|
||||
<van-icon :name="item.icon"></van-icon>
|
||||
</div>
|
||||
<div class="learning-filter-name">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -351,7 +337,7 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
<div class="learning-list-scroll">
|
||||
<div class="learning-section-head">
|
||||
<div class="learning-section-title">{{ listTitle }}</div>
|
||||
<div class="learning-section-title">我的课堂</div>
|
||||
<div class="learning-section-total">共 {{ pageForm.totalCount || 0 }} 门</div>
|
||||
</div>
|
||||
|
||||
@@ -416,36 +402,35 @@ layout("/layouts/platform_h5.html"){
|
||||
loading: false,
|
||||
finished: false,
|
||||
refreshing: false,
|
||||
requestSeq: 0
|
||||
requestSeq: 0,
|
||||
filterCollapsed: { recommend: false, type: false }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterGroups() {
|
||||
const recommendItems = this.recommendOptions.map((item, index) => ({
|
||||
const recommendItems = [{ key: "recommend_all", type: "recommend", value: "", name: "全部" }].concat(this.recommendOptions.map((item) => ({
|
||||
key: "recommend_" + item.code,
|
||||
type: "recommend",
|
||||
value: item.code,
|
||||
name: item.name,
|
||||
icon: this.recommendIcon(index)
|
||||
}))
|
||||
const typeItems = this.courseTypeOptions.map((item, index) => ({
|
||||
name: item.name
|
||||
})))
|
||||
const typeItems = [{ key: "type_all", type: "type", value: "", name: "全部" }].concat(this.courseTypeOptions.map((item) => ({
|
||||
key: "type_" + item.id,
|
||||
type: "type",
|
||||
value: item.id,
|
||||
name: item.typeName,
|
||||
icon: this.typeIcon(index)
|
||||
}))
|
||||
name: item.typeName
|
||||
})))
|
||||
return [
|
||||
{
|
||||
key: "recommend",
|
||||
name: "推荐标识",
|
||||
color: "linear-gradient(135deg, #ff8a3d, #ffbf5e)",
|
||||
icon: "label-o",
|
||||
items: recommendItems
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
name: "课程类型",
|
||||
color: "linear-gradient(135deg, #2878d7, #64a8f4)",
|
||||
icon: "apps-o",
|
||||
items: typeItems
|
||||
}
|
||||
]
|
||||
@@ -538,6 +523,26 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
this.doSearch()
|
||||
},
|
||||
// 切换指定筛选组的展开状态,不改变该组已选择的查询条件。
|
||||
toggleFilterGroup(groupKey) {
|
||||
this.$set(this.filterCollapsed, groupKey, !this.filterCollapsed[groupKey])
|
||||
},
|
||||
// 重置指定筛选组并重新查询;推荐标识和课程类型可独立恢复为全部。
|
||||
resetGroupFilter(group) {
|
||||
if (!group) return
|
||||
if (group.key === "recommend") {
|
||||
this.query.recommendFlag = ""
|
||||
} else if (group.key === "type") {
|
||||
this.query.courseTypeId = ""
|
||||
}
|
||||
this.doSearch()
|
||||
},
|
||||
selectedFilterCount(group) {
|
||||
if (!group) return 0
|
||||
if (group.key === "recommend") return this.query.recommendFlag ? 1 : 0
|
||||
if (group.key === "type") return this.query.courseTypeId ? 1 : 0
|
||||
return 0
|
||||
},
|
||||
isFilterActive(item) {
|
||||
if (!item) return false
|
||||
if (item.type === "recommend") {
|
||||
|
||||
@@ -364,25 +364,33 @@ layout("/layouts/platform_h5.html"){
|
||||
v-if="selectedResource.resourceType === 'video'"
|
||||
ref="mediaPlayer"
|
||||
class="study-media"
|
||||
:src="fileUrl"
|
||||
:src="mediaUrl"
|
||||
controls
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-video-player-type="h5"
|
||||
x5-video-orientation="landscape|portrait"
|
||||
x-webkit-airplay="allow"
|
||||
@loadedmetadata="restoreMediaPosition"
|
||||
@seeked="handleMediaSeeked"
|
||||
@seeking="handleMediaSeeking"
|
||||
@play="startStudy"
|
||||
@pause="saveMediaPosition(false)"
|
||||
@timeupdate="saveProgress"
|
||||
@ended="finishStudy(false)">
|
||||
@ended="finishStudy(false, true)">
|
||||
</video>
|
||||
<div v-else-if="selectedResource.resourceType === 'audio'" class="study-audio-wrap">
|
||||
<audio
|
||||
ref="mediaPlayer"
|
||||
:src="fileUrl"
|
||||
controls
|
||||
@loadedmetadata="restoreMediaPosition"
|
||||
@seeked="handleMediaSeeked"
|
||||
@seeking="handleMediaSeeking"
|
||||
@play="startStudy"
|
||||
@pause="saveMediaPosition(false)"
|
||||
@timeupdate="saveProgress"
|
||||
@ended="finishStudy(false)">
|
||||
@ended="finishStudy(false, true)">
|
||||
</audio>
|
||||
</div>
|
||||
<img v-else-if="selectedResource.resourceType === 'image'" class="study-image-preview" :src="fileUrl" :alt="selectedResource.title">
|
||||
@@ -412,6 +420,14 @@ layout("/layouts/platform_h5.html"){
|
||||
studying: false,
|
||||
pendingSeconds: 0,
|
||||
heartbeatTimer: null,
|
||||
resumePositionSeconds: 0,
|
||||
resumeSeekRetryCount: 0,
|
||||
resumePositionRestored: false,
|
||||
lastSavedPositionSeconds: 0,
|
||||
lastPositionSyncAt: 0,
|
||||
lastAllowedPositionSeconds: 0,
|
||||
restoringMediaPosition: false,
|
||||
pageVisibilityHandler: null,
|
||||
navigatingBack: false
|
||||
}
|
||||
},
|
||||
@@ -422,6 +438,12 @@ layout("/layouts/platform_h5.html"){
|
||||
fileUrl() {
|
||||
return this.getFileUrl(this.selectedResource.fileData)
|
||||
},
|
||||
mediaUrl() {
|
||||
if (this.selectedResource.resourceType !== "video") return this.fileUrl
|
||||
const fileId = this.getFileId(this.selectedResource.fileData)
|
||||
// 视频必须使用支持 Range 的播放接口,否则手动拖动和历史位置恢复都会失效。
|
||||
return fileId ? "/platform/sys/file/videoPlay?id=" + encodeURIComponent(fileId) : this.fileUrl
|
||||
},
|
||||
inlinePreviewUrl() {
|
||||
const id = this.getFileId(this.selectedResource.fileData)
|
||||
return id ? this.apiBase + "/pdfPreview?id=" + encodeURIComponent(id) : this.fileUrl
|
||||
@@ -502,31 +524,99 @@ layout("/layouts/platform_h5.html"){
|
||||
this.openResource(chapter.resources[0])
|
||||
}
|
||||
},
|
||||
progressKey(resource) {
|
||||
const target = resource || this.selectedResource
|
||||
return "learning-progress-" + this.courseId + "-" + (target.id || "")
|
||||
},
|
||||
getResumePosition(resource) {
|
||||
// 仅以当前登录人的服务端学习记录续播,避免共用设备中的本地缓存导致首次学习被错误快进。
|
||||
const serverPosition = Number(resource.lastPositionSeconds || 0)
|
||||
return Math.max(0, serverPosition)
|
||||
},
|
||||
openResource(resource) {
|
||||
if (!resource || !resource.id) return
|
||||
this.pauseMedia()
|
||||
this.finishStudy(false)
|
||||
this.selectedResource = resource
|
||||
this.resumePositionSeconds = this.getResumePosition(resource)
|
||||
this.resumeSeekRetryCount = 0
|
||||
this.resumePositionRestored = this.resumePositionSeconds <= 0
|
||||
this.lastSavedPositionSeconds = 0
|
||||
this.lastPositionSyncAt = 0
|
||||
this.lastAllowedPositionSeconds = Number(resource.maxPositionSeconds || 0)
|
||||
this.restoringMediaPosition = false
|
||||
this.playerVisible = true
|
||||
if (!["video", "audio"].includes(resource.resourceType)) {
|
||||
this.$nextTick(() => this.startStudy())
|
||||
} else {
|
||||
this.$nextTick(() => this.resumeMediaPosition())
|
||||
this.$nextTick(() => this.restoreMediaPosition())
|
||||
}
|
||||
},
|
||||
resumeMediaPosition() {
|
||||
restoreMediaPosition() {
|
||||
const player = this.$refs.mediaPlayer
|
||||
const position = Number(this.selectedResource.lastPositionSeconds || 0)
|
||||
if (player && position > 0) {
|
||||
player.currentTime = position
|
||||
if (!player || !this.selectedResource.id || this.resumePositionRestored || this.resumePositionSeconds <= 0 || player.readyState < 1) return
|
||||
let targetPosition = this.resumePositionSeconds
|
||||
if (Number.isFinite(player.duration) && player.duration > 0) {
|
||||
// 已完成或接近结束的记录回退两秒,防止恢复后立即触发播放结束。
|
||||
targetPosition = Math.min(this.resumePositionSeconds, Math.max(0, Math.floor(player.duration) - 2))
|
||||
}
|
||||
if (targetPosition <= 0) {
|
||||
this.lastAllowedPositionSeconds = 0
|
||||
this.restoringMediaPosition = false
|
||||
this.resumePositionRestored = true
|
||||
return
|
||||
}
|
||||
this.resumePositionSeconds = targetPosition
|
||||
this.restoringMediaPosition = true
|
||||
player.currentTime = targetPosition
|
||||
},
|
||||
verifyMediaPosition() {
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (!player || this.resumePositionRestored || this.resumePositionSeconds <= 0) return
|
||||
const actualPosition = Number(player.currentTime || 0)
|
||||
if (Math.abs(actualPosition - this.resumePositionSeconds) <= 1) {
|
||||
this.lastAllowedPositionSeconds = actualPosition
|
||||
this.restoringMediaPosition = false
|
||||
this.resumePositionRestored = true
|
||||
return
|
||||
}
|
||||
if (this.resumeSeekRetryCount < 1) {
|
||||
this.resumeSeekRetryCount += 1
|
||||
// 部分移动端容器首次定位会被播放器重置,收到 seeked 后再校验并重试一次。
|
||||
setTimeout(() => this.restoreMediaPosition(), 100)
|
||||
} else {
|
||||
this.lastAllowedPositionSeconds = actualPosition
|
||||
this.restoringMediaPosition = false
|
||||
this.resumePositionRestored = true
|
||||
}
|
||||
},
|
||||
// 原生播放器点击进度条会在 seeking 后再次写入时间,因此开始定位和完成定位均需限制向前跳转;回退复习不受影响。
|
||||
restoreBlockedMediaPosition() {
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (!player || this.selectedResource.allowDrag !== false || this.restoringMediaPosition) return false
|
||||
const allowedPosition = Math.max(0, Number(this.lastAllowedPositionSeconds || 0))
|
||||
if (Number(player.currentTime || 0) <= allowedPosition + 0.25) return false
|
||||
player.currentTime = allowedPosition
|
||||
setTimeout(() => {
|
||||
if (!this.restoringMediaPosition && this.selectedResource.allowDrag === false && Number(player.currentTime || 0) > allowedPosition + 0.25) {
|
||||
player.currentTime = allowedPosition
|
||||
}
|
||||
}, 0)
|
||||
return true
|
||||
},
|
||||
handleMediaSeeking() {
|
||||
this.restoreBlockedMediaPosition()
|
||||
},
|
||||
handleMediaSeeked() {
|
||||
this.restoreBlockedMediaPosition()
|
||||
this.verifyMediaPosition()
|
||||
},
|
||||
async startStudy() {
|
||||
if (!this.selectedResource.id || this.studying) return
|
||||
const resp = await this.$axios.post(this.recordApi + "/start", {
|
||||
courseId: this.courseId,
|
||||
resourceId: this.selectedResource.id,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
positionSeconds: this.resumePositionRestored ? this.getMediaPosition() : this.resumePositionSeconds
|
||||
})
|
||||
if (resp.code !== 0) {
|
||||
this.$toast(resp.msg || "开始学习失败")
|
||||
@@ -551,7 +641,8 @@ layout("/layouts/platform_h5.html"){
|
||||
this.applyRecordState(resp.data)
|
||||
}
|
||||
},
|
||||
async finishStudy(force) {
|
||||
// force 用于页面销毁时静默结束;completed 仅在视频或音频自然播放结束时传 true。
|
||||
async finishStudy(force, completed) {
|
||||
this.pauseMedia()
|
||||
if (!this.currentSegmentId) return
|
||||
const segmentId = this.currentSegmentId
|
||||
@@ -563,7 +654,8 @@ layout("/layouts/platform_h5.html"){
|
||||
await this.$axios.post(this.recordApi + "/finish", {
|
||||
segmentId: segmentId,
|
||||
activeSeconds: seconds,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
positionSeconds: this.getMediaPosition(),
|
||||
completed: !!completed
|
||||
})
|
||||
if (force) return
|
||||
this.loadTree()
|
||||
@@ -591,7 +683,55 @@ layout("/layouts/platform_h5.html"){
|
||||
saveProgress() {
|
||||
const position = this.getMediaPosition()
|
||||
if (!position || !this.selectedResource.id) return
|
||||
if (this.selectedResource.allowDrag === false && !this.restoringMediaPosition && position > Number(this.lastAllowedPositionSeconds || 0) + 2) {
|
||||
this.restoreBlockedMediaPosition()
|
||||
return
|
||||
}
|
||||
this.lastAllowedPositionSeconds = Math.max(Number(this.lastAllowedPositionSeconds || 0), position)
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", position)
|
||||
try {
|
||||
localStorage.setItem(this.progressKey(), String(position))
|
||||
} catch (e) {
|
||||
// 无痕模式或受限容器可能禁用本地存储,继续使用服务端记录。
|
||||
}
|
||||
if (Date.now() - this.lastPositionSyncAt >= 5000) {
|
||||
this.saveServerPosition(position, false)
|
||||
}
|
||||
},
|
||||
saveMediaPosition(useBeacon) {
|
||||
const position = this.getMediaPosition()
|
||||
if (!this.selectedResource.id || position <= 0) return
|
||||
try {
|
||||
localStorage.setItem(this.progressKey(), String(position))
|
||||
} catch (e) {
|
||||
// 无痕模式或受限容器可能禁用本地存储,继续使用服务端记录。
|
||||
}
|
||||
this.saveServerPosition(position, useBeacon)
|
||||
},
|
||||
saveServerPosition(position, useBeacon) {
|
||||
if (!this.selectedResource.id || !["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
const seconds = Math.max(0, Math.floor(Number(position || 0)))
|
||||
this.lastPositionSyncAt = Date.now()
|
||||
this.lastSavedPositionSeconds = seconds
|
||||
if (useBeacon && navigator.sendBeacon) {
|
||||
const formData = new URLSearchParams()
|
||||
formData.append("courseId", this.courseId)
|
||||
formData.append("resourceId", this.selectedResource.id)
|
||||
formData.append("positionSeconds", String(seconds))
|
||||
const body = new Blob([formData.toString()], { type: "application/x-www-form-urlencoded;charset=UTF-8" })
|
||||
navigator.sendBeacon(this.recordApi + "/position", body)
|
||||
return
|
||||
}
|
||||
// 播放中每五秒、暂停时立即同步,确保服务端续播位置不会只依赖学习心跳。
|
||||
this.$axios.post(this.recordApi + "/position", {
|
||||
courseId: this.courseId,
|
||||
resourceId: this.selectedResource.id,
|
||||
positionSeconds: seconds
|
||||
}).then((resp) => {
|
||||
if (resp.code !== 0) {
|
||||
this.lastSavedPositionSeconds = 0
|
||||
}
|
||||
})
|
||||
},
|
||||
applyRecordState(data) {
|
||||
if (!this.selectedResource.id || !data) return
|
||||
@@ -601,6 +741,11 @@ layout("/layouts/platform_h5.html"){
|
||||
if (data.lastPositionSeconds !== undefined) {
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", Number(data.lastPositionSeconds || 0))
|
||||
}
|
||||
if (data.maxPositionSeconds !== undefined) {
|
||||
const maxPositionSeconds = Number(data.maxPositionSeconds || 0)
|
||||
this.$set(this.selectedResource, "maxPositionSeconds", maxPositionSeconds)
|
||||
this.lastAllowedPositionSeconds = Math.max(Number(this.lastAllowedPositionSeconds || 0), maxPositionSeconds)
|
||||
}
|
||||
},
|
||||
getMediaPosition() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return 0
|
||||
@@ -709,6 +854,11 @@ layout("/layouts/platform_h5.html"){
|
||||
fragment: "#container",
|
||||
timeout: 8000
|
||||
})
|
||||
},
|
||||
handlePageVisibility(event) {
|
||||
if (document.hidden || (event && event.type === "pagehide")) {
|
||||
this.saveMediaPosition(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -719,10 +869,18 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
await this.loadCourse()
|
||||
await this.loadTree()
|
||||
this.pageVisibilityHandler = (event) => this.handlePageVisibility(event)
|
||||
document.addEventListener("visibilitychange", this.pageVisibilityHandler)
|
||||
window.addEventListener("pagehide", this.pageVisibilityHandler)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.saveMediaPosition(true)
|
||||
this.finishStudy(true)
|
||||
this.stopHeartbeatTimer()
|
||||
if (this.pageVisibilityHandler) {
|
||||
document.removeEventListener("visibilitychange", this.pageVisibilityHandler)
|
||||
window.removeEventListener("pagehide", this.pageVisibilityHandler)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -698,7 +698,7 @@ layout("/layouts/platform_h5.html"){
|
||||
:name="option.id"
|
||||
v-model="selectedRadioId"
|
||||
@click.stop="isDeadlinePassed ? $toast.fail('已过选择截止时间,无法修改') : selectRadioOption(option.id)"
|
||||
:disabled="isDeadlinePassed"
|
||||
:disabled="isDeadlinePassed || (systemDefaultOption && systemDefaultOption.id !== option.id)"
|
||||
></van-radio>
|
||||
</div>
|
||||
|
||||
@@ -717,7 +717,7 @@ layout("/layouts/platform_h5.html"){
|
||||
integer
|
||||
disable-input
|
||||
:default-value="0"
|
||||
:min="0"
|
||||
:min="option.isSystemDefault ? 1 : 0"
|
||||
:disabled="isDeadlinePassed"
|
||||
input-width="40px"
|
||||
button-size="22px"
|
||||
@@ -899,6 +899,12 @@ layout("/layouts/platform_h5.html"){
|
||||
|
||||
computed: {
|
||||
|
||||
// 系统默认福利必须被保留,管理端限制每个项目最多配置一个。
|
||||
systemDefaultOption() {
|
||||
if (!this.projectInfo.options) return null
|
||||
return this.projectInfo.options.find((option) => option.isSystemDefault) || null
|
||||
},
|
||||
|
||||
hasSelection() {
|
||||
if (this.projectInfo.isCheckBox === "radio") {
|
||||
return !!this.selectedRadioId
|
||||
@@ -961,6 +967,11 @@ layout("/layouts/platform_h5.html"){
|
||||
},
|
||||
selectNumChange(index, newValue) {
|
||||
const option = this.projectInfo.options[index];
|
||||
if (option.isSystemDefault && Number(newValue) < 1) {
|
||||
this.$set(option, "selectNum", 1)
|
||||
this.$toast.fail("系统默认福利不可取消")
|
||||
return
|
||||
}
|
||||
const maxSelect = this.projectInfo.multiSelectNum || this.projectInfo.options.length;
|
||||
|
||||
// 计算其他所有选项的总和(不包括当前修改的选项)
|
||||
@@ -1046,6 +1057,11 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
}
|
||||
|
||||
// 首次进入且没有历史选择时,自动带出系统默认福利。
|
||||
if (this.projectInfo.options && this.userSelection.length === 0) {
|
||||
this.applySystemDefaultOption()
|
||||
}
|
||||
|
||||
// 设置已选择的选项
|
||||
if (this.projectInfo.options && this.userSelection.length > 0) {
|
||||
// 单选模式
|
||||
@@ -1270,6 +1286,11 @@ layout("/layouts/platform_h5.html"){
|
||||
this.$toast.fail("已过选择截止时间,无法修改")
|
||||
return
|
||||
}
|
||||
if (this.systemDefaultOption && this.systemDefaultOption.id !== optionId) {
|
||||
this.selectedRadioId = this.systemDefaultOption.id
|
||||
this.$toast.fail("系统默认福利不可取消")
|
||||
return
|
||||
}
|
||||
this.selectedRadioId = optionId
|
||||
|
||||
// 重置所有选项的selectNum
|
||||
@@ -1284,6 +1305,16 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
},
|
||||
|
||||
// 默认项只在首次选择时自动带入,已有历史选择不自动覆盖。
|
||||
applySystemDefaultOption() {
|
||||
if (!this.systemDefaultOption) return
|
||||
if (this.projectInfo.isCheckBox === "radio") {
|
||||
this.selectRadioOption(this.systemDefaultOption.id)
|
||||
} else {
|
||||
this.$set(this.systemDefaultOption, "selectNum", 1)
|
||||
}
|
||||
},
|
||||
|
||||
// 显示选项详情
|
||||
showOptionDetail(option) {
|
||||
this.selectedOption = option
|
||||
|
||||
Reference in New Issue
Block a user