..
This commit is contained in:
@@ -19,14 +19,7 @@ const basicForm = {
|
||||
></file-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="课程分类" prop="category">
|
||||
<el-select v-model="formData.category" placeholder="请选择分类" style="width: 100%;">
|
||||
<el-option label="前端开发" value="前端开发"></el-option>
|
||||
<el-option label="后端开发" value="后端开发"></el-option>
|
||||
<el-option label="移动开发" value="移动开发"></el-option>
|
||||
<el-option label="数据库" value="数据库"></el-option>
|
||||
<el-option label="运维部署" value="运维部署"></el-option>
|
||||
<el-option label="人工智能" value="人工智能"></el-option>
|
||||
</el-select>
|
||||
<dict-select v-model="formData.category" code="TRAIN_EDU_COURSE_TYPE" style="width: 100%"></dict-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否启用">
|
||||
<el-switch v-model="formData.disabled" :active-value="false" :inactive-value="true"></el-switch>
|
||||
|
||||
@@ -161,8 +161,6 @@ const contentForm = {
|
||||
</video>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
|
||||
</el-dialog>
|
||||
`,
|
||||
data() {
|
||||
@@ -352,12 +350,12 @@ const contentForm = {
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
|
||||
.edu_content_dialog .el-dialog__body {
|
||||
::v-deep .edu_content_dialog .el-dialog__body {
|
||||
height: calc(100vh - 155px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
::v-deep .section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -365,7 +363,7 @@ const contentForm = {
|
||||
padding: 20px 20px 0;
|
||||
}
|
||||
|
||||
.video-item {
|
||||
::v-deep .video-item {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
@@ -374,19 +372,19 @@ const contentForm = {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.video-item:hover {
|
||||
::v-deep .video-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.chapter-card {
|
||||
::v-deep .chapter-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chapter-header {
|
||||
::v-deep .chapter-header {
|
||||
background: #f5f7fa;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
@@ -395,11 +393,11 @@ const contentForm = {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chapter-content {
|
||||
::v-deep .chapter-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
::v-deep .stats-card {
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
@@ -407,14 +405,14 @@ const contentForm = {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
::v-deep .course-cover {
|
||||
width: 100px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.duration-display {
|
||||
::v-deep .duration-display {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.course-cover{
|
||||
width: 100px;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<table-tool>
|
||||
@@ -9,19 +16,12 @@ layout("/layouts/platform.html"){
|
||||
</table-tool>
|
||||
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="title" label="课程标题" min-width="150">
|
||||
<el-table-column prop="cover" label="课程封面" width="150">
|
||||
<template slot-scope="scope">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<img v-if="scope.row.cover" :src="scope.row.cover" class="course-cover"
|
||||
style="margin-right: 10px;">
|
||||
<div v-else
|
||||
style="width: 100px; height: 60px; background: #f0f0f0; border-radius: 4px; margin-right: 10px; display: flex; align-items: center; justify-content: center; color: #999;">
|
||||
无封面
|
||||
</div>
|
||||
<strong>{{ scope.row.title }}</strong>
|
||||
</div>
|
||||
<img v-if="scope.row.cover" :src="scope.row.cover" class="course-cover">
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="课程标题" min-width="150"></el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="category" label="分类" width="120">
|
||||
<template slot-scope="scope">
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
<div id="suggestion-box-apply-form" v-cloak>
|
||||
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":" class="flow-task-form">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="姓名">{{formData.userName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工号">{{formData.loginName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="单位">{{formData.unitName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工会">{{formData.unionName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">
|
||||
<el-form-item prop="title" label="标题">
|
||||
<el-input v-model="formData.title" maxlength="100" show-word-limit placeholder="请输入标题"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="填写意见建议内容" :span="2">
|
||||
<el-form-item prop="content" label="填写意见建议内容">
|
||||
<el-input v-model="formData.content" type="textarea" maxlength="500" show-word-limit placeholder="请输入内容"></el-input>
|
||||
</el-form-item>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-form>
|
||||
<snaker-flow-task-form-action @task-action="handleTaskAction" @save-draft="handleSaveDraft"></snaker-flow-task-form-action>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#suggestion-box-apply-form",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
businessId: GetQueryString("businessId"),
|
||||
instanceId: GetQueryString("instanceId"),
|
||||
formData: {},
|
||||
formRules: {
|
||||
title: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
content: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTaskAction(val) {
|
||||
console.log(val)
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (valid) {
|
||||
if (!this.instanceId) {
|
||||
this.handleApply(val)
|
||||
} else {
|
||||
this.handleReApply(val)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 提交申请
|
||||
handleApply(val) {
|
||||
this.$axios
|
||||
.post("/flow/common/startInstanceAndExecute", {
|
||||
...val,
|
||||
bizData: JSON.stringify(this.formData)
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
window.GlobalBroadcastChannel.postMessage({
|
||||
type: "task-complete"
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 重新提交申请
|
||||
handleReApply(val) {
|
||||
this.$axios
|
||||
.post("/flow/common/executeTask", {
|
||||
data: JSON.stringify({
|
||||
processTaskId: val.taskId,
|
||||
processInstanceId: val.instanceId,
|
||||
submitType: val.submitType,
|
||||
f_data: JSON.stringify(this.formData)
|
||||
})
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
window.GlobalBroadcastChannel.postMessage({
|
||||
type: "task-complete"
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 保存申请
|
||||
handleSaveDraft(val) {
|
||||
this.$refs.formRef.validateField(["title"], (errMsg) => {
|
||||
if (errMsg) {
|
||||
this.$message.warning("请填写标题")
|
||||
return
|
||||
}
|
||||
this.$axios
|
||||
.post("/flow/common/startInstance", {
|
||||
...val,
|
||||
bizData: JSON.stringify(this.formData)
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化
|
||||
init() {
|
||||
if (this.businessId) {
|
||||
this.$axios.post("/platform/suggestionBox/view/info", { id: this.businessId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.formData = res.data
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const { username, loginname, id, unit, union, mobile } = this.$store.state.user
|
||||
this.formData = {
|
||||
userName: username,
|
||||
loginName: loginname,
|
||||
submitterId: id,
|
||||
unitId: unit?.id,
|
||||
unitName: unit?.name,
|
||||
unionId: union?.id,
|
||||
unionName: union?.name,
|
||||
concat: mobile
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,128 +0,0 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<guava ref="guava">
|
||||
<div id="app">
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度">
|
||||
<el-date-picker v-model="pageForm.year" type="year" value-format="yyyy" placeholder="年度" style="width: 100%"></el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="标题">
|
||||
<el-input v-model="pageForm.title" placeholder="标题" clearable></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<table-tool></table-tool>
|
||||
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="title" label="投稿标题"></el-table-column>
|
||||
<el-table-column prop="loginName" label="投稿人工号"></el-table-column>
|
||||
<el-table-column prop="userName" label="投稿人姓名"></el-table-column>
|
||||
<el-table-column prop="unitName" label="投稿人单位"></el-table-column>
|
||||
<el-table-column prop="unionName" label="投稿人工会"></el-table-column>
|
||||
<el-table-column prop="submitTime" label="投稿时间"></el-table-column>
|
||||
<el-table-column prop="taskName" label="当前节点"></el-table-column>
|
||||
<el-table-column prop="instanceState" label="流程状态">
|
||||
<template slot-scope="{row}">
|
||||
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="300px">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
|
||||
<el-button v-if="row.taskKey === 'startTask'" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||
<el-button v-if="row.canRevoke" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
|
||||
<!-- <el-button v-if="row.instanceState === 10" size="mini" type="danger" @click="onWithDraw(row)">撤销</el-button>-->
|
||||
<!-- v-if="row.instanceState === 30"-->
|
||||
<el-button @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</div>
|
||||
</guava>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
pageDataUrl: "/platform/suggestionBox2/apply/pageData"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openView(row) {
|
||||
window.open("/flow/common/approval/form?" + "instanceId=" + (row.instanceId || "") + "&businessId=" + row.id + "&defineKey=XWTG")
|
||||
},
|
||||
onEdit(row) {
|
||||
window.open(
|
||||
"/flow/common/approval/form?taskId=" +
|
||||
(row.taskId || "") +
|
||||
"&instanceId=" +
|
||||
(row.instanceId || "") +
|
||||
"&businessId=" +
|
||||
row.id +
|
||||
"&defineKey=XWTG"
|
||||
)
|
||||
},
|
||||
|
||||
onRevoke(row) {
|
||||
this.$confirm("您确定要撤回吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post("/flow/common/revokeTask", { taskId: row.startTaskId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
onWithDraw({ instanceId }) {
|
||||
this.$confirm("您确定要撤销申请吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post("/flow/common/withdrawInstance", { instanceId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
onDelete(id) {
|
||||
this.$confirm("您确定要删除吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post("/platform/suggestionBox2/apply/delete", { id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(res.msg)
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -1,88 +0,0 @@
|
||||
<div id="suggestion-box-view">
|
||||
<!-- <el-form ref="formRef" label-width="0" label-suffix=":" class="flow-task-form">-->
|
||||
<el-descriptions :column="2" border class="flow-task-form">
|
||||
<el-descriptions-item label="姓名">{{viewData.userName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工号">{{viewData.loginName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="单位">{{viewData.unitName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="工会">{{viewData.unionName}}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">{{viewData.title}}</el-descriptions-item>
|
||||
<el-descriptions-item label="填写意见建议内容" :span="2">{{viewData.content}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template v-for="task in doneTasks">
|
||||
<div class="task-panel mt10">
|
||||
<div class="task-panel-header">{{ task.displayName }}</div>
|
||||
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
|
||||
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
|
||||
<el-descriptions-item label="办理用户">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})</el-descriptions-item>
|
||||
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理意见" v-if="!task.ext.isFirstTaskNode">{{ task.taskFormData.opinion }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
<!-- </el-form>-->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#suggestion-box-view",
|
||||
store,
|
||||
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
businessId: GetQueryString("businessId"),
|
||||
instanceId: GetQueryString("instanceId"),
|
||||
viewData: {},
|
||||
doneTasks: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
info() {
|
||||
this.$axios.post("/platform/suggestionBox/view/info", { id: this.businessId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.viewData = res.data
|
||||
}
|
||||
})
|
||||
},
|
||||
getDoneTasks() {
|
||||
this.$axios.post("/flow/common/doneTasks", { instanceId: this.instanceId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.doneTasks = res.data
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.info()
|
||||
this.getDoneTasks()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.task-panel {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-panel-header {
|
||||
background: rgb(250, 250, 250);
|
||||
border: 1px solid rgb(228, 231, 237);
|
||||
border-bottom: none;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
<div id="suggestion-box-apply-form" v-cloak>
|
||||
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="0" label-suffix=":" class="flow-task-form">
|
||||
<el-form-item label="审批意见" prop="tf_opinion" :rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<snaker-flow-task-form-action @task-action="handleTaskAction" @cancel="handleCancel"></snaker-flow-task-form-action>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#suggestion-box-apply-form",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
businessId: GetQueryString("businessId"),
|
||||
formData: {},
|
||||
formRules: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTaskAction(val) {
|
||||
console.log(val)
|
||||
|
||||
this.$axios
|
||||
.post("/flow/common/executeTask", {
|
||||
data: JSON.stringify({
|
||||
...val,
|
||||
...this.formData
|
||||
})
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("操作成功")
|
||||
// 发送完成消息
|
||||
window.GlobalBroadcastChannel.postMessage({
|
||||
type: "task-complete",
|
||||
payload: val
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
handleCancel(val) {
|
||||
console.log(val)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,81 +0,0 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<guava ref="guava">
|
||||
<div id="app">
|
||||
<el-card shadow="never">
|
||||
<search @search="doSearch">
|
||||
<search-item label="年度">
|
||||
<el-date-picker v-model="pageForm.year" type="year" value-format="yyyy" placeholder="年度" style="width: 100%"></el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="标题">
|
||||
<el-input v-model="pageForm.title" placeholder="标题" clearable></el-input>
|
||||
</search-item>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<table-tool>
|
||||
<el-radio-group v-model="pageForm.approval" size="small" @change="doSearch">
|
||||
<el-radio-button :label="true">已审核</el-radio-button>
|
||||
<el-radio-button :label="false">未审核</el-radio-button>
|
||||
</el-radio-group>
|
||||
</table-tool>
|
||||
<el-table :data="tableData" @sort-change="pageOrder" style="width: 100%">
|
||||
<el-table-column type="index" width="50px" label="序号" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="title" label="投稿标题"></el-table-column>
|
||||
<el-table-column prop="loginName" label="投稿人工号"></el-table-column>
|
||||
<el-table-column prop="userName" label="投稿人姓名"></el-table-column>
|
||||
<el-table-column prop="unitName" label="投稿人单位"></el-table-column>
|
||||
<el-table-column prop="unionName" label="投稿人工会"></el-table-column>
|
||||
<el-table-column prop="submitTime" label="投稿时间"></el-table-column>
|
||||
<el-table-column prop="curTaskName" label="当前节点"></el-table-column>
|
||||
<el-table-column prop="instanceState" label="流程状态">
|
||||
<template slot-scope="{row}">
|
||||
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="200px">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
|
||||
<el-button v-if="row.canRevoke" @click="onRevoke(row)" size="mini" type="danger">撤回</el-button>
|
||||
<!-- <el-button v-if="row.taskState === 10" @click="openView(row)" size="mini" type="primary">审核</el-button>-->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</div>
|
||||
</guava>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
pageDataUrl: "/platform/suggestionBox2/xgh/pageData",
|
||||
pageForm: {
|
||||
approval: false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openView(row) {
|
||||
window.open("/flow/common/approval/form?taskId=" + row.taskId + "&instanceId=" + row.instanceId + "&businessId=" + row.businessNo)
|
||||
},
|
||||
onRevoke(row) {
|
||||
console.log(row)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,271 @@
|
||||
<!--#
|
||||
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>
|
||||
|
||||
<div v-if="loading" class="loading-container">
|
||||
<van-loading type="spinner" size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<div v-else-if="course">
|
||||
<!-- 课程封面 -->
|
||||
<div class="course-cover">
|
||||
<van-image v-if="course.cover" :src="course.cover" fit="cover" :alt="course.title">
|
||||
<template v-slot:error>
|
||||
<van-icon name="photo-fail" size="48" color="#ddd"></van-icon>
|
||||
</template>
|
||||
</van-image>
|
||||
<van-icon v-else name="graduation" size="48" color="#ddd" ></van-icon>
|
||||
</div>
|
||||
|
||||
<!-- 课程信息 -->
|
||||
<van-cell-group class="course-info">
|
||||
<div class="course-title">{{ course.title || '未命名课程' }}</div>
|
||||
<div class="course-desc">{{ course.description || '暂无描述' }}</div>
|
||||
<div class="course-meta">
|
||||
<van-tag type="primary" size="mini">{{ course.category || '未分类' }}</van-tag>
|
||||
<span class="course-date">{{ formatDate(course.createdAt) }}</span>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 学习进度 -->
|
||||
<van-cell-group title="学习进度" class="progress-section">
|
||||
<van-cell>
|
||||
<template #default>
|
||||
<div class="progress-container">
|
||||
<van-progress :percentage="progressPercentage" stroke-width="6" color="#1989fa" />
|
||||
<span class="progress-text">{{ progressPercentage }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 课程章节 -->
|
||||
<van-cell-group title="课程章节" class="chapters-section">
|
||||
<van-collapse v-model="activeChapters">
|
||||
<van-collapse-item
|
||||
v-for="(chapter, index) in course.chapters"
|
||||
:key="chapter.id"
|
||||
:name="index"
|
||||
:title="chapter.title || `第${index + 1}章`"
|
||||
>
|
||||
<van-cell
|
||||
v-for="video in chapter.videos"
|
||||
:key="video.id"
|
||||
:title="video.title || '未命名视频'"
|
||||
:label="formatDuration(video.duration)"
|
||||
is-link
|
||||
@click="playVideo(video.id, video.title)"
|
||||
>
|
||||
<template #icon>
|
||||
<van-icon
|
||||
:name="isVideoCompleted(video.id) ? 'success' : 'play-circle-o'"
|
||||
:color="isVideoCompleted(video.id) ? '#07c160' : '#1989fa'"
|
||||
/>
|
||||
</template>
|
||||
<template #right-icon v-if="isVideoCompleted(video.id)">
|
||||
<van-tag type="success" size="mini">已完成</van-tag>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-empty v-if="!chapter.videos || !chapter.videos.length" description="暂无视频" />
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
<van-empty v-if="!course.chapters || !course.chapters.length" description="暂无章节内容" />
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<van-empty v-else description="课程不存在" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: linear-gradient(45deg, #f0f2f5, #e9ecef);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.course-cover .van-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.course-info .van-cell-group {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.course-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.course-date {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-container .van-progress {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.chapters-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
course: null,
|
||||
loading: true,
|
||||
courseId: null,
|
||||
activeChapters: [],
|
||||
progressData: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
progressPercentage() {
|
||||
if (!this.progressData || !this.progressData.totalVideos) return 0;
|
||||
return Math.round(this.progressData.completedVideos / this.progressData.totalVideos * 100);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
|
||||
// 加载课程详情
|
||||
async loadCourseDetail() {
|
||||
if (!this.courseId) {
|
||||
this.$toast.fail('缺少课程ID参数');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { code, data, msg } = await $.get('/platform/h5/edu/course/detail/data', {
|
||||
courseId: this.courseId
|
||||
});
|
||||
|
||||
if (code === 0 && data) {
|
||||
this.course = data;
|
||||
this.loadCourseProgress();
|
||||
} else {
|
||||
this.$toast.fail(msg || '加载课程详情失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast.fail('网络错误');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
// 加载课程进度
|
||||
async loadCourseProgress() {
|
||||
try {
|
||||
const { code, data } = await $.get('/platform/h5/edu/course/progress', {
|
||||
courseId: this.courseId
|
||||
});
|
||||
|
||||
if (code === 0 && data) {
|
||||
this.progressData = data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载进度失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 检查视频是否已完成
|
||||
isVideoCompleted(videoId) {
|
||||
return this.progressData &&
|
||||
this.progressData.completedVideoIds &&
|
||||
this.progressData.completedVideoIds.includes(videoId);
|
||||
},
|
||||
|
||||
// 播放视频
|
||||
playVideo(videoId, videoTitle) {
|
||||
pjaxReplace(`/platform/h5/edu/video/play?videoId=${videoId}&courseId=${this.courseId}&title=${encodeURIComponent(videoTitle || '')}`);
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
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');
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 获取URL参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.courseId = urlParams.get('courseId');
|
||||
|
||||
this.loadCourseDetail();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,234 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.course-item {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 6px;
|
||||
margin: 12px 16px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.course-item:active {
|
||||
transform: translateY(2px) scale(0.98);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
margin: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.course-cover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.course-cover .van-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 14px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.course-cover .van-icon {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
.course-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 16px 16px 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.course-desc {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.course-date {
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" title="教育培训课程" placeholder fixed>
|
||||
<template #right>
|
||||
<van-icon name="clock-o" @click="goToHistory"></van-icon>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
<van-sticky offset-top="46px">
|
||||
<van-tabs v-model="pageForm.category" @change="doSearch">
|
||||
<van-tab name="" title="全部"></van-tab>
|
||||
<van-tab name="理论学习" title="理论学习"></van-tab>
|
||||
<van-tab name="技能培训" title="技能培训"></van-tab>
|
||||
<van-tab name="安全教育" title="安全教育"></van-tab>
|
||||
<van-tab name="职业发展" title="职业发展"></van-tab>
|
||||
</van-tabs>
|
||||
</van-sticky>
|
||||
|
||||
<van-pull-refresh v-model="tableRefreshing" @refresh="onRefresh">
|
||||
<van-list v-model="tableLoading" :finished="tableFinished" finished-text="没有更多了" @load="onLoad">
|
||||
<div class="course-item" v-for="(course, index) in tableData" :key="course.id"
|
||||
@click="viewCourse(course.id)">
|
||||
<div class="course-cover">
|
||||
<van-image v-if="course.cover" :src="course.cover" fit="cover" :alt="course.title">
|
||||
<template v-slot:error>
|
||||
<van-icon name="photo-fail" size="32" color="#ddd"></van-icon>
|
||||
</template>
|
||||
</van-image>
|
||||
<van-icon v-else name="play-circle-o" size="48" color="#ddd"></van-icon>
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<div class="course-title">{{ course.title || '未命名课程' }}</div>
|
||||
<div class="course-desc">{{ course.description || '暂无描述' }}</div>
|
||||
<div class="course-meta">
|
||||
<van-tag type="primary" size="mini">{{ course.category || '未分类' }}</van-tag>
|
||||
<span class="course-date">{{ formatDate(course.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<van-empty v-if="!tableLoading && !tableData.length" description="暂无课程"></van-empty>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
dicts:['TRAIN_EDU_COURSE_TYPE'],
|
||||
data() {
|
||||
return {
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
category: ""
|
||||
},
|
||||
tableData: [],
|
||||
tableLoading: false,
|
||||
tableFinished: false,
|
||||
tableRefreshing: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
|
||||
// 刷新
|
||||
onRefresh() {
|
||||
this.tableFinished = false;
|
||||
this.tableLoading = true
|
||||
this.pageForm.pageNumber = 1;
|
||||
this.onLoad();
|
||||
},
|
||||
|
||||
// 加载更多
|
||||
async onLoad() {
|
||||
if(this.tableRefreshing){
|
||||
this.tableData = []
|
||||
this.tableRefreshing = false
|
||||
}
|
||||
|
||||
this.$axios.post('/platform/h5/edu/courses/list', this.pageForm).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.tableData = this.tableData.concat(res.data.list)
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
if (this.tableData.length >= this.pageForm.totalCount) {
|
||||
this.tableFinished = true
|
||||
}
|
||||
this.pageForm.pageNumber++;
|
||||
}
|
||||
}).finally(() => {
|
||||
this.tableLoading = false
|
||||
this.tableRefreshing = false
|
||||
})
|
||||
},
|
||||
|
||||
// 搜索
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1;
|
||||
this.tableData = [];
|
||||
this.tableFinished = false;
|
||||
this.tableLoading = true;
|
||||
this.onLoad();
|
||||
},
|
||||
|
||||
// 查看课程详情
|
||||
viewCourse(courseId) {
|
||||
pjaxReplace(`/platform/h5/edu/course/detail?courseId=` + courseId);
|
||||
},
|
||||
|
||||
// 前往学习历史
|
||||
goToHistory() {
|
||||
pjaxReplace('/platform/h5/edu/history');
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
return date.getFullYear() + '-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getDate()).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,385 @@
|
||||
<%
|
||||
layout('/platform/zhghh5/layout/layout.html',{
|
||||
title: '学习历史',
|
||||
keywords: '学习历史,视频学习,在线教育',
|
||||
description: '查看学习历史记录和进度统计'
|
||||
}){
|
||||
%>
|
||||
|
||||
<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);
|
||||
}
|
||||
.stats-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
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);
|
||||
}
|
||||
.history-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
display: flex;
|
||||
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;
|
||||
padding: 2px 6px;
|
||||
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;
|
||||
background: #e9ecef;
|
||||
border-radius: 2px;
|
||||
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;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
cursor: pointer;
|
||||
}
|
||||
.video-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.video-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, #1989fa, #1976d2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
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;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="history-container">
|
||||
<van-nav-bar
|
||||
title="学习历史"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
|
||||
<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">
|
||||
<van-icon name="chart-trending-o" style="margin-right: 8px; color: #1989fa;"/>
|
||||
学习统计
|
||||
</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>
|
||||
|
||||
<!-- 学习历史列表 -->
|
||||
<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)">
|
||||
<div class="course-info">
|
||||
<div class="course-title">{{ course.courseTitle || '未命名课程' }}</div>
|
||||
<div class="course-meta">
|
||||
<span class="course-category">{{ course.courseCategory || '未分类' }}</span>
|
||||
<span>最后学习:{{ formatDate(course.lastWatchTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-info">
|
||||
<div class="progress-text">{{ getProgressPercent(course) }}%</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: getProgressPercent(course) + '%' }"></div>
|
||||
</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 class="video-icon" :class="{ completed: video.isCompleted }">
|
||||
<van-icon :name="video.isCompleted ? 'success' : 'play-circle-o'" />
|
||||
</div>
|
||||
<div class="video-info">
|
||||
<div class="video-title">{{ video.videoTitle || '未命名视频' }}</div>
|
||||
<div class="video-meta">
|
||||
<span class="video-duration">{{ formatDuration(video.duration) }}</span>
|
||||
<span class="video-progress">
|
||||
{{ getVideoProgress(video) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="watch-time">
|
||||
{{ formatDate(video.lastWatchTime) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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';
|
||||
},
|
||||
|
||||
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.$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,262 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<van-nav-bar @click-left="historyBack" left-arrow left-text="返回" :title="videoTitle" placeholder fixed />
|
||||
|
||||
<div class="video-container">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<van-loading type="spinner" size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<div v-else-if="videoInfo" class="video-player">
|
||||
<video
|
||||
ref="videoElement"
|
||||
class="video-element"
|
||||
:src="videoInfo.url"
|
||||
controls
|
||||
preload="metadata"
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@play="onVideoPlay"
|
||||
@pause="onVideoPause"
|
||||
@timeupdate="onTimeUpdate"
|
||||
@ended="onVideoEnded"
|
||||
@error="onVideoError"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<van-empty v-else description="视频加载失败" />
|
||||
</div>
|
||||
|
||||
<!-- 进度保存提示 -->
|
||||
<van-toast v-model="showProgressToast" message="进度已保存" :duration="1000" />
|
||||
|
||||
<!-- 完成学习弹窗 -->
|
||||
<van-dialog
|
||||
v-model="showCompletionDialog"
|
||||
title="恭喜完成学习!"
|
||||
message="您已完成本视频的学习,学习记录已保存。"
|
||||
show-cancel-button
|
||||
cancel-button-text="继续观看"
|
||||
confirm-button-text="返回课程"
|
||||
@confirm="backToCourse"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.video-container {
|
||||
background: #000;
|
||||
min-height: calc(100vh - 46px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 300px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: calc(100vh - 46px);
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
videoInfo: null,
|
||||
loading: true,
|
||||
videoId: null,
|
||||
courseId: null,
|
||||
videoTitle: '视频播放',
|
||||
isPlaying: false,
|
||||
progressSaveTimer: null,
|
||||
showProgressToast: false,
|
||||
showCompletionDialog: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
|
||||
// 加载视频信息
|
||||
async loadVideoInfo() {
|
||||
if (!this.videoId) {
|
||||
this.$toast.fail('缺少视频ID参数');
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { code, data, msg } = await $.get('${base}/platform/h5/edu/video/detail', {
|
||||
videoId: this.videoId
|
||||
});
|
||||
|
||||
if (code === 0 && data) {
|
||||
this.videoInfo = data;
|
||||
this.$nextTick(() => {
|
||||
this.loadWatchProgress();
|
||||
});
|
||||
} else {
|
||||
this.$toast.fail(msg || '加载视频信息失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast.fail('网络错误');
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
// 视频加载完成
|
||||
onVideoLoaded() {
|
||||
console.log('视频加载完成');
|
||||
},
|
||||
|
||||
// 视频开始播放
|
||||
onVideoPlay() {
|
||||
this.isPlaying = true;
|
||||
this.startProgressSave();
|
||||
},
|
||||
|
||||
// 视频暂停
|
||||
onVideoPause() {
|
||||
this.isPlaying = false;
|
||||
this.saveProgress();
|
||||
},
|
||||
|
||||
// 时间更新
|
||||
onTimeUpdate() {
|
||||
// 可以在这里添加进度更新逻辑
|
||||
},
|
||||
|
||||
// 视频播放结束
|
||||
onVideoEnded() {
|
||||
this.isPlaying = false;
|
||||
this.markVideoCompleted();
|
||||
},
|
||||
|
||||
// 视频加载错误
|
||||
onVideoError() {
|
||||
this.$toast.fail('视频加载失败');
|
||||
},
|
||||
|
||||
// 加载观看进度
|
||||
async loadWatchProgress() {
|
||||
if (!this.videoId || !this.courseId) return;
|
||||
|
||||
try {
|
||||
const { code, data } = await $.get('${base}/platform/h5/edu/study/record', {
|
||||
videoId: this.videoId
|
||||
});
|
||||
|
||||
if (code === 0 && data && data.watchDuration > 0) {
|
||||
const video = this.$refs.videoElement;
|
||||
if (video) {
|
||||
video.currentTime = data.watchDuration;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载观看进度失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 保存观看进度
|
||||
async saveProgress() {
|
||||
const video = this.$refs.videoElement;
|
||||
if (!video || !this.videoId || !this.courseId) return;
|
||||
|
||||
try {
|
||||
await $.post('${base}/platform/h5/edu/study/progress/save', {
|
||||
videoId: this.videoId,
|
||||
courseId: this.courseId,
|
||||
watchDuration: Math.floor(video.currentTime)
|
||||
});
|
||||
|
||||
this.showProgressToast = true;
|
||||
} catch (error) {
|
||||
console.error('保存进度失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 开始定时保存进度
|
||||
startProgressSave() {
|
||||
this.clearProgressSave();
|
||||
this.progressSaveTimer = setInterval(() => {
|
||||
this.saveProgress();
|
||||
}, 10000); // 每10秒保存一次
|
||||
},
|
||||
|
||||
// 清除定时保存
|
||||
clearProgressSave() {
|
||||
if (this.progressSaveTimer) {
|
||||
clearInterval(this.progressSaveTimer);
|
||||
this.progressSaveTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 标记视频完成
|
||||
async markVideoCompleted() {
|
||||
if (!this.videoId || !this.courseId) return;
|
||||
|
||||
try {
|
||||
const { code } = await $.post('${base}/platform/h5/edu/study/complete', {
|
||||
videoId: this.videoId,
|
||||
courseId: this.courseId
|
||||
});
|
||||
|
||||
if (code === 0) {
|
||||
this.showCompletionDialog = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('标记完成失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 返回课程
|
||||
backToCourse() {
|
||||
if (this.courseId) {
|
||||
this.$router.push(`/course/detail?courseId=${this.courseId}`);
|
||||
} else {
|
||||
this.historyBack();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 获取URL参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.videoId = urlParams.get('videoId');
|
||||
this.courseId = urlParams.get('courseId');
|
||||
this.videoTitle = decodeURIComponent(urlParams.get('title') || '视频播放');
|
||||
|
||||
this.loadVideoInfo();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// 页面销毁前保存进度
|
||||
this.saveProgress();
|
||||
this.clearProgressSave();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user