新增一键催办功能、驳回显示时间戳

This commit is contained in:
2025-09-11 19:32:18 +08:00
parent 583691266f
commit 518d4d3a20
13 changed files with 1417 additions and 482 deletions

1
.vercel/project.json Normal file
View File

@ -0,0 +1 @@
{"projectName":"trae_ppms-uni-vue3_8m55"}

3
components.d.ts vendored
View File

@ -23,7 +23,9 @@ declare module 'vue' {
WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.vue')['default'] WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.vue')['default']
WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default'] WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default']
WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default'] WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default']
WdLoading: typeof import('wot-design-uni/components/wd-loading/wd-loading.vue')['default']
WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default'] WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default']
WdNoticeBar: typeof import('wot-design-uni/components/wd-notice-bar/wd-notice-bar.vue')['default']
WdPicker: typeof import('wot-design-uni/components/wd-picker/wd-picker.vue')['default'] WdPicker: typeof import('wot-design-uni/components/wd-picker/wd-picker.vue')['default']
WdProgress: typeof import('wot-design-uni/components/wd-progress/wd-progress.vue')['default'] WdProgress: typeof import('wot-design-uni/components/wd-progress/wd-progress.vue')['default']
WdRadio: typeof import('wot-design-uni/components/wd-radio/wd-radio.vue')['default'] WdRadio: typeof import('wot-design-uni/components/wd-radio/wd-radio.vue')['default']
@ -32,6 +34,7 @@ declare module 'vue' {
WdStatusTip: typeof import('wot-design-uni/components/wd-status-tip/wd-status-tip.vue')['default'] WdStatusTip: typeof import('wot-design-uni/components/wd-status-tip/wd-status-tip.vue')['default']
WdStep: typeof import('wot-design-uni/components/wd-step/wd-step.vue')['default'] WdStep: typeof import('wot-design-uni/components/wd-step/wd-step.vue')['default']
WdSteps: typeof import('wot-design-uni/components/wd-steps/wd-steps.vue')['default'] WdSteps: typeof import('wot-design-uni/components/wd-steps/wd-steps.vue')['default']
WdSwipeAction: typeof import('wot-design-uni/components/wd-swipe-action/wd-swipe-action.vue')['default']
WdTabbar: typeof import('wot-design-uni/components/wd-tabbar/wd-tabbar.vue')['default'] WdTabbar: typeof import('wot-design-uni/components/wd-tabbar/wd-tabbar.vue')['default']
WdTabbarItem: typeof import('wot-design-uni/components/wd-tabbar-item/wd-tabbar-item.vue')['default'] WdTabbarItem: typeof import('wot-design-uni/components/wd-tabbar-item/wd-tabbar-item.vue')['default']
WdTextarea: typeof import('wot-design-uni/components/wd-textarea/wd-textarea.vue')['default'] WdTextarea: typeof import('wot-design-uni/components/wd-textarea/wd-textarea.vue')['default']

View File

@ -60,9 +60,9 @@
"@dcloudio/uni-quickapp-webview": "3.0.0-4020420240722002", "@dcloudio/uni-quickapp-webview": "3.0.0-4020420240722002",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"vue": "^3.4.21", "vue": "^3.5.18",
"vue-i18n": "^9.1.9", "vue-i18n": "^9.1.9",
"wot-design-uni": "^1.5.1" "wot-design-uni": "^1.11.0"
}, },
"devDependencies": { "devDependencies": {
"@dcloudio/types": "^3.4.8", "@dcloudio/types": "^3.4.8",

646
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -360,4 +360,44 @@ export default class BussApi {
}) })
.then((res) => res.data); .then((res) => res.data);
} }
/**
* 检查当前用户是否需要任务提醒
* @returns Promise<boolean> 返回是否需要提醒
* - 教师:可以催办管理员(状态 1、3或收到管理员消息状态 0、2、4 且有 msg时返回 true
* - 管理员:可以催办教师(状态 0、2、4或有消息需要处理时返回 true
*/
static checkNeedReminder(): Promise<boolean> {
const user = useUser();
return http
.server()
.get("lesson-tasks/need-reminder", {
headers: {
Authorization: `Bearer ${user.token}`,
},
})
.then((res) => res.data.data);
}
/**
* 一键催办 - 发送催办提醒
* @param id - 课程任务ID
* @returns Promise<LessonTask> 返回更新后的课程任务
*/
static sendReminder(id: number): Promise<LessonTask> {
return this.updateLessonTask(id, {
expediteStatus: 1
});
}
/**
* 清除催办提醒
* @param id - 课程任务ID
* @returns Promise<LessonTask> 返回更新后的课程任务
*/
static clearReminder(id: number): Promise<LessonTask> {
return this.updateLessonTask(id, {
expediteStatus: 0
});
}
} }

View File

