Files
huike-front/src/pages/qumu/course-package/detail.vue
T

323 lines
9.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="main">
<div class="body">
<div class="header">
<h1 class="title">{{ pkg?.package_name }}</h1>
<van-tag :type="pkg?.package_status === 'ACTIVE' ? 'success' : 'warning'" size="medium">
{{ pkg?.package_status === 'ACTIVE' ? '上架中' : '已下架' }}
</van-tag>
<van-tag v-if="isPublished" type="info" size="medium" style="margin-left: 0.08rem;">已发布快照</van-tag>
</div>
<div class="section">
<h2>基本信息</h2>
<van-cell-group inset>
<van-cell title="描述" :value="pkg?.description || '无'" />
<van-cell title="课次数量" :value="pkg?.total_lessons?.toString() || '无'" />
<van-cell title="原价" :value="pkg?.original_price ? '¥' + (pkg.original_price / 100).toFixed(2) : '无'" />
<van-cell title="售价" :value="pkg?.selling_price ? '¥' + (pkg.selling_price / 100).toFixed(2) : '无'" />
<van-cell title="有效期" :value="pkg?.validity_days ? pkg.validity_days + ' 天' : '无'" />
<van-cell title="排序" :value="pkg?.sort_order?.toString() || '无'" />
</van-cell-group>
</div>
<div class="section">
<h2>包含课节</h2>
<div v-if="courseGroups.length === 0" class="empty">暂无课节</div>
<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 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, showDialog, showFailToast} from 'vant';
import {useRoute, useRouter} from 'vue-router';
import useCoursePackage from '~/store/course-package';
import useUser from '~/store/user';
import {CourseGroup, CourseSection} from '~/types';
export default defineComponent({
name: 'course-package-detail',
components: {
[Button.name]: Button,
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
[Tag.name]: Tag,
},
setup() {
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;
if (!id) {
showFailToast('缺少课包 ID');
router.go(-1);
return;
}
loading.value = true;
const data = await findById(id);
if (!data) {
router.go(-1);
return;
}
pkg.value = data;
if (data.published_snapshot?.course_groups) {
// 从快照读取课节数据
courseGroups.value = data.published_snapshot.course_groups;
} else {
// 实时获取课节数据
const groups = await listPackageItems(id);
courseGroups.value = groups || [];
}
loading.value = false;
};
const goBack = () => router.go(-1);
const onEdit = () => {
router.push(`/course/course-package/edit?id=${pkg.value?.id}`);
};
const onDelete = async () => {
try {
await showConfirmDialog({message: `确认删除课包【${pkg.value?.package_name}】?`});
const ok = await remove(pkg.value?.id!);
if (ok) router.go(-1);
} catch { /* cancelled */ }
};
const onPublish = async () => {
try {
await showConfirmDialog({
title: '发布课包',
message: '发布后将锁定课包内容,生成快照,不可再编辑。确认发布?',
});
const id = pkg.value?.id!;
// 获取完整课节数据作为快照
let groups = courseGroups.value;
if (groups.length === 0) {
groups = (await listPackageItems(id)) || [];
}
const snapshot = { course_groups: groups };
const ok = await publish(id, snapshot);
if (ok) await loadData();
} catch { /* cancelled */ }
};
const onUnpublish = async () => {
try {
await showConfirmDialog({
title: '下架课包',
message: '下架后课包内容可编辑,确认下架?',
});
const ok = await unpublish(pkg.value?.id!);
if (ok) await loadData();
} 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, isActive, isPublished, isOwner, isLoggedIn,
onEdit, onDelete, onPublish, onUnpublish, previewSection, goBack,
isPreviewable, handleSectionClick,
};
},
});
</script>
<style scoped lang="less">
.main {
.body {
height: calc(100% - 1.06rem);
padding: 0.32rem 0;
overflow: auto;
}
.header {
text-align: center;
margin-bottom: 0.32rem;
}
.title {
font-size: 0.48rem;
margin-block-start: 0;
margin-block-end: 0.16rem;
}
h2 {
font-size: 0.32rem;
padding: 0 0.32rem;
}
.section {
margin-bottom: 0.32rem;
}
.empty {
text-align: center;
color: #999;
font-size: 0.26rem;
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;
width: 100%;
padding: 0.01rem 0.32rem;
display: flex;
gap: 0.16rem;
button {
flex: 1;
}
}
}
</style>