fix: course package detail page - permission gate, status logic, preview lock

This commit is contained in:
2026-05-01 10:50:56 +08:00
parent 6e4ef03e07
commit 53989e5731
+142 -17
View File
@@ -24,31 +24,43 @@
<div class="section">
<h2>包含课节</h2>
<div v-if="courseGroups.length === 0" class="empty">暂无课节</div>
<van-cell v-for="g in courseGroups" :key="g.id" :title="g.group_name">
<template #label>
<div v-if="g.course_sections?.length">
{{ g.course_sections.map((s: any) => (s.course_name || '') + '(' + s.section_name + ')').join('、') }}
</div>
<div v-else style="color: #999;">无课程</div>
</template>
</van-cell>
<div v-for="g in courseGroups" :key="g.id" class="group-sections">
<div class="group-name">{{ g.group_name }}</div>
<div v-if="g.course_sections?.length" class="section-list">
<div v-for="s in g.course_sections" :key="s.id" class="section-item" :class="{ locked: !isPreviewable(s) }" @click="handleSectionClick(s)">
<span class="section-label">{{ s.course_name || '' }}{{ s.section_name }}</span>
<span v-if="isPreviewable(s)" class="section-action">
<span class="preview-badge">试看</span>
<span class="section-arrow">&gt;</span>
</span>
<span v-else class="section-action">
<span class="lock-label">已锁定</span>
</span>
</div>
</div>
<div class="footer">
<van-button v-if="!isPublished" type="primary" @click="onEdit">编辑</van-button>
<van-button v-if="!isPublished" type="danger" @click="onDelete">删除</van-button>
<van-button v-if="!isPublished" type="success" @click="onPublish">发布上架</van-button>
<van-button v-if="isPublished" type="warning" @click="onUnpublish">下架</van-button>
<div v-else style="color: #999; padding: 0.08rem 0.32rem; font-size: 0.26rem;">无课程</div>
</div>
</div>
</div>
<div class="footer" v-if="isOwner">
<van-button v-if="!isActive" type="primary" @click="onEdit">编辑</van-button>
<van-button v-if="!isActive" type="danger" @click="onDelete">删除</van-button>
<van-button v-if="!isActive" type="success" @click="onPublish">发布上架</van-button>
<van-button v-if="isActive" type="warning" @click="onUnpublish">下架</van-button>
</div>
<div class="footer" v-else-if="isLoggedIn">
<van-button type="default" @click="goBack">返回</van-button>
</div>
</div>
</template>
<script lang="ts">
import {computed, defineComponent, onMounted, ref} from 'vue';
import {Button, Cell, CellGroup, Tag, showConfirmDialog, showFailToast} from 'vant';
import {Button, Cell, CellGroup, Tag, showConfirmDialog, showDialog, showFailToast} from 'vant';
import {useRoute, useRouter} from 'vue-router';
import useCoursePackage from '~/store/course-package';
import {CourseGroup} from '~/types';
import useUser from '~/store/user';
import {CourseGroup, CourseSection} from '~/types';
export default defineComponent({
name: 'course-package-detail',
@@ -62,12 +74,19 @@ export default defineComponent({
const route = useRoute();
const router = useRouter();
const {store, findById, remove, listPackageItems, publish, unpublish} = useCoursePackage();
const {store: userStore} = useUser();
const pkg = ref<any>(null);
const courseGroups = ref<CourseGroup[]>([]);
const loading = ref(false);
const isActive = computed(() => pkg.value?.package_status === 'ACTIVE');
const isPublished = computed(() => !!pkg.value?.published_at);
const isLoggedIn = computed(() => !!localStorage.getItem('Authorization'));
const isOwner = computed(() => {
if (!pkg.value?.created_by || !userStore.current?.hty_id) return false;
return pkg.value.created_by === userStore.current.hty_id;
});
const loadData = async () => {
const id = route.query.id as string;
@@ -95,6 +114,8 @@ export default defineComponent({
loading.value = false;
};
const goBack = () => router.go(-1);
const onEdit = () => {
router.push(`/course/course-package/edit?id=${pkg.value?.id}`);
};
@@ -136,11 +157,44 @@ export default defineComponent({
} catch { /* cancelled */ }
};
const previewSection = (section: CourseSection) => {
if (section.id) {
router.push(`/course/section/detail?id=${section.id}`);
}
};
// Non-owner can only preview the first section; owner can preview all
const firstSectionId = computed(() => {
for (const g of courseGroups.value) {
if (g.course_sections?.length) return g.course_sections[0].id;
}
return null;
});
const isPreviewable = (section: CourseSection) => {
if (!section.id) return false;
if (isOwner.value) return true;
return section.id === firstSectionId.value;
};
const handleSectionClick = (section: CourseSection) => {
if (isPreviewable(section)) {
previewSection(section);
} else {
showDialog({
title: '提示',
message: '请购买课包后查看完整内容',
confirmButtonText: '知道了',
});
}
};
onMounted(loadData);
return {
pkg, courseGroups, loading, isPublished,
onEdit, onDelete, onPublish, onUnpublish,
pkg, courseGroups, loading, isActive, isPublished, isOwner, isLoggedIn,
onEdit, onDelete, onPublish, onUnpublish, previewSection, goBack,
isPreviewable, handleSectionClick,
};
},
});
@@ -181,6 +235,77 @@ export default defineComponent({
padding: 0.32rem;
}
.group-sections {
margin-bottom: 0.16rem;
.group-name {
font-size: 0.28rem;
font-weight: 600;
color: #333;
padding: 0.16rem 0.32rem 0.08rem;
}
.section-list {
background: #fff;
border-radius: 8px;
margin: 0 0.24rem;
overflow: hidden;
}
.section-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.2rem 0.32rem;
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
font-size: 0.26rem;
color: #333;
&:last-child {
border-bottom: none;
}
&:active {
background: #f5f5f5;
}
&.locked {
color: #bbb;
cursor: default;
&:active {
background: #fff;
}
}
.section-action {
display: flex;
align-items: center;
gap: 0.08rem;
flex-shrink: 0;
}
.preview-badge {
font-size: 0.2rem;
color: #fff;
background: #1989fa;
border-radius: 4px;
padding: 0.02rem 0.1rem;
}
.lock-label {
font-size: 0.2rem;
color: #ccc;
}
.section-arrow {
color: #ccc;
font-size: 0.28rem;
}
}
}
.footer {
position: absolute;
bottom: 0.16rem;