@ -64,6 +64,130 @@ const expandedCourse = ref(['lesson'])
const groupedLessons = ref<{ [key: string]: LessonTask[] }>({}) const groupedLessons = ref<{ [key: string]: LessonTask[] }>({})
const refreshKey = ref(0) const refreshKey = ref(0)
// 催办消息展示相关
const reminderMessages = ref<LessonTask[]>([])
/**
* 判断当前用户是否负责当前进度
*/
const isUserResponsibleForCurrentProgress = (lesson: LessonTask): boolean => {
if (!lesson || !user.userinfo) return false
const progressStatus = lesson.progressStatus
const userRoles = user.userinfo.roles
const userJobs = user.userinfo.jobs
// 判断进度负责人
switch (progressStatus) {
case 0: // 脚本制作 - 教师负责
case 1: // 脚本提交 - 教师负责
return userRoles === 1 && userJobs === 1 // 校方教师 + 课程制作教师
case 2: // 脚本审核 - 审核人员负责
return userJobs === 2 // 课程审核人员
case 3: // 脚本确认 - 教师负责
return userRoles === 1 && userJobs === 1 // 校方教师 + 课程制作教师
case 4: // 视频制作审核 - 审核人员负责
return userJobs === 2 // 课程审核人员
case 5: // 已完成
return false
default:
return false
}
}
/**
* 判断是否应该显示催办消息
*/
const shouldShowExpediteNotice = (lesson: LessonTask): boolean => {
if (!lesson || !user.userinfo) return false
// 条件1: 当前用户所负责的课程通过userId判断
const isUserCourse = lesson.userId === user.userinfo.id
// 条件2: 课程当前所在的进度是当前用户所负责的进度
const isResponsibleForProgress = isUserResponsibleForCurrentProgress(lesson)
// 条件3: 催办信息为1
const isExpedited = lesson.expediteStatus === 1
return isUserCourse && isResponsibleForProgress && isExpedited
}
/**
* 调试函数:打印当前账户所负责的课程状态
*/
const debugUserCourses = () => {
console.log('🔍 [首页] 执行debugUserCourses函数...')
if (!user.userinfo) {
console.log('❌ 用户未登录')
return
}
console.log('👤 当前用户ID:', user.userinfo.id)
const allLessons = Object.values(groupedLessons.value).flat()
console.log('📚 所有课程数量:', allLessons.length)
// 过滤出当前用户负责的课程
const userCourses = allLessons.filter(lesson => lesson.userId === user.userinfo!.id)
console.log('🎯 当前用户负责的课程数量:', userCourses.length)
// 生成简洁的ID和状态对应关系
const courseStatusMap = userCourses.reduce((acc, lesson) => {
acc[`ID:${lesson.id}`] = `状态:${lesson.progressStatus},催办:${lesson.expediteStatus}`
return acc
}, {} as Record<string, string>)
console.log('📋 当前用户负责的课程状态:', courseStatusMap)
// 需要处理的催办课程(满足三个条件)
const needAttentionMap = userCourses
.filter(lesson => shouldShowExpediteNotice(lesson))
.reduce((acc, lesson) => {
acc[`ID:${lesson.id}`] = `需处理催办`
return acc
}, {} as Record<string, string>)
if (Object.keys(needAttentionMap).length > 0) {
console.log('⚠️ 需要处理的催办课程:', needAttentionMap)
} else {
console.log('✅ 当前没有需要处理的催办课程')
}
}
// 检查催办消息
const checkReminderMessages = () => {
const allLessons = Object.values(groupedLessons.value).flat()
const filteredLessons = allLessons.filter(lesson => {
// 检查是否有催办状态
if (lesson.expediteStatus !== 1) {
return false
}
// 根据用户角色和任务状态判断是否显示催办信息
const progressStatus = lesson.progressStatus
if (user.hasRole('teacher')) {
// 教师:任务状态是 0、2、4 时显示催办信息
return [0, 2, 4].includes(progressStatus)
} else if (user.hasRole('admin')) {
// 公司课程顾问(课程审核人员):任务状态是 1、3 时显示催办信息
return [1, 3].includes(progressStatus)
} else {
return false
}
})
reminderMessages.value = filteredLessons
}
const openLessonDetail = (courseId: number) => { const openLessonDetail = (courseId: number) => {
router.push({ router.push({
name: 'lesson', name: 'lesson',
@ -75,11 +199,22 @@ const openLessonDetail = (courseId: number) => {
const loadLessons = async () => { const loadLessons = async () => {
console.log('🔄 [首页] 开始加载课程列表...')
if (!user.hasValidToken() || !user.userinfo) { if (!user.hasValidToken() || !user.userinfo) {
console.log('❌ [首页] 用户未登录,跳转到登录页')
toast.error({ msg: '请先登录' }) toast.error({ msg: '请先登录' })
router.replace('/pages/login/index') router.replace('/pages/login/index')
return return
} }
console.log('✅ [首页] 用户已登录,用户信息:', {
id: user.userinfo.id,
username: user.userinfo.username,
roles: user.userinfo.roles,
jobs: user.userinfo.jobs
})
toast.loading({ msg: '加载中...' }) toast.loading({ msg: '加载中...' })
try { try {
let res; let res;
@ -118,6 +253,28 @@ const loadLessons = async () => {
return acc return acc
}, {}) }, {})
// 在每个章节内部按徽标状态排序
Object.keys(groupData).forEach(courseName => {
// 使用冒泡排序,将有徽标的课程排在前面
const lessons = groupData[courseName]
for (let i = 0; i < lessons.length; i++) {
for (let j = 0; j < lessons.length - i - 1; j++) {
// 检查徽标是否显示:根据用户角色和课程角色判断
const currentHasBadge = (getLessonRole(lessons[j]) === 'teacher' && user.hasRole('teacher')) ||
(getLessonRole(lessons[j]) === 'admin' && user.hasRole('admin'))
const nextHasBadge = (getLessonRole(lessons[j + 1]) === 'teacher' && user.hasRole('teacher')) ||
(getLessonRole(lessons[j + 1]) === 'admin' && user.hasRole('admin'))
// 如果当前元素没有徽标但下一个元素有徽标,则交换位置
if (!currentHasBadge && nextHasBadge) {
const temp = lessons[j]
lessons[j] = lessons[j + 1]
lessons[j + 1] = temp
}
}
}
})
if (value.value !== 0) { if (value.value !== 0) {
Object.keys(groupData).forEach(courseName => { Object.keys(groupData).forEach(courseName => {
groupData[courseName].sort((a: LessonTask, b: LessonTask) => { groupData[courseName].sort((a: LessonTask, b: LessonTask) => {
@ -132,8 +289,26 @@ const loadLessons = async () => {
groupedLessons.value = groupData groupedLessons.value = groupData
// 调试:打印当前账户所负责的课程状态
console.log('🚀 [首页] 开始调试用户课程状态...')
debugUserCourses()
console.log('✅ [首页] 调试完成')
const expandedGroups = Object.keys(groupData).filter(courseName => { const expandedGroups = Object.keys(groupData).filter(courseName => {
const courses = groupData[courseName] const courses = groupData[courseName]
// 检查是否有徽标需要显示 - 新增逻辑
const hasBadge = courses.some(lesson =>
(getLessonRole(lesson) === 'teacher' && user.hasRole('teacher')) ||
(getLessonRole(lesson) === 'admin' && user.hasRole('admin'))
)
// 如果没有徽标需要显示,则不展开 - 新增逻辑
if (!hasBadge) {
return false
}
// 原有的进度逻辑
const hasCompleted = courses.some(lesson => calcLessonProgress(lesson) === 100) const hasCompleted = courses.some(lesson => calcLessonProgress(lesson) === 100)
const hasNotStarted = courses.some(lesson => calcLessonProgress(lesson) === 0) const hasNotStarted = courses.some(lesson => calcLessonProgress(lesson) === 0)
const hasInProgress = courses.some(lesson => { const hasInProgress = courses.some(lesson => {
@ -144,6 +319,9 @@ const loadLessons = async () => {
}) })
expandedCourse.value = expandedGroups expandedCourse.value = expandedGroups
// 检查催办消息
checkReminderMessages()
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof Error) { if (err instanceof Error) {
toast.error({ msg: err.message }) toast.error({ msg: err.message })
@ -195,11 +373,31 @@ onPullDownRefresh(() => {
onLoad(() => { onLoad(() => {
loadLessons() loadLessons()
}) })
const handleSwipeAction = (action: string, lesson: LessonTask) => {
switch (action) {
case 'edit':
toast.show(`编辑微课: ${lesson.microLessonName}`)
// 这里可以添加编辑逻辑
break
case 'delete':
toast.show(`删除微课: ${lesson.microLessonName}`)
// 这里可以添加删除逻辑
break
default:
break
}
}
</script> </script>
<template> <template>
<page-wrapper> <page-wrapper>
<div> <div>
<!-- 催办消息展示 -->
<div v-if="reminderMessages.length > 0" class="mb-2">
<wd-notice-bar text="您有课程需要完成,请及时处理" type="warning" />
</div>
<div v-if="!user.hasRole('teacher')" class="flex items-center justify-around gap-4 px-4"> <div v-if="!user.hasRole('teacher')" class="flex items-center justify-around gap-4 px-4">
<wd-drop-menu> <wd-drop-menu>
<wd-drop-menu-item v-model="teacherFilterValue" :options="teacherFilterOptions" <wd-drop-menu-item v-model="teacherFilterValue" :options="teacherFilterOptions"
@ -217,14 +415,9 @@ onLoad(() => {
<template #title="{ expanded, disabled, isFirst }"> <template #title="{ expanded, disabled, isFirst }">
<div class="w-full flex justify-between items-center"> <div class="w-full flex justify-between items-center">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<!-- <wd-badge is-dot :right="230" :hidden="!courses.some(lesson =>
(getLessonRole(lesson) === 'teacher' && user.hasRole('teacher')) ||
(getLessonRole(lesson) === 'admin' && user.hasRole('admin'))
)"> -->
<p class="pt-1"> <p class="pt-1">
{{ courseName || '无标题课程' }} {{ courseName || '无标题课程' }}
</p> </p>
<!-- </wd-badge> -->
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<wd-tag v-if="(() => { <wd-tag v-if="(() => {
const hasCompleted = courses.some(lesson => calcLessonProgress(lesson) === 100) const hasCompleted = courses.some(lesson => calcLessonProgress(lesson) === 100)
@ -254,10 +447,10 @@ onLoad(() => {
共{{ courses.length }}节微课 共{{ courses.length }}节微课
</wd-tag> </wd-tag>
<wd-tag custom-class="op-60" plain> <wd-tag custom-class="op-60" plain>
已完成{{ courses.filter(lesson => calcLessonProgress(lesson) === 100).length }}· 已完成{{courses.filter(lesson => calcLessonProgress(lesson) === 100).length}}·
进行中{{ courses.filter(lesson => calcLessonProgress(lesson) !== 0 && calcLessonProgress(lesson) 进行中{{courses.filter(lesson => calcLessonProgress(lesson) !== 0 && calcLessonProgress(lesson)
!== 100).length }}· !== 100).length}}·
未开始{{ courses.filter(lesson => calcLessonProgress(lesson) === 0).length }} 未开始{{courses.filter(lesson => calcLessonProgress(lesson) === 0).length}}
</wd-tag> </wd-tag>
</div> </div>
</div> </div>
@ -266,20 +459,20 @@ onLoad(() => {
<div class="flex items-center gap-0.5"> <div class="flex items-center gap-0.5">
<div class="w-1.5 aspect-square rounded-full bg-emerald"></div> <div class="w-1.5 aspect-square rounded-full bg-emerald"></div>
<span class="text-xs text-emerald font-bold font-mono"> <span class="text-xs text-emerald font-bold font-mono">
{{ courses.filter(lesson => calcLessonProgress(lesson) === 100).length }} {{courses.filter(lesson => calcLessonProgress(lesson) === 100).length}}
</span> </span>
</div> </div>
<div class="flex items-center gap-0.5"> <div class="flex items-center gap-0.5">
<div class="w-1.5 aspect-square rounded-full bg-blue"></div> <div class="w-1.5 aspect-square rounded-full bg-blue"></div>
<span class="text-xs text-blue font-bold font-mono"> <span class="text-xs text-blue font-bold font-mono">
{{ courses.filter(lesson => calcLessonProgress(lesson) !== 0 && calcLessonProgress(lesson) !== {{courses.filter(lesson => calcLessonProgress(lesson) !== 0 && calcLessonProgress(lesson) !==
100).length }} 100).length}}
</span> </span>
</div> </div>
<div class="flex items-center gap-0.5"> <div class="flex items-center gap-0.5">
<div class="w-1.5 aspect-square rounded-full bg-neutral"></div> <div class="w-1.5 aspect-square rounded-full bg-neutral"></div>
<span class="text-xs text-neutral font-bold font-mono"> <span class="text-xs text-neutral font-bold font-mono">
{{ courses.filter(lesson => calcLessonProgress(lesson) === 0).length }} {{courses.filter(lesson => calcLessonProgress(lesson) === 0).length}}
</span> </span>
</div> </div>
</div> </div>
@ -292,31 +485,45 @@ onLoad(() => {
<wd-status-tip v-if="courses.length === 0" <wd-status-tip v-if="courses.length === 0"
image="https://registry.npmmirror.com/wot-design-uni-assets/*/files/content.png" tip="没有课程小节" /> image="https://registry.npmmirror.com/wot-design-uni-assets/*/files/content.png" tip="没有课程小节" />
<div v-else v-for="(lesson, i) in courses" :key="`${lesson.id}-${refreshKey}`" <div v-else v-for="(lesson, i) in courses" :key="`${lesson.id}-${refreshKey}`"
@click="openLessonDetail(lesson.id)" class="w-full border-b border-b-solid border-neutral-100 last:border-b-0">
class="w-full py-2 gap-12 flex justify-between items-center border-b border-b-solid border-neutral-100 last:border-b-0 first:pt-0 last:pb-0"> <!-- 所有用户都使用 SwipeAction 组件 -->
<div class="flex items-center gap-1 self-center"> <wd-swipe-action :disabled="!(user.hasRole('admin') || user.hasRole('sysadmin'))">
<div> <div @click="openLessonDetail(lesson.id)"
<div v-if="calcLessonProgress(lesson) === 100" class="i-tabler-circle-check text-emerald"></div> class="w-full py-2 gap-12 flex justify-between items-center first:pt-0 last:pb-0">
<div v-else-if="calcLessonProgress(lesson) === 0" class="i-tabler-progress-x text-neutral"> <div class="flex items-center gap-1 self-center">
<div>
<div v-if="calcLessonProgress(lesson) === 100" class="i-tabler-circle-check text-emerald"></div>
<div v-else-if="calcLessonProgress(lesson) === 0" class="i-tabler-progress-x text-neutral">
</div>
<div v-else class="i-tabler-hourglass-empty text-blue"></div>
</div>
<wd-badge is-dot
:hidden="!(getLessonRole(lesson) === 'teacher' && user.hasRole('teacher') || getLessonRole(lesson) === 'admin' && user.hasRole('admin'))">
<span>{{ lesson.microLessonName || '无标题微课' }}</span>
</wd-badge>
</div>
<div class="flex items-center gap-3 shrink-0">
<span v-if="!user.hasRole('teacher') && getUsernameById(lesson.userId)"
class="text-xs text-gray-400 ml-2 whitespace-nowrap">
{{ getUsernameById(lesson.userId) }}
</span>
<div class="w-16">
<wd-progress :percentage="calcLessonProgress(lesson)"
:color="calcLessonProgress(lesson) === 100 ? '#34d399' : '#60a5fa'" hide-text />
</div>
<div class="i-tabler-dots text-neutral-400 text-xl"></div>
</div> </div>
<div v-else class="i-tabler-hourglass-empty text-blue"></div>
</div> </div>
<wd-badge is-dot <!-- 右滑操作按钮 - 仅管理员可见 -->
:hidden="!(getLessonRole(lesson) === 'teacher' && user.hasRole('teacher') || getLessonRole(lesson) === 'admin' && user.hasRole('admin'))"> <template #right>
<span>{{ lesson.microLessonName || '无标题微课' }}</span> <view class="action">
</wd-badge> <!-- <view class="button" style="background: #f59e0b;" @click.stop="handleSwipeAction('edit', lesson)">编辑
</div> </view> -->
<div class="flex items-center gap-3 shrink-0"> <view class="button" style="background: #ef4444;" @click.stop="handleSwipeAction('delete', lesson)">
<span v-if="!user.hasRole('teacher') && getUsernameById(lesson.userId)" 删除</view>
class="text-xs text-gray-400 ml-2 whitespace-nowrap"> </view>
{{ getUsernameById(lesson.userId) }} </template>
</span> </wd-swipe-action>
<div class="w-16">
<wd-progress :percentage="calcLessonProgress(lesson)"
:color="calcLessonProgress(lesson) === 100 ? '#34d399' : '#60a5fa'" hide-text />
</div>
<div class="i-tabler-dots text-neutral-400 text-xl"></div>
</div>
</div> </div>
</div> </div>
</wd-collapse-item> </wd-collapse-item>
@ -329,4 +536,21 @@ onLoad(() => {
.wd-collapse-item__header>view { .wd-collapse-item__header>view {
width: 100%; width: 100%;
} }
/* SwipeAction 样式 */
.action {
height: 100%;
display: flex;
}
.button {
display: flex;
align-items: center;
justify-content: center;
padding: 0 16px;
height: 100%;
color: white;
font-size: 14px;
min-width: 60px;
}
</style> </style>

View File

@ -20,6 +20,37 @@ const message = useMessage()
const lesson = ref<LessonTask | null>(null) const lesson = ref<LessonTask | null>(null)
// 判断当前用户是否应该处理该课程
const shouldCurrentUserHandle = () => {
if (!lesson.value) return false
const role = getLessonRole(lesson.value)
return (role === 'teacher' && user.hasRole('teacher')) ||
(role === 'admin' && user.hasRole('admin'))
}
// 发送催办提醒
const sendReminder = async () => {
if (!lesson.value) return
try {
toast.loading({ msg: '发送催办中...' })
await BussApi.sendReminder(lesson.value.id)
toast.success({ msg: '催办成功,已通知相关人员处理' })
// 重新加载课程详情
await loadLesson()
} catch (err: unknown) {
if (err instanceof Error) {
toast.error({ msg: err.message })
} else {
toast.error({ msg: '催办失败' })
}
} finally {
toast.close()
}
}
const lessonSteps = computed(() => lesson.value ? getLessonSteps(lesson.value) : []) const lessonSteps = computed(() => lesson.value ? getLessonSteps(lesson.value) : [])
const lessonStages = computed(() => lesson.value ? extractLessonStage(lesson.value) : null) const lessonStages = computed(() => lesson.value ? extractLessonStage(lesson.value) : null)
const lessonProgress = computed(() => lesson.value ? calcLessonProgress(lesson.value) : 0) const lessonProgress = computed(() => lesson.value ? calcLessonProgress(lesson.value) : 0)
@ -39,31 +70,61 @@ const goProgress = (lessonId: number) => {
return return
} }
router.replaceAll({ // 更新导航栏状态
name: 'progress', tabbar.activeTab = 'progress'
params: {
courseName: `${lesson.value.courseName}`, // 先清除之前的缓存参数,确保每次使用最新的参数
lessonId: `${lessonId}` uni.removeStorageSync('progress_params')
}
// 使用本地存储传递参数
const courseNameParam = lesson.value.courseName
const lessonIdParam = lessonId.toString()
// 将参数存储到本地缓存
uni.setStorageSync('progress_params', {
courseName: courseNameParam,
lessonId: lessonIdParam
})
// 不带参数跳转到tabBar页面
router.pushTab({
name: 'progress'
}) })
} }
const loadLesson = async () => { const loadLesson = async () => {
if (!route.params?.courseId) { if (!route.params?.courseId) {
toast.error({ toast.error({
msg: '参数错误' msg: '参数错误缺少课程ID'
}) })
console.error('Missing courseId in route params:', route.params)
return return
} }
console.log('Loading lesson with courseId:', route.params.courseId)
toast.loading({ toast.loading({
msg: '加载中...' msg: '加载中...'
}) })
BussApi.getLessonTask(route.params.courseId).then(courseData => {
toast.close() try {
// 确保courseId是数字类型
const courseId = parseInt(route.params.courseId as string)
if (isNaN(courseId)) {
throw new Error('课程ID格式错误')
}
const courseData = await BussApi.getLessonTask(courseId)
console.log('Lesson data loaded:', courseData)
lesson.value = courseData lesson.value = courseData
}).catch(err => { toast.close()
toast.error({ msg: err.message }) } catch (err: any) {
}) console.error('Failed to load lesson:', err)
toast.error({
msg: err?.message || '加载课程数据失败,请重试'
})
}
} }
onMounted(() => { onMounted(() => {
@ -92,6 +153,9 @@ onPullDownRefresh(() => {
// 页面加载时执行 // 页面加载时执行
onLoad(() => { onLoad(() => {
// 清除进度页面的缓存参数,避免缓存累积
uni.removeStorageSync('progress_params')
loadLesson() loadLesson()
}) })
@ -100,36 +164,60 @@ onLoad(() => {
<template> <template>
<div> <div>
<wd-message-box></wd-message-box> <wd-message-box></wd-message-box>
<div
:class="`pattern p-4 flex flex-col gap-6 relative ${lessonProgress === 100 ? 'bg-emerald' : (lessonProgress === 0 ? 'bg-neutral' : 'bg-blue')}`"> <!-- 加载状态 -->
<div class="flex flex-col gap-0"> <div v-if="!lesson" class="p-4 text-center">
<h2 class="text-sm text-white font-black op-50">{{ lesson?.courseName }}</h2> <wd-loading vertical>
<h1 class="text-lg text-white font-bold">{{ lesson?.microLessonName }}</h1> 加载课程信息中...
</wd-loading>
</div>
<!-- 课程信息 -->
<div v-else>
<div
:class="`pattern p-4 flex flex-col gap-6 relative ${lessonProgress === 100 ? 'bg-emerald' : (lessonProgress === 0 ? 'bg-neutral' : 'bg-blue')}`">
<div class="flex flex-col gap-0">
<h2 class="text-sm text-white font-black op-50">{{ lesson?.courseName || '无课程名称' }}</h2>
<h1 class="text-lg text-white font-bold">{{ lesson?.microLessonName || '无微课名称' }}</h1>
</div>
<p class="text-xs text-white font-bold op-50" style="line-height: 1;">
创建于 {{ lesson?.createdAt ? dayjs(lesson.createdAt * 1000).format('YYYY-MM-DD HH:mm:ss') : '未知时间' }}
</p>
<p v-if="lessonProgress === 100" class="text-xs text-white font-bold op-50" style="line-height: 1;">
完成于 {{ lesson?.finishTime ? dayjs(lesson.finishTime * 1000).format('YYYY-MM-DD HH:mm:ss') : '未知时间' }}
</p>
<div class="absolute text-white top-2 right-2 op-35 text-18">
<div v-if="lessonProgress === 100" class="i-tabler-circle-check"></div>
<div v-else-if="lessonProgress === 0" class="i-tabler-progress-x"></div>
<div v-else class="i-tabler-hourglass-empty"></div>
</div>
</div> </div>
<p class="text-xs text-white font-bold op-50" style="line-height: 1;">
创建于 {{ lesson?.createdAt ? dayjs(lesson.createdAt * 1000).format('YYYY-MM-DD HH:mm:ss') : '-' }} <div class="p-4">
</p> <wd-steps :active="lessonStages?.step || 0" vertical>
<p v-if="lessonProgress === 100" class="text-xs text-white font-bold op-50" style="line-height: 1;"> <wd-step v-for="(step, index) in lessonSteps" :key="index" :title="step.title"
完成于 {{ lesson?.finishTime ? dayjs(lesson.finishTime * 1000).format('YYYY-MM-DD HH:mm:ss') : '-' }} :description="step.description" />
</p> </wd-steps>
<div class="absolute text-white top-2 right-2 op-35 text-18"> </div>
<div v-if="lessonProgress === 100" class="i-tabler-circle-check"></div>
<div v-else-if="lessonProgress === 0" class="i-tabler-progress-x"></div> <!-- 进度处理按钮区域 -->
<div v-else class="i-tabler-hourglass-empty"></div> <div v-if="lessonProgress !== 100" class="px-4 pt-2 ">
<wd-button v-if="!user.hasRole('liaison')" type="primary" :round="false" plain block
@click="goProgress(lesson?.id!)">进度处理</wd-button>
</div>
<div v-else class="px-4 pt-2">
<wd-button v-if="!user.hasRole('liaison')" disabled type="success" :round="false" plain block>已完成</wd-button>
</div>
<!-- 催办按钮区域 -->
<div
v-if="lesson && !shouldCurrentUserHandle() && lessonProgress !== 100 && (user.hasRole('teacher') || user.hasRole('admin'))"
class="px-4 pt-2">
<wd-button type="warning" :round="false" plain block @click="sendReminder">
<wd-icon name="bell" class="mr-1"></wd-icon>
催办
</wd-button>
</div> </div>
</div>
<div class="p-4">
<wd-steps :active="lessonStages?.step || 0" vertical>
<wd-step v-for="(step, index) in lessonSteps" :key="index" :title="step.title"
:description="step.description" />
</wd-steps>
</div>
<div v-if="lessonProgress !== 100" class="px-4 pt-2 ">
<wd-button v-if="!user.hasRole('liaison')" type="primary" :round="false" plain block
@click="goProgress(lesson?.id!)">进度处理</wd-button>
</div>
<div v-else class="px-4 pt-2">
<wd-button v-if="!user.hasRole('liaison')" disabled type="success" :round="false" plain block>已完成</wd-button>
</div> </div>
</div> </div>
</template> </template>

View File

@ -37,6 +37,14 @@ const handleSubmit = () => {
remember: model.remember remember: model.remember
}).then(res => { }).then(res => {
user.setToken(res.token) user.setToken(res.token)
// 如果用户勾选了记住密码,保存登录凭证
if (model.remember) {
user.saveCredentials(model.email, model.password)
} else {
user.clearCredentials()
}
toast.loading({ toast.loading({
msg: '加载资料...' msg: '加载资料...'
}) })
@ -74,6 +82,15 @@ onPullDownRefresh(() => {
refresh() refresh()
}) })
// 初始化时加载登录凭证
onLoad(() => {
const credentials = user.getCredentials()
if (credentials) {
model.email = credentials.email
model.password = credentials.password
model.remember = true
}
})
</script> </script>
<template> <template>
@ -87,7 +104,7 @@ onPullDownRefresh(() => {
<wd-cell> <wd-cell>
<template #title> <template #title>
<div class="pl-[5px]"> <div class="pl-[5px]">
<wd-checkbox v-model="model.remember">记住登录</wd-checkbox> <wd-checkbox v-model="model.remember">记住密码</wd-checkbox>
</div> </div>
</template> </template>
</wd-cell> </wd-cell>

View File

@ -9,7 +9,7 @@ import StatusBlock from './StatusBlock.vue';
import { useRoute } from 'uni-mini-router'; import { useRoute } from 'uni-mini-router';
import { useUser } from '@/stores/useUser'; import { useUser } from '@/stores/useUser';
import { useTabbar } from '@/stores/useTabbar' import { useTabbar } from '@/stores/useTabbar'
import { onPullDownRefresh } from '@dcloudio/uni-app' import { onPullDownRefresh, onLoad, onShow } from '@dcloudio/uni-app'
import { useRouter } from 'uni-mini-router'; import { useRouter } from 'uni-mini-router';
const router = useRouter() const router = useRouter()
@ -29,8 +29,16 @@ const pickerCourseValue = ref()
const pickerLessonColumns = computed(() => { const pickerLessonColumns = computed(() => {
return pickerCourseValue.value ? groupedLessons.value[pickerCourseValue.value].map((lesson: LessonTask) => { return pickerCourseValue.value ? groupedLessons.value[pickerCourseValue.value].map((lesson: LessonTask) => {
const isCompleted = extractLessonStage(lesson).step === 5
const shouldShowExpedite = shouldShowExpediteNotice(lesson)
let prefix = ''
if (isCompleted) {
prefix = '✅ '
} else if (shouldShowExpedite) {
prefix = '🔔 '
}
return { return {
label: (extractLessonStage(lesson).step === 5 ? '✅ ' : '') + lesson.microLessonName, label: prefix + lesson.microLessonName,
value: lesson.id, value: lesson.id,
} }
}) : [] }) : []
@ -48,6 +56,142 @@ const selectedLessonStage = computed(() => {
return extractLessonStage(selectedLesson.value) return extractLessonStage(selectedLesson.value)
}) })
const isExpedited = computed(() => {
return selectedLesson.value?.expediteStatus === 1
})
/**
* 判断当前用户是否负责当前进度
* @param lesson 课程任务
* @returns 是否负责当前进度
*/
const isUserResponsibleForCurrentProgress = (lesson: LessonTask): boolean => {
if (!lesson || !user.userinfo) return false
const progressStatus = lesson.progressStatus
const userRoles = user.userinfo.roles
const userJobs = user.userinfo.jobs
// 判断进度负责人
switch (progressStatus) {
case 0: // 脚本制作 - 教师负责
case 1: // 脚本提交 - 教师负责
return userRoles === 1 && userJobs === 1 // 校方教师 + 课程制作教师
case 2: // 脚本审核 - 审核人员负责
return userJobs === 2 // 课程审核人员
case 3: // 脚本确认 - 教师负责
return userRoles === 1 && userJobs === 1 // 校方教师 + 课程制作教师
case 4: // 视频制作审核 - 审核人员负责
return userJobs === 2 // 课程审核人员
case 5: // 已完成
return false
default:
return false
}
}
/**
* 判断是否应该显示催办消息
* @param lesson 课程任务
* @returns 是否显示催办消息
*/
const shouldShowExpediteNotice = (lesson: LessonTask): boolean => {
if (!lesson || !user.userinfo) return false
// 条件1: 当前用户所负责的课程通过userId判断
const isUserCourse = lesson.userId === user.userinfo.id
// 条件2: 课程当前所在的进度是当前用户所负责的进度
const isResponsibleForProgress = isUserResponsibleForCurrentProgress(lesson)
// 条件3: 催办信息为1
const isExpedited = lesson.expediteStatus === 1
return isUserCourse && isResponsibleForProgress && isExpedited
}
const shouldShowExpediteNoticeForSelected = computed(() => {
return selectedLesson.value ? shouldShowExpediteNotice(selectedLesson.value) : false
})
/**
* 调试函数:打印当前账户所负责的课程状态
*/
const debugUserCourses = () => {
console.log('🔍 执行debugUserCourses函数...')
if (!user.userinfo) {
console.log('❌ 用户未登录')
return
}
console.log('👤 当前用户ID:', user.userinfo.id)
const allLessons = Object.values(groupedLessons.value).flat()
console.log('📚 所有课程数量:', allLessons.length)
// 过滤出当前用户负责的课程
const userCourses = allLessons.filter(lesson => lesson.userId === user.userinfo!.id)
console.log('🎯 当前用户负责的课程数量:', userCourses.length)
// 生成简洁的ID和状态对应关系
const courseStatusMap = userCourses.reduce((acc, lesson) => {
acc[`ID:${lesson.id}`] = `状态:${lesson.progressStatus}`
return acc
}, {} as Record<string, string>)
console.log('📋 当前用户负责的课程状态:', courseStatusMap)
// 催办状态的课程
const expeditedCourseMap = userCourses
.filter(lesson => lesson.expediteStatus === 1)
.reduce((acc, lesson) => {
acc[`ID:${lesson.id}`] = `催办:${lesson.expediteStatus}`
return acc
}, {} as Record<string, string>)
if (Object.keys(expeditedCourseMap).length > 0) {
console.log('🔔 催办状态的课程:', expeditedCourseMap)
}
// 需要处理的催办课程
const needAttentionMap = userCourses
.filter(lesson => shouldShowExpediteNotice(lesson))
.reduce((acc, lesson) => {
acc[`ID:${lesson.id}`] = `需处理:是`
return acc
}, {} as Record<string, string>)
if (Object.keys(needAttentionMap).length > 0) {
console.log('⚠️ 需要处理的催办课程:', needAttentionMap)
}
}
// 自动清除催办消息的辅助函数
const autoClearReminder = async (lessonId: number) => {
try {
console.log('开始清除催办消息课程ID:', lessonId)
await BussApi.clearReminder(lessonId)
console.log('催办消息清除成功课程ID:', lessonId)
// 显示清除成功的提示
toast.success({
msg: '催办消息已清除'
})
} catch (err: any) {
console.error('自动清除催办消息失败:', err)
// 显示清除失败的提示
toast.error({
msg: `清除催办消息失败: ${err?.message || '未知错误'}`
})
}
}
/** /**
* 判断advise是否为驳回状态 * 判断advise是否为驳回状态
* @param advise 修改建议字符串 * @param advise 修改建议字符串
@ -57,7 +201,7 @@ const isRejectAdvise = (advise: string | undefined): boolean => {
try { try {
if (!advise) return false if (!advise) return false
const adviseObj = JSON.parse(advise) const adviseObj = JSON.parse(advise)
return 'reject' in adviseObj return 'type' in adviseObj && 'data' in adviseObj
} catch { } catch {
return false return false
} }
@ -67,24 +211,76 @@ const adviseText = computed(() => {
try { try {
if (!selectedLesson.value?.advise) return '暂无修改建议' if (!selectedLesson.value?.advise) return '暂无修改建议'
const adviseObj = JSON.parse(selectedLesson.value.advise) const adviseObj = JSON.parse(selectedLesson.value.advise)
return adviseObj.reject || '暂无修改建议' return adviseObj.data || '暂无修改建议'
} catch { } catch {
return selectedLesson?.value?.advise || '暂无修改建议' return selectedLesson?.value?.advise || '暂无修改建议'
} }
}) })
const hasValidAdvise = computed(() => {
try {
if (!selectedLesson.value?.advise) return false
const adviseObj = JSON.parse(selectedLesson.value.advise)
const dataText = adviseObj.data
return dataText && dataText.trim() !== ''
} catch {
const adviseText = selectedLesson?.value?.advise
return adviseText && adviseText.trim() !== ''
}
})
const adviseTimestamp = computed(() => {
try {
if (!selectedLesson.value?.advise) return null
const adviseObj = JSON.parse(selectedLesson.value.advise)
return adviseObj.time || null
} catch {
return null
}
})
const formatTimestamp = (timestamp: string | null) => {
if (!timestamp) return ''
try {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const formatted = `${year}/${month}/${day} ${hour}:${minute}`
console.log('格式化时间:', timestamp, '->', formatted)
return formatted
} catch (error) {
console.error('时间格式化错误:', error)
return ''
}
}
const getAdviseText = (adviseString: string | undefined): string => { const getAdviseText = (adviseString: string | undefined): string => {
try { try {
if (!adviseString) return '' if (!adviseString) return ''
const adviseObj = JSON.parse(adviseString) const adviseObj = JSON.parse(adviseString)
const rejectText = adviseObj.reject || '' const dataText = adviseObj.data || ''
return rejectText return dataText
} catch (e) { } catch (e) {
console.error('JSON解析失败:', e) console.error('JSON解析失败:', e)
return '' return ''
} }
} }
const getAdviseTimestamp = (adviseString: string | undefined): string | null => {
try {
if (!adviseString) return null
const adviseObj = JSON.parse(adviseString)
return adviseObj.time || null
} catch (e) {
console.error('JSON解析失败:', e)
return null
}
}
const onCoursePick = ({ value }: { value: string }) => { const onCoursePick = ({ value }: { value: string }) => {
pickerCourseValue.value = value pickerCourseValue.value = value
pickerLessonValue.value = undefined pickerLessonValue.value = undefined
@ -92,6 +288,28 @@ const onCoursePick = ({ value }: { value: string }) => {
const onLessonPick = ({ value }: { value: number }) => { const onLessonPick = ({ value }: { value: number }) => {
pickerLessonValue.value = value pickerLessonValue.value = value
// 调试:打印选中课程的详细信息
if (selectedLesson.value) {
const lesson = selectedLesson.value
const isResponsible = isUserResponsibleForCurrentProgress(lesson)
const shouldShowExpedite = shouldShowExpediteNotice(lesson)
console.log('🎯 选中课程详情:', {
id: lesson.id,
courseName: lesson.courseName,
microLessonName: lesson.microLessonName,
progressStatus: lesson.progressStatus,
expediteStatus: lesson.expediteStatus,
userId: lesson.userId,
currentUserId: user.userinfo?.id,
userRoles: user.userinfo?.roles,
userJobs: user.userinfo?.jobs,
isUserCourse: lesson.userId === user.userinfo?.id,
isUserResponsibleForProgress: isResponsible,
shouldShowExpediteNotice: shouldShowExpedite
})
}
} }
/** /**
@ -119,10 +337,19 @@ const onStep0 = () => {
courseName: selectedLesson.value.courseName, courseName: selectedLesson.value.courseName,
microLessonName: selectedLesson.value.microLessonName, microLessonName: selectedLesson.value.microLessonName,
userId: selectedLesson.value.userId, userId: selectedLesson.value.userId,
progressStatus: ProgressStatus.SCRIPT_CREATING//1 progressStatus: ProgressStatus.SCRIPT_CREATING,//1
msg: "" // 提交时清除催办消息
} }
BussApi.updateLessonTask(selectedLesson.value.id, params).then(res => {
console.log('onStep0 请求体:', params)
console.log('onStep0 课程ID:', selectedLesson.value.id)
BussApi.updateLessonTask(selectedLesson.value.id, params).then(async res => {
// 清理催办提醒
if (selectedLesson.value?.id) {
await autoClearReminder(selectedLesson.value.id)
}
toast.success({ toast.success({
msg: '提交成功' msg: '提交成功'
}) })
@ -152,30 +379,49 @@ const onStep1 = (rejected: boolean = false) => {
toast.loading({ toast.loading({
msg: '正在处理...' msg: '正在处理...'
}) })
BussApi.updateLessonTask(
selectedLesson.value.id, const requestData = rejected ? {
rejected ? { advise: JSON.stringify({
advise: JSON.stringify({ reject: adviseTextValue.value.toString() }), type: user.hasRole('teacher') ? 'teacher' : 'general_admin',
courseName: selectedLesson.value.courseName, data: adviseTextValue.value.toString(),
microLessonName: selectedLesson.value.microLessonName, time: new Date().toISOString()
userId: selectedLesson.value.userId, }),
progressStatus: ProgressStatus.NOT_STARTED//0 courseName: selectedLesson.value.courseName,
} : { microLessonName: selectedLesson.value.microLessonName,
advise: JSON.stringify({ confirm: "" }), userId: selectedLesson.value.userId,
courseName: selectedLesson.value.courseName, progressStatus: ProgressStatus.NOT_STARTED,//0
microLessonName: selectedLesson.value.microLessonName, msg: "" // 驳回时清除催办消息
userId: selectedLesson.value.userId, } : {
progressStatus: ProgressStatus.SCRIPT_REVIEW//2 advise: JSON.stringify({ confirm: "" }),
}).then(res => { courseName: selectedLesson.value.courseName,
toast.success({ microLessonName: selectedLesson.value.microLessonName,
msg: rejected ? '驳回成功' : '审核通过' userId: selectedLesson.value.userId,
}) progressStatus: ProgressStatus.SCRIPT_REVIEW,//2
setTimeout(() => { msg: "" // 通过时清除催办消息
updateLessons() }
}, 1500);
}).catch(err => { console.log('onStep1 请求体:', requestData)
toast.error({ msg: err.message }) console.log('onStep1 课程ID:', selectedLesson.value.id)
BussApi.updateLessonTask(selectedLesson.value.id, requestData).then(async res => {
console.log('onStep1 任务状态更新成功:', res)
// 清理催办提醒
if (selectedLesson.value?.id) {
await autoClearReminder(selectedLesson.value.id)
}
toast.success({
msg: rejected ? '驳回成功' : '审核通过'
}) })
setTimeout(() => {
updateLessons()
}, 1500);
}).catch(err => {
console.error('onStep1 任务状态更新失败:', err)
toast.error({ msg: err.message })
})
}) })
} }
@ -194,30 +440,49 @@ const onStep2 = (rejected: boolean = false) => {
toast.loading({ toast.loading({
msg: '正在处理...' msg: '正在处理...'
}) })
BussApi.updateLessonTask(
selectedLesson.value.id, const requestData = rejected ? {
rejected ? { advise: JSON.stringify({
advise: JSON.stringify({ reject: adviseTextValue.value.toString() }), type: user.hasRole('teacher') ? 'teacher' : 'general_admin',
courseName: selectedLesson.value.courseName, data: adviseTextValue.value.toString(),
microLessonName: selectedLesson.value.microLessonName, time: new Date().toISOString()
userId: selectedLesson.value.userId, }),
progressStatus: ProgressStatus.SCRIPT_CREATING//1 courseName: selectedLesson.value.courseName,
} : { microLessonName: selectedLesson.value.microLessonName,
advise: JSON.stringify({ confirm: "" }), userId: selectedLesson.value.userId,
courseName: selectedLesson.value.courseName, progressStatus: ProgressStatus.SCRIPT_CREATING,//1
microLessonName: selectedLesson.value.microLessonName, msg: "" // 驳回时清除催办消息
userId: selectedLesson.value.userId, } : {
progressStatus: ProgressStatus.SCRIPT_CONFIRMED//3 advise: JSON.stringify({ confirm: "" }),
}).then(res => { courseName: selectedLesson.value.courseName,
toast.success({ microLessonName: selectedLesson.value.microLessonName,
msg: rejected ? '驳回成功' : '审核通过' userId: selectedLesson.value.userId,
}) progressStatus: ProgressStatus.SCRIPT_CONFIRMED,//3
setTimeout(() => { msg: "" // 通过时清除催办消息
updateLessons() }
}, 1500);
}).catch(err => { console.log('onStep2 请求体:', requestData)
toast.error({ msg: err.message }) console.log('onStep2 课程ID:', selectedLesson.value.id)
BussApi.updateLessonTask(selectedLesson.value.id, requestData).then(async res => {
console.log('onStep2 任务状态更新成功:', res)
// 清理催办提醒
if (selectedLesson.value?.id) {
await autoClearReminder(selectedLesson.value.id)
}
toast.success({
msg: rejected ? '驳回成功' : '审核通过'
}) })
setTimeout(() => {
updateLessons()
}, 1500);
}).catch(err => {
console.error('onStep2 任务状态更新失败:', err)
toast.error({ msg: err.message })
})
}) })
} }
@ -236,30 +501,49 @@ const onStep3 = (rejected: boolean = false) => {
toast.loading({ toast.loading({
msg: '正在处理...' msg: '正在处理...'
}) })
BussApi.updateLessonTask(
selectedLesson.value.id, const requestData = rejected ? {
rejected ? { advise: JSON.stringify({
advise: JSON.stringify({ reject: 'huertian' + adviseTextValue.value.toString() }), type: user.hasRole('teacher') ? 'teacher' : 'general_admin',
courseName: selectedLesson.value.courseName, data: adviseTextValue.value.toString(),
microLessonName: selectedLesson.value.microLessonName, time: new Date().toISOString()
userId: selectedLesson.value.userId, }),
progressStatus: ProgressStatus.SCRIPT_CONFIRMED//3 courseName: selectedLesson.value.courseName,
} : { microLessonName: selectedLesson.value.microLessonName,
advise: JSON.stringify({ confirm: "" }), userId: selectedLesson.value.userId,
courseName: selectedLesson.value.courseName, progressStatus: ProgressStatus.SCRIPT_REVIEW,//2
microLessonName: selectedLesson.value.microLessonName, msg: "" // 驳回时清除催办消息
userId: selectedLesson.value.userId, } : {
progressStatus: ProgressStatus.VIDEO_CREATING//4 advise: JSON.stringify({ confirm: "" }),
}).then(res => { courseName: selectedLesson.value.courseName,
toast.success({ microLessonName: selectedLesson.value.microLessonName,
msg: rejected ? '驳回成功' : '审核通过' userId: selectedLesson.value.userId,
}) progressStatus: ProgressStatus.VIDEO_CREATING,//4
setTimeout(() => { msg: "" // 通过时清除催办消息
updateLessons() }
}, 1500);
}).catch(err => { console.log('onStep3 请求体:', requestData)
toast.error({ msg: err.message }) console.log('onStep3 课程ID:', selectedLesson.value.id)
BussApi.updateLessonTask(selectedLesson.value.id, requestData).then(async res => {
console.log('onStep3 任务状态更新成功:', res)
// 清理催办提醒
if (selectedLesson.value?.id) {
await autoClearReminder(selectedLesson.value.id)
}
toast.success({
msg: rejected ? '驳回成功' : '审核通过'
}) })
setTimeout(() => {
updateLessons()
}, 1500);
}).catch(err => {
console.error('onStep3 任务状态更新失败:', err)
toast.error({ msg: err.message })
})
}) })
} }
const onStep4 = (rejected: boolean = false) => { const onStep4 = (rejected: boolean = false) => {
@ -277,41 +561,69 @@ const onStep4 = (rejected: boolean = false) => {
toast.loading({ toast.loading({
msg: '正在处理...' msg: '正在处理...'
}) })
BussApi.updateLessonTask(
selectedLesson.value.id, const requestData = rejected ? {
rejected ? { advise: JSON.stringify({
advise: JSON.stringify({ reject: adviseTextValue.value.toString() }), type: user.hasRole('teacher') ? 'teacher' : 'general_admin',
courseName: selectedLesson.value.courseName, data: adviseTextValue.value.toString(),
microLessonName: selectedLesson.value.microLessonName, time: new Date().toISOString()
userId: selectedLesson.value.userId, }),
progressStatus: ProgressStatus.SCRIPT_CONFIRMED//3 courseName: selectedLesson.value.courseName,
} : { microLessonName: selectedLesson.value.microLessonName,
advise: JSON.stringify({ confirm: "" }), userId: selectedLesson.value.userId,
courseName: selectedLesson.value.courseName, progressStatus: ProgressStatus.SCRIPT_CONFIRMED,//3
microLessonName: selectedLesson.value.microLessonName, msg: "" // 驳回时清除催办消息
userId: selectedLesson.value.userId, } : {
progressStatus: ProgressStatus.VIDEO_CONFIRMED//5 advise: JSON.stringify({ confirm: "" }),
}).then(res => { courseName: selectedLesson.value.courseName,
toast.success({ microLessonName: selectedLesson.value.microLessonName,
msg: rejected ? '驳回成功' : '审核通过' userId: selectedLesson.value.userId,
}) progressStatus: ProgressStatus.VIDEO_CONFIRMED,//5
setTimeout(() => { msg: "" // 通过时清除催办消息
updateLessons() }
}, 1500);
}).catch(err => { console.log('onStep4 请求体:', requestData)
toast.error({ msg: err.message }) console.log('onStep4 课程ID:', selectedLesson.value.id)
BussApi.updateLessonTask(selectedLesson.value.id, requestData).then(async res => {
console.log('onStep4 任务状态更新成功:', res)
// 清理催办提醒
if (selectedLesson.value?.id) {
await autoClearReminder(selectedLesson.value.id)
}
toast.success({
msg: rejected ? '驳回成功' : '审核通过'
}) })
setTimeout(() => {
updateLessons()
}, 1500);
}).catch(err => {
console.error('onStep4 任务状态更新失败:', err)
toast.error({ msg: err.message })
})
}) })
} }
const updateLessons = async () => { const updateLessons = async () => {
console.log('🔄 开始更新课程列表...')
adviseTextValue.value = "" adviseTextValue.value = ""
if (!user.hasValidToken() || !user.userinfo) { if (!user.hasValidToken() || !user.userinfo) {
console.log('❌ 用户未登录,跳转到登录页')
toast.error({ msg: '请先登录' }) toast.error({ msg: '请先登录' })
router.replace('/pages/login/index') router.replace('/pages/login/index')
return return
} }
console.log('✅ 用户已登录,用户信息:', {
id: user.userinfo.id,
username: user.userinfo.username,
roles: user.userinfo.roles,
jobs: user.userinfo.jobs
})
toast.loading({ toast.loading({
msg: '加载中...' msg: '加载中...'
}) })
@ -338,11 +650,24 @@ const updateLessons = async () => {
...Object.keys(groupData) ...Object.keys(groupData)
] ]
if (route.params?.courseName) { // 调试:打印当前账户所负责的课程状态
onCoursePick({ value: decodeURI(route.params.courseName) }) console.log('🚀 开始调试用户课程状态...')
if (route.params?.lessonId) { debugUserCourses()
if (typeof route.params.lessonId === 'string') { console.log('✅ 调试完成')
onLessonPick({ value: parseInt(route.params.lessonId) })
// 从本地缓存获取参数
const progress_params = uni.getStorageSync('progress_params')
if (progress_params && progress_params.courseName && progress_params.lessonId) {
// 选择课程
if (Object.keys(groupData).includes(progress_params.courseName)) {
onCoursePick({ value: progress_params.courseName })
// 选择课程ID
const lessonIdValue = parseInt(progress_params.lessonId)
if (!isNaN(lessonIdValue)) {
onLessonPick({ value: lessonIdValue })
} }
} }
} }
@ -359,9 +684,24 @@ const updateLessons = async () => {
onMounted(() => { onMounted(() => {
console.log('📱 页面已挂载,开始加载数据...')
// 从本地缓存获取参数
const progress_params = uni.getStorageSync('progress_params')
updateLessons() updateLessons()
}) })
// 添加onShow钩子确保每次显示页面时都刷新数据
onShow(() => {
const progress_params = uni.getStorageSync('progress_params')
// 如果有缓存参数,则需要重新加载数据
if (progress_params && progress_params.courseName && progress_params.lessonId) {
updateLessons()
}
})
const refresh = async () => { const refresh = async () => {
try { try {
const startTime = Date.now() const startTime = Date.now()
@ -405,6 +745,11 @@ const handleClick = () => {
<wd-status-tip v-if="!pickerLessonValue" <wd-status-tip v-if="!pickerLessonValue"
image="https://registry.npmmirror.com/wot-design-uni-assets/*/files/search.png" tip="请先选择微课" /> image="https://registry.npmmirror.com/wot-design-uni-assets/*/files/search.png" tip="请先选择微课" />
<div v-else class="space-y-6"> <div v-else class="space-y-6">
<!-- 催办状态提示 -->
<!-- <div v-if="shouldShowExpediteNoticeForSelected" class="mx-2">
<wd-notice-bar text="🔔 此课程已被催办,请加快处理进度" type="warning" />
</div> -->
<div> <div>
<wd-steps :active="selectedLessonStage?.step || 0" align-center> <wd-steps :active="selectedLessonStage?.step || 0" align-center>
<wd-step v-for="(step, index) in getLessonSteps(selectedLesson!, true)" :key="index" :title="step.title" <wd-step v-for="(step, index) in getLessonSteps(selectedLesson!, true)" :key="index" :title="step.title"
@ -414,20 +759,25 @@ const handleClick = () => {
<div v-if="selectedLessonStage?.step === 0" class="px-2"> <div v-if="selectedLessonStage?.step === 0" class="px-2">
<div v-if="user.hasRole('teacher')"> <div v-if="user.hasRole('teacher')">
<div v-if="isRejectAdvise(selectedLesson?.advise)" class="m-2 text-sm"> <div v-if="isRejectAdvise(selectedLesson?.advise) && hasValidAdvise" class="m-2 text-sm">
<wd-card title="修改建议"> <wd-card title="修改建议">
{{ adviseText }} <div class="space-y-2">
<div class="break-words whitespace-pre-wrap">{{ adviseText }}</div>
<div v-if="adviseTimestamp" class="text-xs text-gray-500">
时间{{ formatTimestamp(adviseTimestamp) }}
</div>
</div>
</wd-card> </wd-card>
</div> </div>
<wd-cell-group> <div class="mb-4">
<wd-cell title="脚本提交途径" :title-width="'100px'" center custom-class="mb-4"> <div class="text-sm font-semibold text-gray-700 mb-4 ">脚本提交途径</div>
<wd-radio-group v-model="script_file_destination" shape="button"> <wd-radio-group v-model="script_file_destination" class="flex justify-center">
<wd-radio value="wechat">微信</wd-radio> <wd-radio value="wechat" shape="button" class="mx-1">微信</wd-radio>
<wd-radio value="qq">QQ</wd-radio> <wd-radio value="qq" shape="button" class="mx-1">QQ</wd-radio>
<wd-radio value="platform">平台</wd-radio> <wd-radio value="baidu" shape="button" class="mx-1">百度网盘</wd-radio>
</wd-radio-group> <wd-radio value="platform" shape="button" class="mx-1">平台</wd-radio>
</wd-cell> </wd-radio-group>
</wd-cell-group> </div>
<wd-button type="primary" block @click="onStep0" custom-class="w-full">提交脚本</wd-button> <wd-button type="primary" block @click="onStep0" custom-class="w-full">提交脚本</wd-button>
</div> </div>
<StatusBlock v-else title="脚本还未提交" subtitle="请耐心等待提交"> <StatusBlock v-else title="脚本还未提交" subtitle="请耐心等待提交">
@ -446,9 +796,14 @@ const handleClick = () => {
</StatusBlock> </StatusBlock>
</div> </div>
<div v-else> <div v-else>
<div v-if="isRejectAdvise(selectedLesson?.advise) === true" class=" m-2 text-sm"> <div v-if="isRejectAdvise(selectedLesson?.advise) && hasValidAdvise" class=" m-2 text-sm">
<wd-card title="修改建议"> <wd-card title="修改建议">
{{ adviseText }} <div class="space-y-2">
<div class="break-words whitespace-pre-wrap">{{ adviseText }}</div>
<div v-if="adviseTimestamp" class="text-xs text-gray-500">
时间{{ formatTimestamp(adviseTimestamp) }}
</div>
</div>
</wd-card> </wd-card>
</div> </div>
<StatusBlock title="脚本已提交" subtitle="请耐心等待审核"> <StatusBlock title="脚本已提交" subtitle="请耐心等待审核">
@ -457,7 +812,7 @@ const handleClick = () => {
</template> </template>
</StatusBlock> </StatusBlock>
<div class="mt-4 space-y-4"> <div class="mt-4 space-y-4">
<div class="flex gap-3 px-4"> <div class="flex gap-3 px-4 justify-center">
<wd-button type="primary" block @click="onStep1()">确认</wd-button> <wd-button type="primary" block @click="onStep1()">确认</wd-button>
<wd-button type="error" block @click="onStep1(true)">驳回</wd-button> <wd-button type="error" block @click="onStep1(true)">驳回</wd-button>
</div> </div>
@ -467,13 +822,23 @@ const handleClick = () => {
<div v-if="selectedLessonStage?.step === 2"> <div v-if="selectedLessonStage?.step === 2">
<div v-if="user.hasRole('teacher')"> <div v-if="user.hasRole('teacher')">
<div v-if="isRejectAdvise(selectedLesson?.advise) && hasValidAdvise" class="m-2 text-sm">
<wd-card title="修改建议">
<div class="space-y-2">
<div class="break-words whitespace-pre-wrap">{{ adviseText }}</div>
<div v-if="adviseTimestamp" class="text-xs text-gray-500">
时间{{ formatTimestamp(adviseTimestamp) }}
</div>
</div>
</wd-card>
</div>
<StatusBlock title="脚本审核已完成" subtitle="请确认脚本内容"> <StatusBlock title="脚本审核已完成" subtitle="请确认脚本内容">
<template #icon> <template #icon>
<div class="i-tabler-file-text text-7xl text-neutral-400"></div> <div class="i-tabler-file-text text-7xl text-neutral-400"></div>
</template> </template>
</StatusBlock> </StatusBlock>
<div class="mt-4 space-y-4"> <div class="mt-4 space-y-4">
<div class="flex gap-3 px-4"> <div class="flex gap-3 px-4 justify-center">
<wd-button type="primary" block @click="onStep2()">确认</wd-button> <wd-button type="primary" block @click="onStep2()">确认</wd-button>
<wd-button type="error" block @click="onStep2(true)">驳回</wd-button> <wd-button type="error" block @click="onStep2(true)">驳回</wd-button>
</div> </div>
@ -490,32 +855,30 @@ const handleClick = () => {
<div v-if="selectedLessonStage?.step === 3"> <div v-if="selectedLessonStage?.step === 3">
<div v-if="user.hasRole('teacher')"> <div v-if="user.hasRole('teacher')">
<div v-if="getAdviseText(selectedLesson?.advise).startsWith('huertian')" class=" m-2 text-sm"> <StatusBlock title="后期制作进行中" subtitle="请等待后期制作">
<wd-card title="修改建议">
{{ getAdviseText(selectedLesson?.advise).substring('huertian'.length) }}
</wd-card>
</div>
<StatusBlock title="视频拍摄制作进行中" subtitle="请等待视频拍摄制作">
<template #icon> <template #icon>
<div class="i-tabler-video text-7xl text-neutral-400"></div> <div class="i-tabler-video text-7xl text-neutral-400"></div>
</template> </template>
</StatusBlock> </StatusBlock>
</div> </div>
<div v-else> <div v-else>
<div v-if="!getAdviseText(selectedLesson?.advise).startsWith('huertian')"> <div v-if="isRejectAdvise(selectedLesson?.advise) && hasValidAdvise" class=" m-2 text-sm">
<div v-if="isRejectAdvise(selectedLesson?.advise) === true" class=" m-2 text-sm"> <wd-card title="修改建议">
<wd-card title="修改建议"> <div class="space-y-2">
{{ adviseText }} <div class="break-words whitespace-pre-wrap">{{ adviseText }}</div>
</wd-card> <div v-if="adviseTimestamp" class="text-xs text-gray-500">
</div> 时间{{ formatTimestamp(adviseTimestamp) }}
</div>
</div>
</wd-card>
</div> </div>
<StatusBlock title="视频拍摄制作进行中" subtitle="完成后请确认"> <StatusBlock title="后期制作进行中" subtitle="完成后请确认">
<template #icon> <template #icon>
<div class="i-tabler-video text-7xl text-neutral-400"></div> <div class="i-tabler-video text-7xl text-neutral-400"></div>
</template> </template>
</StatusBlock> </StatusBlock>
<div class="mt-4 space-y-4"> <div class="mt-4 space-y-4">
<div class="flex gap-3 px-4 center"> <div class="flex gap-3 px-4 justify-center">
<wd-button type="primary" block @click="onStep3()">通过</wd-button> <wd-button type="primary" block @click="onStep3()">通过</wd-button>
<wd-button type="error" block @click="onStep3(true)">驳回</wd-button> <wd-button type="error" block @click="onStep3(true)">驳回</wd-button>
</div> </div>
@ -525,20 +888,20 @@ const handleClick = () => {
<div v-if="selectedLessonStage?.step === 4"> <div v-if="selectedLessonStage?.step === 4">
<div v-if="user.hasRole('teacher')"> <div v-if="user.hasRole('teacher')">
<StatusBlock title="视频拍摄制作已完成" subtitle="请确认视频内容"> <StatusBlock title="后期制作已完成" subtitle="请确认资源内容">
<template #icon> <template #icon>
<div class="i-tabler-brand-parsinta text-7xl text-neutral-400"></div> <div class="i-tabler-brand-parsinta text-7xl text-neutral-400"></div>
</template> </template>
</StatusBlock> </StatusBlock>
<div class="mt-4 space-y-4"> <div class="mt-4 space-y-4">
<div class="flex gap-3 px-4"> <div class="flex gap-3 px-4 justify-center">
<wd-button type="primary" block @click="onStep4()">通过</wd-button> <wd-button type="primary" block @click="onStep4()">通过</wd-button>
<wd-button type="error" block @click="onStep4(true)">驳回</wd-button> <wd-button type="error" block @click="onStep4(true)">驳回</wd-button>
</div> </div>
</div> </div>
</div> </div>
<div v-else> <div v-else>
<StatusBlock title="视频内容确认中" subtitle="请等待视频内容确认"> <StatusBlock title="资源内容确认中" subtitle="请等待资源内容确认">
<template #icon> <template #icon>
<div class="i-tabler-brand-parsinta text-7xl text-neutral-400"></div> <div class="i-tabler-brand-parsinta text-7xl text-neutral-400"></div>
</template> </template>

View File

@ -84,6 +84,22 @@ export const useUser = defineStore("user", () => {
} }
} }
// 保存登录凭证
function saveCredentials(email: string, password: string) {
uni.setStorageSync('credentials', { email, password });
}
// 获取登录凭证
function getCredentials(): { email: string, password: string } | null {
const credentials = uni.getStorageSync('credentials');
return credentials || null;
}
// 清除登录凭证
function clearCredentials() {
uni.removeStorageSync('credentials');
}
// 检查 token 是否有效 // 检查 token 是否有效
function hasValidToken(): boolean { function hasValidToken(): boolean {
return !!token.value && token.value.length > 0; return !!token.value && token.value.length > 0;
@ -92,6 +108,8 @@ export const useUser = defineStore("user", () => {
function logout() { function logout() {
token.value = null; token.value = null;
userinfo.value = null; userinfo.value = null;
uni.removeStorageSync('token');
// 不要清除登录凭证,以便用户下次能够快速登录
} }
return { return {
@ -103,6 +121,9 @@ export const useUser = defineStore("user", () => {
canEditCourse, canEditCourse,
logout, logout,
setToken, setToken,
hasValidToken hasValidToken,
saveCredentials,
getCredentials,
clearCredentials
}; };
}); });

View File

@ -24,8 +24,12 @@ export interface LessonTask {
videoConfirmTime?: number; videoConfirmTime?: number;
/** 任务完成时间(时间戳) */ /** 任务完成时间(时间戳) */
finishTime?: number; finishTime?: number;
/** 催办状态 0:未催办 1:催办 */
expediteStatus: number;
/** 建议/反馈信息 */ /** 建议/反馈信息 */
advise?: string; advise?: string;
/** 催办消息 */
msg?: string | null;
/** 创建时间(时间戳) */ /** 创建时间(时间戳) */
createdAt: number; createdAt: number;
/** 更新时间(时间戳) */ /** 更新时间(时间戳) */
@ -47,8 +51,8 @@ export interface LessonTaskPagination {
* NOT_STARTED = 0, // 脚本制作 * NOT_STARTED = 0, // 脚本制作
SCRIPT_CREATING = 1, // 脚本审核 SCRIPT_CREATING = 1, // 脚本审核
SCRIPT_REVIEW = 2, // 脚本审核 SCRIPT_REVIEW = 2, // 脚本审核
SCRIPT_CONFIRMED = 3, // 视频拍摄与制作 SCRIPT_CONFIRMED = 3, // 后期制作
VIDEO_CREATING = 4, // 视频确认 VIDEO_CREATING = 4, // 资源确认
VIDEO_CONFIRMED = 5 // 任务完成 VIDEO_CONFIRMED = 5 // 任务完成
*/ */
// 进度状态枚举 // 进度状态枚举
@ -56,12 +60,12 @@ export enum ProgressStatus {
NOT_STARTED = 0, // 脚本制作 NOT_STARTED = 0, // 脚本制作
SCRIPT_CREATING = 1, // 脚本审核 SCRIPT_CREATING = 1, // 脚本审核
SCRIPT_REVIEW = 2, // 脚本审核 SCRIPT_REVIEW = 2, // 脚本审核
SCRIPT_CONFIRMED = 3, // 视频拍摄与制作 SCRIPT_CONFIRMED = 3, // 后期制作
VIDEO_CREATING = 4, // 视频确认 VIDEO_CREATING = 4, // 资源确认
VIDEO_CONFIRMED = 5 // 任务完成 VIDEO_CONFIRMED = 5 // 任务完成
} }
export type FileUploadDestination = "qq" | "wechat" | "platform"; export type FileUploadDestination = "qq" | "wechat" | "platform" | "baidu";
/** /**
* 创建课程任务请求参数接口 * 创建课程任务请求参数接口
@ -101,6 +105,10 @@ export interface UpdateLessonTaskRequest {
videoConfirmTime?: number; videoConfirmTime?: number;
/** 任务完成时间 */ /** 任务完成时间 */
finishTime?: number; finishTime?: number;
/** 催办状态 0:未催办 1:催办 */
expediteStatus?: number;
/** 建议信息 */ /** 建议信息 */
advise?: string; advise?: string;
/** 催办消息 */
msg?: string | null;
} }

View File

@ -20,18 +20,18 @@ export interface Authority {
// 角色枚举 // 角色枚举
export enum Roles { export enum Roles {
TEACHER = 1, // 教师 TEACHER = 1, // 校方教师
GENERAL_ADMIN = 2, // 管理员 GENERAL_ADMIN = 2, // 公司课程顾问
CONTACTOR = 3, // 项目负责人 CONTACTOR = 3, // 校方项目负责人
SYSTEM_ADMIN = 4 // 系统管理员 SYSTEM_ADMIN = 4 // 公司系统管理员
} }
// 岗位枚举 // 岗位枚举
export enum Jobs { export enum Jobs {
COURSE_TEACHER = 1, // 课程教师 COURSE_TEACHER = 1, // 课程制作教师
PROJECT_MANAGER = 2, // 项目经理 PROJECT_MANAGER = 2, // 课程审核人员
COURSE_CONTACTOR = 3, // 课程负责人 COURSE_CONTACTOR = 3, // 校方项目负责人
SYSTEM_MANAGER = 4 // 系统管理 SYSTEM_MANAGER = 4 // 公司系统管理
} }
// 用户状态枚举 // 用户状态枚举

View File

@ -7,8 +7,8 @@ export const extractLessonStage = (lesson: LessonTask) => {
script_creating: currentStep >= 1, // 脚本制作 script_creating: currentStep >= 1, // 脚本制作
script_review: currentStep >= 2, // 脚本审核 script_review: currentStep >= 2, // 脚本审核
script_confirmed: currentStep >= 3, // 脚本确认 script_confirmed: currentStep >= 3, // 脚本确认
video_creating: currentStep >= 4, // 视频制作 video_creating: currentStep >= 4, // 后期制作
video_confirmed: currentStep >= 5, // 视频确认 video_confirmed: currentStep >= 5, // 资源确认
step: 0, step: 0,
}; };
stages.step = currentStep; stages.step = currentStep;
@ -21,7 +21,7 @@ export const getLessonRole = (lesson: LessonTask): 'teacher' | 'admin' | '' => {
switch (lesson.progressStatus) { switch (lesson.progressStatus) {
case 0: // 未开始 case 0: // 未开始
case 2: // 脚本审核 case 2: // 脚本审核
case 4: // 视频拍摄与制作 case 4: // 后期制作
return 'teacher'; return 'teacher';
case 1: // 脚本制作 case 1: // 脚本制作
@ -37,11 +37,11 @@ export const calcLessonProgress = (lesson: LessonTask) => {
if (!lesson) return 0; if (!lesson) return 0;
// 根据 progressStatus 计算进度 // 根据 progressStatus 计算进度
// 0-未开始, 1-脚本制作, 2-脚本审核, 3-脚本确认, 4-视频拍摄与制作, 5-视频确认 // 0-未开始, 1-脚本制作, 2-脚本审核, 3-脚本确认, 4-后期制作, 5-资源确认
switch (lesson.progressStatus) { switch (lesson.progressStatus) {
case 5: // 视频确认 case 5: // 资源确认
return 100; return 100;
case 4: // 视频拍摄与制作 case 4: // 后期制作
return 80; return 80;
case 3: // 脚本确认 case 3: // 脚本确认
return 60; return 60;
@ -92,20 +92,20 @@ export const getLessonSteps = (lesson: LessonTask, simplify: boolean = false) =>
: "确认脚本内容", : "确认脚本内容",
}, },
{ {
title: progress.video_creating ? "拍摄制作" : undefined, title: progress.video_creating ? "后期制作" : undefined,
description: progress.video_creating description: progress.video_creating
? simplify ? simplify
? "已完成" ? "已完成"
: `已于 ${formatTime(lesson.videoCreateTime)} 完成拍摄制作` : `已于 ${formatTime(lesson.videoCreateTime)} 完成后期制作`
: "拍摄制作", : "后期制作",
}, },
{ {
title: progress.video_confirmed ? "视频确认" : undefined, title: progress.video_confirmed ? "资源确认" : undefined,
description: progress.video_confirmed description: progress.video_confirmed
? simplify ? simplify
? "已完成" ? "已完成"
: `已于 ${formatTime(lesson.videoConfirmTime)} 完成视频确认` : `已于 ${formatTime(lesson.videoConfirmTime)} 完成资源确认`
: "确认视频内容", : "确认资源内容",
}, },
]; ];
}; };