Files
VLoop/frontend/src/views/HotView.vue
2025-12-26 22:50:21 +08:00

158 lines
4.3 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.
<script setup lang="ts">
import { computed, onMounted, reactive } from 'vue'
import { ApiError } from '../api/client'
import * as feedApi from '../api/feed'
import * as likeApi from '../api/like'
import type { FeedVideoItem } from '../api/types'
import AppShell from '../components/AppShell.vue'
import FeedVideoCard from '../components/FeedVideoCard.vue'
import { useAuthStore } from '../stores/auth'
import { useToastStore } from '../stores/toast'
const auth = useAuthStore()
const toast = useToastStore()
const canLike = computed(() => auth.isLoggedIn)
const state = reactive({
loading: false,
error: '',
items: [] as FeedVideoItem[],
hasMore: false,
limit: 10,
asOf: 0,
nextOffset: 0,
})
const likeBusy = reactive<Record<string, boolean>>({})
async function loadHot(reset: boolean) {
if (state.loading) return
state.loading = true
state.error = ''
try {
const res = await feedApi.listByPopularity({
limit: state.limit,
as_of: reset ? 0 : state.asOf,
offset: reset ? 0 : state.nextOffset,
})
state.hasMore = res.has_more
state.asOf = res.as_of
state.nextOffset = res.next_offset
state.items = reset ? res.video_list : state.items.concat(res.video_list)
} catch (e) {
state.error = e instanceof ApiError ? e.message : String(e)
} finally {
state.loading = false
}
}
async function toggleLike(item: FeedVideoItem) {
if (!auth.isLoggedIn) {
toast.error('请先登录')
return
}
const key = String(item.id)
if (likeBusy[key]) return
likeBusy[key] = true
try {
if (item.is_liked) await likeApi.unlike(item.id)
else await likeApi.like(item.id)
item.is_liked = !item.is_liked
item.likes_count = Math.max(0, item.likes_count + (item.is_liked ? 1 : -1))
} catch (e) {
const msg = e instanceof ApiError ? e.message : String(e)
toast.error(msg)
} finally {
likeBusy[key] = false
}
}
onMounted(async () => {
await loadHot(true)
})
</script>
<template>
<AppShell>
<div class="card">
<div class="row" style="justify-content: space-between; align-items: baseline">
<div>
<p class="title" style="margin: 0">热榜</p>
<p class="subtle" style="margin: 6px 0 0">按热度排序/feed/listByPopularity</p>
</div>
<div class="row">
<label class="subtle" style="margin: 0">limit</label>
<input v-model.number="state.limit" type="number" min="1" max="50" style="width: 90px" :disabled="state.loading" />
<button class="primary" type="button" :disabled="state.loading" @click="loadHot(true)">刷新</button>
<button type="button" :disabled="state.loading || !state.hasMore" @click="loadHot(false)">加载更多</button>
</div>
</div>
<div v-if="state.error" class="pill bad" style="margin-top: 12px">错误{{ state.error }}</div>
<div v-else-if="state.loading && state.items.length === 0" class="subtle" style="margin-top: 12px">加载中</div>
<div v-else-if="state.items.length === 0" class="subtle" style="margin-top: 12px">暂无内容</div>
<div v-if="state.items.length" class="rank-list" style="margin-top: 14px">
<div v-for="(item, idx) in state.items" :key="`hot-${item.id}`" class="rank-row">
<div class="rank-num" :class="idx < 3 ? 'top' : ''">{{ idx + 1 }}</div>
<FeedVideoCard
:item="item"
:can-like="canLike"
:busy="!!likeBusy[String(item.id)]"
@toggle-like="toggleLike"
/>
</div>
</div>
</div>
</AppShell>
</template>
<style scoped>
.rank-list {
display: grid;
gap: 12px;
}
.rank-row {
display: grid;
grid-template-columns: 44px minmax(0, 1fr);
gap: 12px;
align-items: start;
}
.rank-num {
height: 44px;
width: 44px;
border-radius: 16px;
display: grid;
place-items: center;
font-weight: 900;
letter-spacing: 0.2px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.86);
user-select: none;
}
.rank-num.top {
border-color: rgba(254, 44, 85, 0.55);
background: rgba(254, 44, 85, 0.18);
color: rgba(255, 255, 255, 0.96);
}
@media (max-width: 900px) {
.rank-row {
grid-template-columns: 38px minmax(0, 1fr);
gap: 10px;
}
.rank-num {
height: 38px;
width: 38px;
border-radius: 14px;
}
}
</style>