feat: expand portal entry flows and dynamic recommendation routing
This commit is contained in:
@@ -1,14 +1,25 @@
|
||||
<script setup>
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { tenantApi } from "../../api/tenant";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { contentApi } from "../../api/content";
|
||||
import { tenantApi } from "../../api/tenant";
|
||||
import { tenantPath } from "../../utils/tenant";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const toast = useToast();
|
||||
|
||||
const tenantID = ref(0);
|
||||
|
||||
const resolveTenantIDFromRoute = () => {
|
||||
const raw = route.params.id;
|
||||
const parsed = Number(raw);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
||||
};
|
||||
|
||||
const showFollowActions = computed(() => tenantID.value > 0);
|
||||
const currentTab = ref("home");
|
||||
const isFollowing = ref(false);
|
||||
const tenant = ref({});
|
||||
@@ -25,46 +36,57 @@ const hasMore = ref(false);
|
||||
const sortOption = ref("latest");
|
||||
const selectedTopic = ref("");
|
||||
const limit = 10;
|
||||
const querySyncReady = ref(false);
|
||||
|
||||
const normalizeGenreQuery = (value) => {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
return typeof value[0] === "string" ? value[0] : "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const fetchData = async (isLoadMore = false) => {
|
||||
if (!isLoadMore) loading.value = true;
|
||||
try {
|
||||
const id = route.params.id;
|
||||
const effectiveTenantID = tenantID.value || resolveTenantIDFromRoute();
|
||||
const query = {
|
||||
tenant_id: id,
|
||||
sort: sortOption.value,
|
||||
page: page.value,
|
||||
limit: limit,
|
||||
keyword: searchKeyword.value,
|
||||
};
|
||||
if (effectiveTenantID > 0) {
|
||||
query.tenant_id = effectiveTenantID;
|
||||
}
|
||||
if (selectedTopic.value) {
|
||||
query.genre = selectedTopic.value;
|
||||
}
|
||||
|
||||
const reqs = [contentApi.list(query)];
|
||||
|
||||
// Only fetch tenant info & featured on first load
|
||||
// Only fetch featured on first load
|
||||
if (!isLoadMore && page.value === 1) {
|
||||
reqs.push(tenantApi.get(id));
|
||||
reqs.push(
|
||||
contentApi.list({
|
||||
tenant_id: id,
|
||||
is_pinned: true,
|
||||
genre: selectedTopic.value || "",
|
||||
}),
|
||||
);
|
||||
const pinnedQuery = {
|
||||
is_pinned: true,
|
||||
genre: selectedTopic.value || "",
|
||||
};
|
||||
if (effectiveTenantID > 0) {
|
||||
pinnedQuery.tenant_id = effectiveTenantID;
|
||||
}
|
||||
|
||||
reqs.push(contentApi.list(pinnedQuery));
|
||||
}
|
||||
|
||||
const results = await Promise.all(reqs);
|
||||
const c = results[0]; // Content List
|
||||
|
||||
if (!isLoadMore && page.value === 1) {
|
||||
const t = results[1];
|
||||
const f = results[2];
|
||||
tenant.value = t || {};
|
||||
isFollowing.value = t?.is_following || false;
|
||||
const f = results[1];
|
||||
|
||||
if (f && f.items && f.items.length > 0) {
|
||||
if (f?.items?.length > 0) {
|
||||
featuredContent.value = f.items[0];
|
||||
} else {
|
||||
featuredContent.value = null;
|
||||
@@ -92,12 +114,34 @@ const fetchData = async (isLoadMore = false) => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => fetchData());
|
||||
onMounted(async () => {
|
||||
selectedTopic.value = normalizeGenreQuery(route.query.genre);
|
||||
querySyncReady.value = true;
|
||||
|
||||
try {
|
||||
topics.value = (await contentApi.listTopics()) || [];
|
||||
const topicsRes = await contentApi.listTopics();
|
||||
|
||||
topics.value = topicsRes || [];
|
||||
|
||||
tenantID.value = resolveTenantIDFromRoute();
|
||||
tenant.value = {
|
||||
id: tenantID.value,
|
||||
name: route.params.tenantCode,
|
||||
bio: "",
|
||||
cover: "",
|
||||
avatar: "",
|
||||
stats: {
|
||||
followers: 0,
|
||||
contents: 0,
|
||||
likes: 0,
|
||||
},
|
||||
};
|
||||
isFollowing.value = false;
|
||||
|
||||
fetchData();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
fetchData();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -119,13 +163,25 @@ const loadMore = () => {
|
||||
|
||||
const toggleFollow = async () => {
|
||||
if (followLoading.value) return;
|
||||
|
||||
const effectiveTenantID = tenantID.value || resolveTenantIDFromRoute();
|
||||
if (!effectiveTenantID) {
|
||||
toast.add({
|
||||
severity: "warn",
|
||||
summary: "暂不可用",
|
||||
detail: "当前频道暂不支持关注操作",
|
||||
life: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
followLoading.value = true;
|
||||
try {
|
||||
if (isFollowing.value) {
|
||||
await tenantApi.unfollow(route.params.id);
|
||||
await tenantApi.unfollow(effectiveTenantID);
|
||||
isFollowing.value = false;
|
||||
} else {
|
||||
await tenantApi.follow(route.params.id);
|
||||
await tenantApi.follow(effectiveTenantID);
|
||||
isFollowing.value = true;
|
||||
toast.add({
|
||||
severity: "success",
|
||||
@@ -156,9 +212,43 @@ const sortLabel = computed(() =>
|
||||
sortOption.value === "hot" ? "最热" : "最新",
|
||||
);
|
||||
|
||||
watch([sortOption, selectedTopic], () => {
|
||||
applyFilters();
|
||||
const currentTopicBreadcrumb = computed(() => {
|
||||
const fromQuery = normalizeGenreQuery(route.query.genre);
|
||||
if (fromQuery) return fromQuery;
|
||||
if (selectedTopic.value) return selectedTopic.value;
|
||||
return "全部专题";
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query.genre,
|
||||
(newGenre) => {
|
||||
const normalized = normalizeGenreQuery(newGenre);
|
||||
if (normalized !== selectedTopic.value) {
|
||||
selectedTopic.value = normalized;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
[sortOption, selectedTopic],
|
||||
([nextSort, nextTopic], [prevSort, prevTopic]) => {
|
||||
if (!querySyncReady.value) return;
|
||||
|
||||
if (nextTopic !== prevTopic) {
|
||||
const nextQuery = { ...route.query };
|
||||
if (nextTopic) {
|
||||
nextQuery.genre = nextTopic;
|
||||
} else {
|
||||
delete nextQuery.genre;
|
||||
}
|
||||
router.replace({ query: nextQuery });
|
||||
}
|
||||
|
||||
if (nextSort !== prevSort || nextTopic !== prevTopic) {
|
||||
applyFilters();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -229,6 +319,7 @@ watch([sortOption, selectedTopic], () => {
|
||||
<div class="flex flex-col items-end gap-5 pb-2">
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
v-if="showFollowActions"
|
||||
@click="toggleFollow"
|
||||
:disabled="followLoading"
|
||||
class="h-11 w-32 rounded-full font-bold text-base transition-all flex items-center justify-center gap-2 backdrop-blur-md"
|
||||
@@ -310,7 +401,16 @@ watch([sortOption, selectedTopic], () => {
|
||||
<div
|
||||
class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 flex items-center justify-between h-14"
|
||||
>
|
||||
<div class="flex gap-8 h-full">
|
||||
<div class="flex items-center gap-4 h-full">
|
||||
<div
|
||||
class="hidden md:flex items-center text-xs text-slate-500 bg-slate-100 rounded-full px-3 py-1"
|
||||
>
|
||||
<span>频道</span>
|
||||
<i class="pi pi-angle-right text-[10px] mx-1.5"></i>
|
||||
<span class="font-semibold text-slate-700">{{
|
||||
currentTopicBreadcrumb
|
||||
}}</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
@@ -324,6 +424,12 @@ watch([sortOption, selectedTopic], () => {
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
<router-link
|
||||
:to="tenantRoute('/topics')"
|
||||
class="text-xs px-3 py-1.5 rounded-full border border-slate-200 text-slate-600 hover:border-primary-200 hover:text-primary-600 transition-colors"
|
||||
>
|
||||
去专题
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- In-Tenant Search -->
|
||||
|
||||
Reference in New Issue
Block a user