Files
TeyvatGuide/src/pages/common/PostTopic.vue
2025-03-31 11:49:44 +08:00

394 lines
11 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>
<v-app-bar>
<template #prepend>
<div class="post-topic-top" v-if="topicInfo">
<TMiImg :src="topicInfo.topic.cover" alt="cover" :ori="true" />
<div class="post-topic-info">
<span>{{ topicInfo.topic.name }}({{ curTopic }})</span>
<span :title="topicInfo.topic.desc">{{ topicInfo.topic.desc }}</span>
</div>
</div>
</template>
<template #extension>
<TGameNav :model-value="curGid" v-if="curGid !== 0" style="margin-left: 8px" />
</template>
<div class="post-topic-switch">
<v-select
v-model="curGame"
class="post-switch-item"
:items="getGameList(topicInfo?.game_info_list)"
item-title="name"
:item-value="(item) => item"
variant="outlined"
label="分区"
:disabled="isReq"
>
<template #selection="{ item }">
<div class="select-item main">
<img
v-if="item.raw.icon"
:src="item.raw.icon"
:alt="item.raw.name"
:title="item.raw.name"
class="icon"
/>
<span>{{ item.raw.name }}</span>
</div>
</template>
<template #item="{ props, item }">
<div v-bind="props" class="select-item sub" :class="{ selected: item.raw.id === curGid }">
<img
v-if="item.raw.icon"
:src="item.raw.icon"
:alt="item.raw.name"
:title="item.raw.name"
class="icon"
/>
<span>{{ item.raw.name }}</span>
</div>
</template>
</v-select>
<v-select
v-model="curSortType"
class="post-switch-item"
:items="sortList"
item-title="text"
item-value="value"
variant="outlined"
label="排序"
:disabled="isReq"
/>
<v-text-field
v-model="search"
class="post-switch-item"
append-inner-icon="mdi-magnify"
label="请输入帖子 ID 或搜索词"
variant="outlined"
:single-line="true"
:hide-details="true"
@click:append="searchPost"
@keyup.enter="searchPost"
/>
<v-btn
:loading="isReq"
class="post-topic-btn"
@click="firstLoad()"
prepend-icon="mdi-refresh"
>
刷新
</v-btn>
</div>
</v-app-bar>
<div class="post-topic-grid">
<div v-for="post in posts" :key="post.post.post_id">
<TPostCard :model-value="post" :user-click="true" @onUserClick="handleUserClick" />
</div>
</div>
<div class="load-more">
<v-btn class="post-topic-btn" @click="freshPostData()">
已加载:{{ posts.length }}
{{ postRaw.isLast ? "已加载完毕" : "加载更多" }}
</v-btn>
</div>
<VpOverlaySearch :gid="curGid.toString()" v-model="showSearch" :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 { storeToRefs } from "pinia";
import { computed, onMounted, ref, shallowRef, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import useBBSStore from "@/store/modules/bbs.js";
import { createPost } from "@/utils/TGWindow.js";
import postReq from "@/web/request/postReq.js";
import topicReq from "@/web/request/topicReq.js";
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 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 { gameList } = storeToRefs(useBBSStore());
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 firstLoad();
});
watch(
() => curGame.value,
async () => {
if (curGame.value) curGid.value = curGame.value.id;
await firstLoad();
},
);
watch(
() => curSortType.value,
async () => await firstLoad(),
);
async function firstLoad(): 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 freshPostData(): 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("已经到底了");
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)) {
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="css" scoped>
.post-topic-top {
position: relative;
display: flex;
overflow: hidden;
align-items: center;
justify-content: center;
padding-right: 4px;
border-radius: 4px;
margin-left: 12px;
background: var(--box-bg-2);
gap: 4px;
img {
width: 64px;
height: 64px;
}
.post-topic-info {
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
:first-child {
color: var(--common-text-title);
font-family: var(--font-title);
font-size: 20px;
}
:last-child {
overflow: hidden;
height: 24px;
max-lines: 1;
text-overflow: ellipsis;
word-break: break-all;
}
}
}
.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);
}
.dark .post-topic-btn {
border: 1px solid var(--common-shadow-2);
}
.post-topic-grid {
display: grid;
font-family: var(--font-title);
grid-auto-rows: auto;
grid-gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
.load-more {
display: flex;
align-items: center;
justify-content: center;
margin: 10px;
font-family: var(--font-title);
transition: all 0.3s linear;
}
.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>