feat: add course package storefront for unauth and home pages
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="course-package-store">
|
||||
<template v-if="state.loading">
|
||||
<div class="skeleton-list" :class="{ compact }">
|
||||
<div v-for="i in 4" :key="i" class="skeleton-card">
|
||||
<van-skeleton title :row="2" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="state.error">
|
||||
<van-empty description="加载失败">
|
||||
<van-button size="small" type="primary" @click="loadPackages">重试</van-button>
|
||||
</van-empty>
|
||||
</template>
|
||||
<template v-else-if="!packages.length">
|
||||
<van-empty description="暂无可售课包" />
|
||||
</template>
|
||||
<div v-else class="package-list" :class="{ compact }">
|
||||
<div v-for="pkg in packages" :key="pkg.id" class="package-card">
|
||||
<div class="card-cover">
|
||||
<van-image
|
||||
:src="pkg.cover_image_url"
|
||||
fit="cover"
|
||||
height="3.2rem"
|
||||
>
|
||||
<template v-slot:error>
|
||||
<div class="cover-placeholder">📦</div>
|
||||
</template>
|
||||
</van-image>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-title">{{ pkg.package_name }}</div>
|
||||
<div class="card-desc" v-if="pkg.description">{{ pkg.description }}</div>
|
||||
<div class="card-meta">
|
||||
<span v-if="pkg.total_lessons" class="meta-item">{{ pkg.total_lessons }} 课次</span>
|
||||
<span v-if="pkg.validity_days" class="meta-item">{{ pkg.validity_days }} 天有效</span>
|
||||
</div>
|
||||
<van-button size="small" round plain type="primary" class="preview-btn" @click="onPreview(pkg)">
|
||||
试看
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Button, Empty, Image, Skeleton, showDialog } from 'vant';
|
||||
import useCoursePackage from '~/store/course-package';
|
||||
import { CoursePackage } from '~/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'course-package-store',
|
||||
components: {
|
||||
[Button.name!]: Button,
|
||||
[Empty.name!]: Empty,
|
||||
[Image.name!]: Image,
|
||||
[Skeleton.name!]: Skeleton,
|
||||
},
|
||||
props: {
|
||||
orgId: { type: String, default: '' },
|
||||
compact: { type: Boolean, default: false },
|
||||
},
|
||||
setup(props) {
|
||||
const router = useRouter();
|
||||
const { store, queryPublicPackages, queryOrgPackages } = useCoursePackage();
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
error: false,
|
||||
loaded: false,
|
||||
});
|
||||
|
||||
const packages = computed(() => store.list || []);
|
||||
|
||||
const isLoggedIn = () => !!localStorage.getItem('Authorization');
|
||||
|
||||
const loadPackages = async () => {
|
||||
state.loading = true;
|
||||
state.error = false;
|
||||
let ok: boolean;
|
||||
if (props.orgId) {
|
||||
ok = await queryPublicPackages(props.orgId);
|
||||
} else {
|
||||
// Fallback: use auth'd API if logged in, otherwise skip
|
||||
ok = isLoggedIn() ? await queryOrgPackages() : false;
|
||||
if (!ok && !isLoggedIn()) {
|
||||
state.loading = false;
|
||||
state.error = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
state.error = !ok;
|
||||
state.loading = false;
|
||||
state.loaded = true;
|
||||
};
|
||||
|
||||
const onPreview = (pkg: CoursePackage) => {
|
||||
if (isLoggedIn()) {
|
||||
router.push(`/course/course-package/detail?id=${pkg.id}`);
|
||||
} else {
|
||||
showDialog({
|
||||
title: '提示',
|
||||
message: '请通过微信登录后查看完整内容',
|
||||
confirmButtonText: '知道了',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadPackages);
|
||||
|
||||
return { state, packages, loadPackages, onPreview };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.course-package-store {
|
||||
width: 100%;
|
||||
|
||||
.skeleton-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.24rem;
|
||||
padding: 0 0.24rem;
|
||||
|
||||
&.compact {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 0.24rem;
|
||||
}
|
||||
}
|
||||
|
||||
.package-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.24rem;
|
||||
padding: 0 0.24rem 0.24rem;
|
||||
|
||||
&.compact {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.package-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card-cover {
|
||||
width: 100%;
|
||||
|
||||
.cover-placeholder {
|
||||
width: 100%;
|
||||
height: 3.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f0f0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.2rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.28rem;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.22rem;
|
||||
color: #999;
|
||||
margin-top: 0.08rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.12rem;
|
||||
margin-top: 0.12rem;
|
||||
|
||||
.meta-item {
|
||||
font-size: 0.2rem;
|
||||
color: #888;
|
||||
background: #f5f5f5;
|
||||
padding: 0.04rem 0.12rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
margin-top: 0.16rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+3
-1
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
<van-empty v-else description="账号未启用或未通过注册审核" />
|
||||
</template>
|
||||
<van-empty v-if="!has_login" description="请返回微信小程序完成登录" />
|
||||
<course-package-store v-if="!has_login" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
RadioGroup,
|
||||
showConfirmDialog,
|
||||
} from "vant";
|
||||
import CoursePackageStore from "~/components/course-package-store.vue";
|
||||
import { CurrentUserRole } from "~/utils";
|
||||
import { sanitizeMiniProgramWebViewPathForRouter } from "~/utils/wxMiniProgramWebViewShare";
|
||||
|
||||
@@ -61,6 +62,7 @@ export default defineComponent({
|
||||
[Form.name!]: Form,
|
||||
[Popup.name!]: Popup,
|
||||
[Loading.name!]: Loading,
|
||||
[CoursePackageStore.name!]: CoursePackageStore,
|
||||
},
|
||||
props: ["params"],
|
||||
setup({ params }) {
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
</div>
|
||||
<div v-else class="org-homepage-md" v-html="homepageHtml" />
|
||||
</div>
|
||||
<div class="course-package-section" v-if="orgStore.currentOrgId">
|
||||
<div class="section-header">课包商店</div>
|
||||
<course-package-store :org-id="orgStore.currentOrgId" compact />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -18,10 +22,14 @@ import { computedAsync } from "@vueuse/core";
|
||||
import { computed, defineComponent, onActivated, onMounted, ref } from "vue";
|
||||
import useUser from "~/store/user";
|
||||
import useOrg from "~/store/org";
|
||||
import CoursePackageStore from "~/components/course-package-store.vue";
|
||||
import { renderOrgHomepageMarkdown } from "~/utils/orgHomepageMarkdown";
|
||||
|
||||
export default defineComponent({
|
||||
name: "student-home",
|
||||
components: {
|
||||
[CoursePackageStore.name!]: CoursePackageStore,
|
||||
},
|
||||
setup() {
|
||||
const usingUser = useUser();
|
||||
const { store: orgStore, getHomepage } = useOrg();
|
||||
@@ -140,5 +148,17 @@ export default defineComponent({
|
||||
padding-left: 0.4rem;
|
||||
margin: 0.16rem 0;
|
||||
}
|
||||
|
||||
.course-package-section {
|
||||
margin: 0 0.24rem 0.24rem;
|
||||
|
||||
.section-header {
|
||||
font-weight: 600;
|
||||
font-size: 0.32rem;
|
||||
color: #333;
|
||||
margin-bottom: 0.16rem;
|
||||
padding: 0.24rem 0 0.16rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
</div>
|
||||
<div v-else class="org-homepage-md" v-html="homepageHtml" />
|
||||
</div>
|
||||
<div class="course-package-section" v-if="orgStore.currentOrgId">
|
||||
<div class="section-header">课包商店</div>
|
||||
<course-package-store :org-id="orgStore.currentOrgId" compact />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -18,10 +22,14 @@ import { computedAsync } from "@vueuse/core";
|
||||
import { computed, defineComponent, onActivated, onMounted, ref } from "vue";
|
||||
import useUser from "~/store/user";
|
||||
import useOrg from "~/store/org";
|
||||
import CoursePackageStore from "~/components/course-package-store.vue";
|
||||
import { renderOrgHomepageMarkdown } from "~/utils/orgHomepageMarkdown";
|
||||
|
||||
export default defineComponent({
|
||||
name: "teacher-home",
|
||||
components: {
|
||||
[CoursePackageStore.name!]: CoursePackageStore,
|
||||
},
|
||||
setup() {
|
||||
const usingUser = useUser();
|
||||
const { store: orgStore, getHomepage } = useOrg();
|
||||
@@ -140,5 +148,17 @@ export default defineComponent({
|
||||
padding-left: 0.4rem;
|
||||
margin: 0.16rem 0;
|
||||
}
|
||||
|
||||
.course-package-section {
|
||||
margin: 0 0.24rem 0.24rem;
|
||||
|
||||
.section-header {
|
||||
font-weight: 600;
|
||||
font-size: 0.32rem;
|
||||
color: #333;
|
||||
margin-bottom: 0.16rem;
|
||||
padding: 0.24rem 0 0.16rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,6 +37,22 @@ export default function useCoursePackage() {
|
||||
return r;
|
||||
}
|
||||
|
||||
async function queryPublicPackages(orgId: string, keyword?: string, page = 1, pageSize = 20): Promise<boolean> {
|
||||
load_start();
|
||||
const params = new URLSearchParams({org_id: orgId, page: String(page), page_size: String(pageSize)});
|
||||
if (keyword) params.set('keyword', keyword);
|
||||
const {r, d, e} = await request({url: `/api/v1/clazz/course-package/public-packages?${params}`});
|
||||
load_done();
|
||||
if (r) {
|
||||
const [list, totalPage, totalCount] = d as [CoursePackage[], number, number];
|
||||
state.list = Array.isArray(list) ? list : [];
|
||||
state.total = totalPage;
|
||||
} else {
|
||||
showFailToast(e);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
async function queryOrgPackages(keyword?: string, page = 1, pageSize = 20): Promise<boolean> {
|
||||
load_start();
|
||||
const params = new URLSearchParams({page: String(page), page_size: String(pageSize)});
|
||||
@@ -178,6 +194,7 @@ export default function useCoursePackage() {
|
||||
store: state,
|
||||
queryMyPackages,
|
||||
queryOrgPackages,
|
||||
queryPublicPackages,
|
||||
findById,
|
||||
create,
|
||||
update,
|
||||
|
||||
Reference in New Issue
Block a user