mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2025-12-06 08:32:51 +08:00
445 lines
13 KiB
Vue
445 lines
13 KiB
Vue
<template>
|
||
<v-app-bar>
|
||
<template #prepend>
|
||
<div class="posts-top">
|
||
<img src="/source/UI/posts.webp" alt="posts" />
|
||
<span>帖子</span>
|
||
</div>
|
||
</template>
|
||
<div class="posts-switch">
|
||
<v-select
|
||
v-model="curGid"
|
||
class="post-switch-item"
|
||
:items="sortGameList"
|
||
item-title="text"
|
||
item-value="gid"
|
||
variant="outlined"
|
||
label="分区"
|
||
:disabled="isReq"
|
||
>
|
||
<template #selection="{ item }">
|
||
<div class="select-item main">
|
||
<TMiImg
|
||
v-if="item.raw.icon"
|
||
:src="item.raw.icon"
|
||
:ori="true"
|
||
:alt="item.raw.text"
|
||
:title="item.raw.text"
|
||
class="icon"
|
||
/>
|
||
<span>{{ item.raw.text }}</span>
|
||
</div>
|
||
</template>
|
||
<template #item="{ props, item }">
|
||
<div
|
||
v-bind="props"
|
||
class="select-item sub"
|
||
:class="{ selected: item.raw.gid === curGid }"
|
||
>
|
||
<TMiImg
|
||
v-if="item.raw.icon"
|
||
:src="item.raw.icon"
|
||
:alt="item.raw.text"
|
||
class="icon"
|
||
:ori="true"
|
||
/>
|
||
<span>{{ item.raw.text }}</span>
|
||
</div>
|
||
</template>
|
||
</v-select>
|
||
<v-select
|
||
v-model="selectedForum"
|
||
class="post-switch-item"
|
||
:items="curForums"
|
||
item-title="text"
|
||
variant="outlined"
|
||
label="版块"
|
||
:disabled="isReq"
|
||
>
|
||
<template #selection="{ item }">
|
||
<div class="select-item main">
|
||
<TMiImg
|
||
:src="item.raw.icon"
|
||
:alt="item.raw.text"
|
||
:ori="true"
|
||
:title="item.raw.text"
|
||
class="icon"
|
||
/>
|
||
<span>{{ item.raw.text }}</span>
|
||
</div>
|
||
</template>
|
||
<template #item="{ props, item }">
|
||
<div
|
||
v-bind="props"
|
||
class="select-item sub"
|
||
@click="selectedForum = item.raw"
|
||
:class="{ selected: item.raw.value === selectedForum?.value }"
|
||
>
|
||
<TMiImg :src="item.raw.icon" :alt="item.raw.text" :ori="true" class="icon" />
|
||
<span>{{ item.raw.text }}</span>
|
||
</div>
|
||
</template>
|
||
</v-select>
|
||
<v-select
|
||
v-model="curSortType"
|
||
class="post-switch-item"
|
||
:items="sortOrderList"
|
||
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 :rounded="true" class="post-forum-btn" @click="freshPostData()" :loading="isReq">
|
||
<v-icon>mdi-refresh</v-icon>
|
||
<span>刷新</span>
|
||
</v-btn>
|
||
</div>
|
||
<template #extension>
|
||
<TGameNav :model-value="curGid" />
|
||
</template>
|
||
</v-app-bar>
|
||
<div class="posts-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="posts-load-more">
|
||
<v-btn class="post-forum-btn" :rounded="true" @click="loadMore()">
|
||
已加载:{{ 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 setup lang="ts">
|
||
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 { computed, onMounted, ref, shallowRef, watch } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
|
||
import TGLogger from "@/utils/TGLogger.js";
|
||
import { createPost } from "@/utils/TGWindow.js";
|
||
import ApiHubReq from "@/web/request/apiHubReq.js";
|
||
import painterReq from "@/web/request/painterReq.js";
|
||
|
||
type SortSelect = { text: string; value: number; icon: string };
|
||
type SortSelectGame = { gid: number; forum: Array<SortSelect>; text: string; icon?: string };
|
||
type PostRaw = { isLast: boolean; lastId: string; total: number };
|
||
|
||
const sortOrderList: Array<Omit<SortSelect, "icon">> = [
|
||
{ text: "最新回复", value: 1 },
|
||
{ text: "最新发布", value: 2 },
|
||
{ text: "热门", value: 3 },
|
||
];
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const curGid = ref<number>(2);
|
||
const curSortType = ref<number>(1);
|
||
const firstLoad = ref<boolean>(false);
|
||
const isReq = ref<boolean>(false);
|
||
|
||
const search = ref<string>("");
|
||
const showSearch = ref<boolean>(false);
|
||
const curUid = ref<string>("");
|
||
const showUser = ref<boolean>(false);
|
||
|
||
const selectedForum = shallowRef<SortSelect>();
|
||
const sortGameList = shallowRef<Array<SortSelectGame>>([]);
|
||
const postRaw = shallowRef<PostRaw>({ isLast: false, lastId: "", total: 0 });
|
||
const posts = shallowRef<Array<TGApp.BBS.Post.FullData>>([]);
|
||
const curGame = computed<SortSelectGame | undefined>(() => {
|
||
return sortGameList.value.find((item) => item.gid === curGid.value);
|
||
});
|
||
const curForums = computed<Array<SortSelect>>(() => {
|
||
return curGame.value?.forum ?? [];
|
||
});
|
||
|
||
onMounted(async () => {
|
||
await showLoading.start("正在加载帖子数据");
|
||
await loadForums();
|
||
let { gid, forum } = route.query;
|
||
if (!gid) gid = route.params.gid;
|
||
if (!forum) forum = route.params.forum;
|
||
if (gid && typeof gid === "string") curGid.value = Number(gid);
|
||
if (forum && typeof forum === "string") {
|
||
selectedForum.value = getForum(Number(forum));
|
||
} else {
|
||
selectedForum.value = curForums.value[0];
|
||
}
|
||
const gameLabel = curGame.value?.text ?? "";
|
||
await TGLogger.Info(`[Posts][${gameLabel}][onMounted][${selectedForum.value.text}] 打开帖子列表`);
|
||
await freshPostData();
|
||
firstLoad.value = true;
|
||
});
|
||
watch(
|
||
() => curGid.value,
|
||
() => {
|
||
if (!selectedForum.value) return;
|
||
const forumFind = curForums.value.find((item) => item.text === selectedForum.value?.text);
|
||
if (!firstLoad.value) return;
|
||
selectedForum.value = forumFind ?? curForums.value[0];
|
||
showSnackbar.success(`已将分区切换到 ${curGame.value?.text}`);
|
||
},
|
||
);
|
||
watch(
|
||
() => selectedForum.value,
|
||
async () => {
|
||
if (!selectedForum.value) return;
|
||
await freshPostData();
|
||
showSnackbar.success(`已将版块切换到 ${selectedForum.value.text}`);
|
||
},
|
||
);
|
||
watch(
|
||
() => curSortType.value,
|
||
async () => {
|
||
await freshPostData();
|
||
const sortLabel = getSortLabel(curSortType.value);
|
||
showSnackbar.success(`已将排序切换到 ${sortLabel}`);
|
||
},
|
||
);
|
||
|
||
// 初始化
|
||
async function loadForums(): Promise<void> {
|
||
const allGames = await ApiHubReq.game();
|
||
const allForums = await ApiHubReq.forum();
|
||
const gameList: Array<SortSelectGame> = [];
|
||
for (const gameForum of allForums) {
|
||
const gameFind = allGames.find((i) => i.id === gameForum.game_id);
|
||
if (!gameFind) continue;
|
||
const gameItem: SortSelectGame = {
|
||
gid: gameForum.game_id,
|
||
icon: gameFind.app_icon,
|
||
forum: gameForum.forums
|
||
.sort((a, b) => a.order - b.order)
|
||
.map((i) => ({ text: i.name, value: i.id, icon: i.icon_pure })),
|
||
text: gameFind.name,
|
||
};
|
||
gameList.push(gameItem);
|
||
}
|
||
sortGameList.value = gameList;
|
||
}
|
||
|
||
function getForum(forum: number): SortSelect {
|
||
const forumItem = curForums.value.find((item) => item.value === forum);
|
||
return forumItem ? forumItem : curForums.value[0];
|
||
}
|
||
|
||
function getSortLabel(value: number): string {
|
||
const order = sortOrderList.find((item) => item.value === value);
|
||
return order ? order.text : "";
|
||
}
|
||
|
||
async function getCurrentPosts(
|
||
loadMore: boolean = false,
|
||
forum: number,
|
||
): Promise<TGApp.BBS.Forum.PostForumRes> {
|
||
const mod20 = postRaw.value.total % 20;
|
||
const pageSize = mod20 === 0 ? 20 : 20 - mod20;
|
||
if (curSortType.value === 3) {
|
||
if (loadMore) {
|
||
return await painterReq.forum.hot(forum, curGid.value, postRaw.value.lastId, pageSize);
|
||
}
|
||
return await painterReq.forum.hot(forum, curGid.value);
|
||
}
|
||
if (loadMore) {
|
||
return await painterReq.forum.recent(
|
||
forum,
|
||
curGid.value,
|
||
curSortType.value,
|
||
postRaw.value.lastId,
|
||
pageSize,
|
||
);
|
||
}
|
||
return await painterReq.forum.recent(forum, curGid.value, curSortType.value);
|
||
}
|
||
|
||
async function freshPostData(): Promise<void> {
|
||
if (!selectedForum.value || isReq.value) return;
|
||
await router.push({
|
||
name: "酒馆",
|
||
params: route.params,
|
||
query: { gid: curGid.value, forum: selectedForum.value.value },
|
||
});
|
||
isReq.value = true;
|
||
if (showSearch.value) showSearch.value = false;
|
||
if (showUser.value) showUser.value = false;
|
||
const gameLabel = curGame.value?.text ?? "";
|
||
await showLoading.start(`正在刷新${gameLabel}帖子`);
|
||
const forumLabel = getForum(selectedForum.value.value).text;
|
||
const sortLabel = getSortLabel(curSortType.value);
|
||
await TGLogger.Info(
|
||
`[Posts][${gameLabel}][freshPostData][${forumLabel}][${sortLabel}] 刷新帖子列表`,
|
||
);
|
||
await showLoading.update(`版块:${forumLabel},排序:${sortLabel}`);
|
||
document.documentElement.scrollTo({ top: 0, behavior: "smooth" });
|
||
const postsGet = await getCurrentPosts(false, selectedForum.value.value);
|
||
posts.value = postsGet.list;
|
||
postRaw.value = {
|
||
isLast: postsGet.is_last,
|
||
lastId: postsGet.last_id,
|
||
total: postsGet.list.length,
|
||
};
|
||
showSnackbar.success(`刷新成功,共加载 ${postsGet.list.length} 条帖子`);
|
||
await showLoading.end();
|
||
isReq.value = false;
|
||
}
|
||
|
||
async function loadMore(): Promise<void> {
|
||
if (!selectedForum.value) {
|
||
showSnackbar.warn("请先选择一个版块");
|
||
return;
|
||
}
|
||
if (postRaw.value.isLast) {
|
||
showSnackbar.warn("没有更多帖子了");
|
||
return;
|
||
}
|
||
if (isReq.value) return;
|
||
isReq.value = true;
|
||
await showLoading.start("正在加载更多帖子数据", `游戏:${curGame.value?.text}`);
|
||
const postsGet = await getCurrentPosts(true, selectedForum.value.value);
|
||
await showLoading.update(
|
||
`版块:${selectedForum.value.text},排序:${getSortLabel(curSortType.value)},数量:${postsGet.list.length}`,
|
||
);
|
||
posts.value = posts.value.concat(postsGet.list);
|
||
postRaw.value = {
|
||
isLast: postsGet.is_last,
|
||
lastId: postsGet.last_id,
|
||
total: postRaw.value.total + postsGet.list.length,
|
||
};
|
||
showSnackbar.success(`加载成功,共加载 ${postsGet.list.length} 条帖子`);
|
||
await showLoading.end();
|
||
isReq.value = false;
|
||
}
|
||
|
||
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 handleUserClick(user: TGApp.BBS.Post.User): void {
|
||
if (showSearch.value) showSearch.value = false;
|
||
curUid.value = user.uid;
|
||
showUser.value = true;
|
||
}
|
||
</script>
|
||
<style lang="css" scoped>
|
||
.posts-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
|
||
img {
|
||
width: 32px;
|
||
height: 32px;
|
||
}
|
||
|
||
span {
|
||
color: var(--common-text-title);
|
||
font-family: var(--font-title);
|
||
font-size: 20px;
|
||
}
|
||
}
|
||
|
||
.posts-switch {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
margin: 0 10px;
|
||
gap: 10px;
|
||
}
|
||
|
||
.post-switch-item {
|
||
width: 250px;
|
||
height: 50px;
|
||
}
|
||
|
||
.post-forum-btn {
|
||
height: 40px;
|
||
background: var(--tgc-btn-1);
|
||
color: var(--btn-text);
|
||
font-family: var(--font-title);
|
||
}
|
||
|
||
.dark .post-forum-btn {
|
||
border: 1px solid var(--common-shadow-2);
|
||
}
|
||
|
||
.posts-grid {
|
||
display: grid;
|
||
font-family: var(--font-title);
|
||
grid-auto-rows: auto;
|
||
grid-gap: 8px;
|
||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||
}
|
||
|
||
.posts-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;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
</style>
|