mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-03-29 06:09:45 +08:00
434 lines
11 KiB
Vue
434 lines
11 KiB
Vue
<!-- 话题页面 -->
|
||
<template>
|
||
<v-app-bar>
|
||
<template #prepend>
|
||
<div v-if="topicInfo" :class="sidebar.collapse ? 'wide' : 'thin'" class="post-topic-top">
|
||
<TMiImg :ori="true" :src="topicInfo.topic.cover" alt="cover" />
|
||
<div class="post-topic-info">
|
||
<span class="post-topic-title">{{ topicInfo.topic.name }}</span>
|
||
<span :title="topicInfo.topic.desc" class="post-topic-desc">
|
||
{{ topicInfo.topic.desc }}
|
||
</span>
|
||
</div>
|
||
<div :title="`话题ID:${topicInfo.topic.id}`" class="post-topic-id">
|
||
{{ topicInfo.topic.id }}
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template #extension>
|
||
<TGameNav v-if="curGid !== 0" :gid="curGid" :mini="false" style="margin-left: 8px" />
|
||
</template>
|
||
<div class="post-topic-switch">
|
||
<v-select
|
||
v-model="curGame"
|
||
:disabled="isReq"
|
||
:item-value="(item) => item"
|
||
:items="getGameList(topicInfo?.game_info_list)"
|
||
class="post-switch-item"
|
||
item-title="name"
|
||
label="分区"
|
||
variant="outlined"
|
||
>
|
||
<template #selection="{ item }">
|
||
<div class="select-item main">
|
||
<img
|
||
v-if="item.icon"
|
||
:alt="item.name"
|
||
:src="item.icon"
|
||
:title="item.name"
|
||
class="icon"
|
||
/>
|
||
<span>{{ item.name }}</span>
|
||
</div>
|
||
</template>
|
||
<template #item="{ props, item }">
|
||
<div :class="{ selected: item.id === curGid }" class="select-item sub" v-bind="props">
|
||
<img
|
||
v-if="item.icon"
|
||
:alt="item.name"
|
||
:src="item.icon"
|
||
:title="item.name"
|
||
class="icon"
|
||
/>
|
||
<span>{{ item.name }}</span>
|
||
</div>
|
||
</template>
|
||
</v-select>
|
||
<v-select
|
||
v-model="curSortType"
|
||
:disabled="isReq"
|
||
:items="sortList"
|
||
class="post-switch-item"
|
||
item-title="text"
|
||
item-value="value"
|
||
label="排序"
|
||
variant="outlined"
|
||
/>
|
||
<v-text-field
|
||
v-model="search"
|
||
:hide-details="true"
|
||
:clearable="true"
|
||
append-inner-icon="mdi-magnify"
|
||
class="post-switch-item"
|
||
label="请输入帖子 ID 或搜索词"
|
||
variant="outlined"
|
||
@click:append-inner="searchPost"
|
||
@keyup.enter="searchPost"
|
||
/>
|
||
<v-btn
|
||
:loading="isReq"
|
||
class="post-topic-btn"
|
||
prepend-icon="mdi-refresh"
|
||
rounded
|
||
variant="elevated"
|
||
@click="freshPostData()"
|
||
>
|
||
刷新
|
||
</v-btn>
|
||
</div>
|
||
</v-app-bar>
|
||
<div class="post-topic-grid">
|
||
<div v-for="post in posts" :key="post.post.post_id">
|
||
<TPostCard :post @onUserClick="handleUserClick" />
|
||
</div>
|
||
</div>
|
||
<VpOverlaySearch v-model="showSearch" :gid="curGid" :keyword="search" />
|
||
<VpOverlayUser v-model="showUser" :gid="curGid" :uid="curUid" />
|
||
</template>
|
||
<script lang="ts" setup>
|
||
import TGameNav from "@comp/app/t-gameNav.vue";
|
||
import TMiImg from "@comp/app/t-mi-img.vue";
|
||
import TPostCard from "@comp/app/t-postcard.vue";
|
||
import showLoading from "@comp/func/loading.js";
|
||
import showSnackbar from "@comp/func/snackbar.js";
|
||
import VpOverlaySearch from "@comp/viewPost/vp-overlay-search.vue";
|
||
import VpOverlayUser from "@comp/viewPost/vp-overlay-user.vue";
|
||
import { usePageReachBottom } from "@hooks/reachBottom.js";
|
||
import postReq from "@req/postReq.js";
|
||
import topicReq from "@req/topicReq.js";
|
||
import useAppStore from "@store/app.js";
|
||
import useBBSStore from "@store/bbs.js";
|
||
import { createPost } from "@utils/TGWindow.js";
|
||
import { storeToRefs } from "pinia";
|
||
import { computed, onMounted, ref, shallowRef, watch } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
|
||
type SortSelect = { text: string; value: number };
|
||
type PostMiniData = { isLast: boolean; lastId: string; total: number };
|
||
type GameList = TGApp.BBS.Topic.GameInfo & { icon?: string };
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
|
||
const { isReachBottom } = usePageReachBottom();
|
||
const { sidebar } = storeToRefs(useAppStore());
|
||
const { gameList } = storeToRefs(useBBSStore());
|
||
|
||
const curGid = ref<number>(0);
|
||
const curSortType = ref<0 | 1 | 2>(0);
|
||
const curTopic = ref<string>("");
|
||
|
||
const search = ref<string>("");
|
||
const showSearch = ref<boolean>(false);
|
||
const curUid = ref<string>("");
|
||
const showUser = ref<boolean>(false);
|
||
|
||
const isReq = ref<boolean>(false);
|
||
const firstLoad = ref<boolean>(false);
|
||
const postRaw = shallowRef<PostMiniData>({ isLast: false, lastId: "", total: 0 });
|
||
const topicInfo = shallowRef<TGApp.BBS.Topic.InfoRes>();
|
||
const posts = shallowRef<Array<TGApp.BBS.Post.FullData>>([]);
|
||
const curGame = shallowRef<GameList>();
|
||
const sortList = computed<Array<SortSelect>>(() => {
|
||
if (!topicInfo.value) return [];
|
||
if (!topicInfo.value.good_post_exist) {
|
||
return [
|
||
{ text: "最新", value: 0 },
|
||
{ text: "热门", value: 2 },
|
||
];
|
||
}
|
||
return [
|
||
{ text: "最新", value: 0 },
|
||
{ text: "热门", value: 2 },
|
||
{ text: "精选", value: 1 },
|
||
];
|
||
});
|
||
|
||
onMounted(async () => {
|
||
let { gid, topic } = route.query;
|
||
if (!gid) gid = route.params.gid;
|
||
if (!topic) topic = route.params.topic;
|
||
if (!gid || typeof gid !== "string") gid = "0";
|
||
if (!topic || typeof topic !== "string") topic = "0";
|
||
curGid.value = Number(gid);
|
||
curTopic.value = topic;
|
||
await showLoading.start(`正在加载话题${topic}信息`);
|
||
const info = await topicReq.info(gid, topic);
|
||
if ("retcode" in info) {
|
||
await showLoading.end();
|
||
showSnackbar.error(`[${info.retcode}] ${info.message}`);
|
||
return;
|
||
}
|
||
topicInfo.value = info;
|
||
let tmpGame: GameList | undefined;
|
||
if (curGame.value === undefined) {
|
||
tmpGame = info.game_info_list.find((i) => i.id === curGid.value);
|
||
}
|
||
if (tmpGame === undefined) tmpGame = info.game_info_list[0];
|
||
const gameFind = gameList.value.find((i) => i.id === tmpGame?.id);
|
||
curGame.value = { ...tmpGame, icon: gameFind?.app_icon };
|
||
await freshPostData();
|
||
firstLoad.value = true;
|
||
});
|
||
|
||
watch(
|
||
() => isReachBottom.value,
|
||
async () => {
|
||
if (!isReachBottom.value || !firstLoad.value) return;
|
||
await loadMore();
|
||
},
|
||
);
|
||
watch(
|
||
() => curGame.value,
|
||
async () => {
|
||
if (curGame.value) curGid.value = curGame.value.id;
|
||
await freshPostData();
|
||
},
|
||
);
|
||
watch(
|
||
() => curSortType.value,
|
||
async () => await freshPostData(),
|
||
);
|
||
|
||
async function freshPostData(): Promise<void> {
|
||
if (isReq.value) return;
|
||
isReq.value = true;
|
||
await showLoading.start(`正在加载话题${topicInfo.value?.topic.name}信息`);
|
||
await router.push({
|
||
name: "话题",
|
||
params: route.params,
|
||
query: { gid: curGid.value, topic: curTopic.value },
|
||
});
|
||
document.documentElement.scrollTo({ top: 0, behavior: "smooth" });
|
||
const postList = await postReq.topic(curGid.value, curTopic.value, curSortType.value);
|
||
if ("retcode" in postList) {
|
||
await showLoading.end();
|
||
showSnackbar.error(`[${postList.retcode}] ${postList.message}`);
|
||
return;
|
||
}
|
||
await showLoading.update(`数量:${postList.posts.length},是否最后一页:${postList.is_last}`);
|
||
postRaw.value = {
|
||
isLast: postList.is_last,
|
||
lastId: postList.last_id,
|
||
total: postList.posts.length,
|
||
};
|
||
posts.value = postList.posts;
|
||
await showLoading.end();
|
||
isReq.value = false;
|
||
showSnackbar.success(`加载了 ${postList.posts.length} 条帖子`);
|
||
}
|
||
|
||
async function loadMore(): Promise<void> {
|
||
if (isReq.value) return;
|
||
isReq.value = true;
|
||
if (showSearch.value) showSearch.value = false;
|
||
if (showUser.value) showUser.value = false;
|
||
if (postRaw.value.isLast) {
|
||
showSnackbar.warn("已经到底了");
|
||
isReq.value = false;
|
||
return;
|
||
}
|
||
await showLoading.start(`正在刷新${topicInfo.value?.topic.name}帖子列表`);
|
||
const mod20 = postRaw.value.total % 20;
|
||
const pageSize = mod20 === 0 ? 20 : 20 - mod20;
|
||
const resp = await postReq.topic(
|
||
curGid.value,
|
||
curTopic.value,
|
||
curSortType.value,
|
||
postRaw.value.lastId,
|
||
pageSize,
|
||
);
|
||
if ("retcode" in resp) {
|
||
await showLoading.end();
|
||
isReq.value = false;
|
||
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
|
||
return;
|
||
}
|
||
await showLoading.update(`数量:${resp.posts.length},是否最后一页:${resp.is_last}`);
|
||
postRaw.value = {
|
||
isLast: resp.is_last,
|
||
lastId: resp.last_id,
|
||
total: postRaw.value.total + resp.posts.length,
|
||
};
|
||
posts.value = posts.value.concat(resp.posts);
|
||
await showLoading.end();
|
||
isReq.value = false;
|
||
showSnackbar.success(`加载了 ${resp.posts.length} 条帖子`);
|
||
}
|
||
|
||
function searchPost(): void {
|
||
if (search.value === "") {
|
||
showSnackbar.warn("请输入搜索内容");
|
||
return;
|
||
}
|
||
const numCheck = Number(search.value);
|
||
if (isNaN(numCheck) || numCheck % 1 !== 0) {
|
||
if (showUser.value) showUser.value = false;
|
||
showSearch.value = true;
|
||
} else createPost(search.value);
|
||
}
|
||
|
||
function getGameList(list: Array<TGApp.BBS.Topic.GameInfo> | undefined): Array<GameList> {
|
||
if (!list) return [];
|
||
return list.map((item) => {
|
||
const game = gameList.value.find((i) => i.id === item.id);
|
||
return { ...item, icon: game?.app_icon };
|
||
});
|
||
}
|
||
|
||
function handleUserClick(user: TGApp.BBS.Post.User, gid: number): void {
|
||
if (showSearch.value) showSearch.value = false;
|
||
curGid.value = gid;
|
||
curUid.value = user.uid;
|
||
showUser.value = true;
|
||
}
|
||
</script>
|
||
<style lang="scss" scoped>
|
||
@use "@styles/github.styles.scss" as github-styles;
|
||
|
||
.post-topic-top {
|
||
@include github-styles.github-card;
|
||
|
||
position: relative;
|
||
display: flex;
|
||
overflow: hidden;
|
||
max-width: 100%;
|
||
height: 48px;
|
||
box-sizing: border-box;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
padding-right: 8px;
|
||
border-radius: 4px;
|
||
margin-right: 12px;
|
||
margin-left: 12px;
|
||
gap: 4px;
|
||
|
||
img {
|
||
height: 100%;
|
||
flex-shrink: 0;
|
||
aspect-ratio: 1;
|
||
}
|
||
|
||
&.wide {
|
||
max-width: 600px;
|
||
transition: max-width 0.5s ease-in-out;
|
||
}
|
||
|
||
&.thin {
|
||
max-width: 400px;
|
||
}
|
||
}
|
||
|
||
.dark .post-topic-top {
|
||
@include github-styles.github-card("dark");
|
||
}
|
||
|
||
.post-topic-info {
|
||
position: relative;
|
||
display: flex;
|
||
overflow: hidden;
|
||
max-width: 100%;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
}
|
||
|
||
.post-topic-title {
|
||
color: var(--common-text-title);
|
||
font-family: var(--font-title);
|
||
font-size: 18px;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.post-topic-desc {
|
||
overflow: hidden;
|
||
max-width: 100%;
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
max-lines: 1;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.post-topic-id {
|
||
position: absolute;
|
||
z-index: 1;
|
||
top: 2px;
|
||
right: 2px;
|
||
color: var(--tgc-od-red);
|
||
font-size: 8px;
|
||
}
|
||
|
||
.post-topic-switch {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
margin-right: 16px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.post-switch-item {
|
||
width: 250px;
|
||
height: 50px;
|
||
}
|
||
|
||
.post-topic-btn {
|
||
height: 40px;
|
||
background: var(--tgc-btn-1);
|
||
color: var(--btn-text);
|
||
font-family: var(--font-title);
|
||
}
|
||
|
||
.post-topic-grid {
|
||
display: grid;
|
||
font-family: var(--font-title);
|
||
gap: 8px;
|
||
grid-auto-rows: auto;
|
||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||
}
|
||
|
||
.select-item {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
column-gap: 4px;
|
||
|
||
&.main {
|
||
position: relative;
|
||
height: 24px;
|
||
font-family: var(--font-title);
|
||
font-size: 16px;
|
||
}
|
||
|
||
&.sub {
|
||
padding: 8px;
|
||
font-family: var(--font-title);
|
||
font-size: 16px;
|
||
|
||
&:hover {
|
||
background: var(--common-shadow-2);
|
||
}
|
||
|
||
&.selected:not(:hover) {
|
||
background: var(--common-shadow-1);
|
||
}
|
||
}
|
||
|
||
.icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
}
|
||
}
|
||
</style>
|