323 lines
9.2 KiB
Vue
323 lines
9.2 KiB
Vue
<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">></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>
|