refactor(project): Reorganize frontend and backend directories

This commit is contained in:
2026-03-18 10:45:15 +08:00
parent cc9eaf4cdf
commit 92a139e470
513 changed files with 28251 additions and 287 deletions
@@ -0,0 +1,114 @@
<template>
<div class="follow-page">
<van-nav-bar title="粉丝" fixed placeholder left-arrow @click-left="router.back()" />
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="loadMore"
>
<van-cell-group inset>
<van-cell v-for="user in users" :key="user.id" @click="goToProfile(user.id)">
<template #icon>
<van-image :src="user.avatar || defaultAvatar" round width="40" height="40" />
</template>
<template #title>
<span class="nickname">{{ user.nickname }}</span>
</template>
<template #label>
<span class="signature">{{ user.signature || '这个人很懒' }}</span>
</template>
<template #value>
<van-button size="small" :type="user.isFollowed ? 'default' : 'primary'" @click.stop="toggleFollow(user)">
{{ user.isFollowed ? '已关注' : '关注' }}
</van-button>
</template>
</van-cell>
</van-cell-group>
<van-empty v-if="!loading && !users.length" description="暂无粉丝" />
</van-list>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast, showSuccessToast } from 'vant'
import { userApi, type UserFollow } from '@/api/user.ts'
import { useUserStore } from '@/stores/user.ts'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const userId = Number(route.params.id)
const users = ref<UserFollow[]>([])
const loading = ref(false)
const finished = ref(false)
const currentPage = ref(1)
const defaultAvatar = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Ccircle cx="50" cy="50" r="50" fill="%23e0e0e0"/%3E%3Ctext x="50%25" y="50%25" fill="%23999" text-anchor="middle" dy=".3em"%3EU%3C/text%3E%3C/svg%3E'
const loadUsers = async () => {
loading.value = true
try {
const data = await userApi.getFollowers(userId)
users.value = data
finished.value = true
} catch (error) {
showToast('加载失败')
} finally {
loading.value = false
}
}
const loadMore = () => {
if (!finished.value) loadUsers()
}
const toggleFollow = async (user: UserFollow) => {
if (!userStore.isLoggedIn) {
showToast('请先登录')
return
}
try {
if (user.isFollowed) {
await userApi.unfollow(user.id)
user.isFollowed = false
showSuccessToast('已取消关注')
} else {
await userApi.follow(user.id)
user.isFollowed = true
showSuccessToast('关注成功')
}
} catch (error) {
showToast('操作失败')
}
}
const goToProfile = (id: number) => {
router.push(`/user/${id}`)
}
onMounted(() => {
loadUsers()
})
</script>
<style scoped lang="scss">
.follow-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 60px;
.nickname {
font-weight: 500;
}
.signature {
font-size: 12px;
color: #999;
}
}
</style>
@@ -0,0 +1,101 @@
<template>
<div class="follow-page">
<van-nav-bar title="关注" fixed placeholder left-arrow @click-left="router.back()" />
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="loadMore"
>
<van-cell-group inset>
<van-cell v-for="user in users" :key="user.id" @click="goToProfile(user.id)">
<template #icon>
<van-image :src="user.avatar || defaultAvatar" round width="40" height="40" />
</template>
<template #title>
<span class="nickname">{{ user.nickname }}</span>
</template>
<template #label>
<span class="signature">{{ user.signature || '这个人很懒' }}</span>
</template>
<template #value>
<van-button size="small" type="default" @click.stop="unfollow(user)">
已关注
</van-button>
</template>
</van-cell>
</van-cell-group>
<van-empty v-if="!loading && !users.length" description="暂无关注" />
</van-list>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast, showSuccessToast } from 'vant'
import { userApi, type UserFollow } from '@/api/user.ts'
const router = useRouter()
const route = useRoute()
const userId = Number(route.params.id)
const users = ref<UserFollow[]>([])
const loading = ref(false)
const finished = ref(false)
const defaultAvatar = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Ccircle cx="50" cy="50" r="50" fill="%23e0e0e0"/%3E%3Ctext x="50%25" y="50%25" fill="%23999" text-anchor="middle" dy=".3em"%3EU%3C/text%3E%3C/svg%3E'
const loadUsers = async () => {
loading.value = true
try {
const data = await userApi.getFollowing(userId)
users.value = data
finished.value = true
} catch (error) {
showToast('加载失败')
} finally {
loading.value = false
}
}
const loadMore = () => {
if (!finished.value) loadUsers()
}
const unfollow = async (user: UserFollow) => {
try {
await userApi.unfollow(user.id)
users.value = users.value.filter((u) => u.id !== user.id)
showSuccessToast('已取消关注')
} catch (error) {
showToast('操作失败')
}
}
const goToProfile = (id: number) => {
router.push(`/user/${id}`)
}
onMounted(() => {
loadUsers()
})
</script>
<style scoped lang="scss">
.follow-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 60px;
.nickname {
font-weight: 500;
}
.signature {
font-size: 12px;
color: #999;
}
}
</style>
@@ -0,0 +1,117 @@
<template>
<div class="history-page">
<van-nav-bar title="播放历史" fixed placeholder left-arrow @click-left="router.back()">
<template #right>
<van-icon name="delete-o" size="22" @click="clearHistory" />
</template>
</van-nav-bar>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="loadMore"
>
<van-cell-group v-if="videos.length" inset>
<van-cell v-for="item in videos" :key="item.videoId" @click="goToVideo(item.videoId)">
<template #icon>
<van-image :src="item.coverUrl" width="80" height="45" fit="cover" radius="4" />
</template>
<template #title>
<div class="video-title">{{ item.title }}</div>
</template>
<template #label>
<div class="video-progress">
观看至 {{ formatDuration(item.progress) }} / {{ formatDuration(item.duration) }}
</div>
</template>
</van-cell>
</van-cell-group>
<van-empty v-else description="暂无播放记录" />
</van-list>
</van-pull-refresh>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showConfirmDialog } from 'vant'
import { formatDuration } from '@/utils'
interface HistoryItem {
videoId: number
title: string
coverUrl: string
progress: number
duration: number
}
const router = useRouter()
const videos = ref<HistoryItem[]>([])
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const loadVideos = async (isRefresh = false) => {
if (isRefresh) {
videos.value = []
finished.value = false
}
loading.value = true
// TODO: 实际从API获取播放历史
finished.value = true
loading.value = false
refreshing.value = false
}
const loadMore = () => {
if (!finished.value) loadVideos()
}
const onRefresh = () => {
loadVideos(true)
}
const clearHistory = async () => {
try {
await showConfirmDialog({
title: '提示',
message: '确定要清空播放历史吗?'
})
videos.value = []
showToast('已清空')
} catch {
// 取消
}
}
const goToVideo = (id: number) => {
router.push(`/video/${id}`)
}
onMounted(() => {
loadVideos(true)
})
</script>
<style scoped lang="scss">
.history-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 60px;
.video-title {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.video-progress {
font-size: 12px;
color: #999;
}
}
</style>
@@ -0,0 +1,147 @@
<template>
<div class="my-videos-page">
<van-nav-bar title="我的视频" fixed placeholder left-arrow @click-left="router.back()">
<template #right>
<van-icon name="plus" size="22" @click="router.push('/upload')" />
</template>
</van-nav-bar>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="loadMore"
>
<van-cell-group v-if="videos.length" inset>
<van-cell v-for="video in videos" :key="video.id" @click="goToVideo(video.id)">
<template #icon>
<van-image :src="video.coverUrl" width="80" height="45" fit="cover" radius="4" />
</template>
<template #title>
<div class="video-title">{{ video.title }}</div>
</template>
<template #label>
<div class="video-stats">
<span>{{ formatNumber(video.viewCount) }}播放</span>
<span>{{ formatNumber(video.likeCount) }}赞</span>
</div>
</template>
<template #value>
<van-tag :type="getStatusType(video.status)">{{ getStatusText(video.status) }}</van-tag>
</template>
</van-cell>
</van-cell-group>
<van-empty v-else description="暂无视频">
<van-button type="primary" round @click="router.push('/upload')">去上传</van-button>
</van-empty>
</van-list>
</van-pull-refresh>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { videoApi } from '@/api/video.ts'
import { useUserStore } from '@/stores/user.ts'
import { formatNumber } from '@/utils'
import type { VideoInfo } from '@/types'
const router = useRouter()
const userStore = useUserStore()
const videos = ref<VideoInfo[]>([])
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const currentPage = ref(1)
const getStatusType = (status: number) => {
const types: Record<number, string> = {
0: 'warning',
1: 'success',
2: 'danger',
3: 'default'
}
return types[status] || 'default'
}
const getStatusText = (status: number) => {
const texts: Record<number, string> = {
0: '审核中',
1: '已发布',
2: '审核拒绝',
3: '已下架'
}
return texts[status] || '未知'
}
const loadVideos = async (isRefresh = false) => {
if (!userStore.userId) return
if (isRefresh) {
currentPage.value = 1
videos.value = []
finished.value = false
}
loading.value = true
try {
const res = await videoApi.getUserVideos(userStore.userId, { page: currentPage.value, size: 20 })
if (isRefresh) {
videos.value = res.records
} else {
videos.value.push(...res.records)
}
if (videos.value.length >= res.total || res.records.length < 20) {
finished.value = true
}
currentPage.value++
} catch (error) {
showToast('加载失败')
} finally {
loading.value = false
refreshing.value = false
}
}
const loadMore = () => {
if (!finished.value) loadVideos()
}
const onRefresh = () => {
loadVideos(true)
}
const goToVideo = (id: number) => {
router.push(`/video/${id}`)
}
onMounted(() => {
loadVideos(true)
})
</script>
<style scoped lang="scss">
.my-videos-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 60px;
.video-title {
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.video-stats {
display: flex;
gap: 12px;
font-size: 12px;
color: #999;
}
}
</style>
@@ -0,0 +1,147 @@
<template>
<div class="profile-edit-page">
<van-nav-bar title="编辑资料" fixed placeholder left-arrow @click-left="router.back()">
<template #right>
<van-button type="primary" size="small" :loading="saving" @click="handleSave">保存</van-button>
</template>
</van-nav-bar>
<van-form @submit="handleSave">
<van-cell-group inset>
<van-cell title="头像" is-link @click="chooseAvatar">
<template #value>
<van-image :src="form.avatar || defaultAvatar" round width="48" height="48" fit="cover" />
</template>
</van-cell>
<van-field v-model="form.nickname" name="nickname" label="昵称" placeholder="请输入昵称" />
<van-field v-model="form.signature" name="signature" label="签名" type="textarea" rows="2" autosize placeholder="介绍一下自己" maxlength="100" show-word-limit />
<van-field name="gender" label="性别">
<template #input>
<van-radio-group v-model="form.gender" direction="horizontal">
<van-radio :value="0">保密</van-radio>
<van-radio :value="1">男</van-radio>
<van-radio :value="2">女</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field v-model="form.birthday" name="birthday" label="生日" is-link readonly placeholder="选择生日" @click="showBirthdayPicker = true" />
<van-field v-model="form.region" name="region" label="地区" placeholder="请输入地区" />
</van-cell-group>
</van-form>
<van-popup v-model:show="showBirthdayPicker" position="bottom" round>
<van-date-picker v-model="selectedDate" title="选择生日" @confirm="onBirthdayConfirm" @cancel="showBirthdayPicker = false" />
</van-popup>
<input ref="avatarInput" type="file" accept="image/*" style="display: none" @change="onAvatarChange" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showSuccessToast } from 'vant'
import { userApi } from '@/api/user.ts'
import { useUserStore } from '@/stores/user.ts'
const router = useRouter()
const userStore = useUserStore()
const saving = ref(false)
const showBirthdayPicker = ref(false)
const avatarInput = ref<HTMLInputElement>()
const selectedDate = ref(['2000', '01', '01'])
const defaultAvatar = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Ccircle cx="50" cy="50" r="50" fill="%23e0e0e0"/%3E%3Ctext x="50%25" y="50%25" fill="%23999" text-anchor="middle" dy=".3em"%3EU%3C/text%3E%3C/svg%3E'
const form = reactive({
avatar: '',
nickname: '',
signature: '',
gender: 0,
birthday: '',
region: ''
})
const loadProfile = async () => {
try {
const profile = await userApi.getCurrentProfile()
form.avatar = profile.avatar || ''
form.nickname = profile.nickname || ''
form.signature = profile.signature || ''
form.gender = profile.gender || 0
form.birthday = profile.birthday || ''
form.region = profile.region || ''
if (form.birthday) {
selectedDate.value = form.birthday.split('-')
}
} catch (error) {
showToast('加载资料失败')
}
}
const chooseAvatar = () => {
avatarInput.value?.click()
}
const onAvatarChange = async (e: Event) => {
const target = e.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
try {
const url = await userApi.uploadAvatar(file)
form.avatar = url
showSuccessToast('头像上传成功')
} catch (error) {
showToast('头像上传失败')
}
}
const onBirthdayConfirm = ({ selectedValues }: any) => {
form.birthday = selectedValues.join('-')
showBirthdayPicker.value = false
}
const handleSave = async () => {
if (!form.nickname.trim()) {
showToast('请输入昵称')
return
}
saving.value = true
try {
await userApi.updateProfile({
nickname: form.nickname,
signature: form.signature,
gender: form.gender,
birthday: form.birthday,
region: form.region
})
if (form.avatar) {
// 头像已单独上传
}
showSuccessToast('保存成功')
router.back()
} catch (error) {
showToast('保存失败')
} finally {
saving.value = false
}
}
onMounted(() => {
loadProfile()
})
</script>
<style scoped lang="scss">
.profile-edit-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 60px;
.van-cell-group {
margin: 12px;
}
}
</style>
@@ -0,0 +1,222 @@
<template>
<div class="profile-page">
<van-nav-bar title="用户主页" fixed placeholder left-arrow @click-left="router.back()" />
<div v-if="loading" class="loading-container">
<van-loading size="24px">加载中...</van-loading>
</div>
<template v-else-if="userInfo">
<div class="user-header">
<div class="user-info">
<van-image :src="userInfo.avatar || defaultAvatar" round width="80" height="80" />
<div class="info">
<h2 class="nickname">{{ userInfo.nickname }}</h2>
<div class="level">Lv.{{ userInfo.level }}</div>
<div class="signature">{{ userInfo.signature || '这个人很懒,什么都没写' }}</div>
</div>
</div>
<div class="stats">
<div class="stat-item">
<div class="value">{{ userInfo.videoCount }}</div>
<div class="label">视频</div>
</div>
<div class="stat-item">
<div class="value">{{ userInfo.followerCount }}</div>
<div class="label">粉丝</div>
</div>
<div class="stat-item">
<div class="value">{{ userInfo.followingCount }}</div>
<div class="label">关注</div>
</div>
<div class="stat-item">
<div class="value">{{ userInfo.likeCount }}</div>
<div class="label">获赞</div>
</div>
</div>
<div class="actions">
<van-button v-if="!isSelf" :type="isFollowed ? 'default' : 'primary'" size="small" @click="toggleFollow">
{{ isFollowed ? '已关注' : '关注' }}
</van-button>
<van-button v-else type="primary" size="small" @click="router.push('/settings/profile')">
编辑资料
</van-button>
</div>
</div>
<van-tabs v-model:active="activeTab" sticky>
<van-tab title="视频" name="videos">
<div class="video-grid">
<VideoCard v-for="video in videos" :key="video.id" :video="video" @click="goToVideo(video.id)" />
</div>
</van-tab>
<van-tab title="收藏" name="favorites">
<van-empty description="暂无收藏" />
</van-tab>
</van-tabs>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast, showSuccessToast } from 'vant'
import VideoCard from '@/components/video/VideoCard.vue'
import { userApi } from '@/api/user.ts'
import { videoApi } from '@/api/video.ts'
import { useUserStore } from '@/stores/user.ts'
import type { UserInfo, VideoInfo } from '@/types'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const userId = computed(() => Number(route.params.id))
const isSelf = computed(() => userStore.userId === userId.value)
const loading = ref(true)
const userInfo = ref<UserInfo | null>(null)
const isFollowed = ref(false)
const activeTab = ref('videos')
const videos = ref<VideoInfo[]>([])
const defaultAvatar = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Ccircle cx="50" cy="50" r="50" fill="%23e0e0e0"/%3E%3Ctext x="50%25" y="50%25" fill="%23999" text-anchor="middle" dy=".3em"%3EU%3C/text%3E%3C/svg%3E'
const loadUserInfo = async () => {
try {
userInfo.value = await userApi.getPublicProfile(userId.value)
if (!isSelf.value && userStore.isLoggedIn) {
isFollowed.value = await userApi.checkFollow(userId.value)
}
} catch (error) {
showToast('加载用户信息失败')
}
}
const loadUserVideos = async () => {
try {
const res = await videoApi.getUserVideos(userId.value, { page: 1, size: 20 })
videos.value = res.records
} catch (error) {
console.error('加载视频失败:', error)
}
}
const toggleFollow = async () => {
if (!userStore.isLoggedIn) {
showToast('请先登录')
return
}
try {
if (isFollowed.value) {
await userApi.unfollow(userId.value)
isFollowed.value = false
showSuccessToast('已取消关注')
} else {
await userApi.follow(userId.value)
isFollowed.value = true
showSuccessToast('关注成功')
}
} catch (error) {
showToast('操作失败')
}
}
const goToVideo = (id: number) => {
router.push(`/video/${id}`)
}
onMounted(async () => {
loading.value = true
await Promise.all([loadUserInfo(), loadUserVideos()])
loading.value = false
})
</script>
<style scoped lang="scss">
.profile-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 60px;
}
.loading-container {
display: flex;
justify-content: center;
padding: 40px;
}
.user-header {
background: #fff;
padding: 20px;
.user-info {
display: flex;
gap: 16px;
margin-bottom: 16px;
.info {
flex: 1;
.nickname {
font-size: 18px;
font-weight: bold;
margin: 0 0 4px;
}
.level {
display: inline-block;
padding: 2px 8px;
background: linear-gradient(to right, #00a1d6, #fb7299);
color: #fff;
font-size: 12px;
border-radius: 10px;
margin-bottom: 8px;
}
.signature {
font-size: 14px;
color: #666;
}
}
}
.stats {
display: flex;
justify-content: space-around;
padding: 16px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
.stat-item {
text-align: center;
.value {
font-size: 18px;
font-weight: bold;
}
.label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
}
}
.actions {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 16px;
}
}
.video-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
padding: 12px;
}
</style>
@@ -0,0 +1,68 @@
<template>
<div class="settings-page">
<van-nav-bar title="设置" fixed placeholder left-arrow @click-left="router.back()" />
<van-cell-group inset>
<van-cell title="账号与安全" is-link to="/settings/profile" />
<van-cell title="隐私设置" is-link to="/settings/privacy" />
<van-cell title="通知设置" is-link to="/settings/notification" />
</van-cell-group>
<van-cell-group inset>
<van-cell title="清除缓存" :value="cacheSize" @click="clearCache" />
<van-cell title="关于我们" is-link to="/about" />
<van-cell title="版本号" value="1.0.0" />
</van-cell-group>
<div class="logout-btn">
<van-button type="danger" block @click="handleLogout">退出登录</van-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { showConfirmDialog, showToast, showSuccessToast } from 'vant'
import { useUserStore } from '@/stores/user.ts'
const router = useRouter()
const userStore = useUserStore()
const cacheSize = ref('0KB')
const clearCache = () => {
showToast('缓存已清除')
cacheSize.value = '0KB'
}
const handleLogout = async () => {
try {
await showConfirmDialog({
title: '提示',
message: '确定要退出登录吗?'
})
await userStore.logout()
showSuccessToast('已退出登录')
router.push('/')
} catch {
// 取消
}
}
</script>
<style scoped lang="scss">
.settings-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 60px;
.van-cell-group {
margin: 12px;
}
.logout-btn {
padding: 20px 16px;
}
}
</style>
@@ -0,0 +1,95 @@
<template>
<div class="user-videos-page">
<van-nav-bar title="用户视频" fixed placeholder left-arrow @click-left="router.back()" />
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="loadMore"
>
<div class="video-grid">
<VideoCard v-for="video in videos" :key="video.id" :video="video" @click="goToVideo(video.id)" />
</div>
</van-list>
</van-pull-refresh>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
import VideoCard from '@/components/video/VideoCard.vue'
import { videoApi } from '@/api/video.ts'
import type { VideoInfo } from '@/types'
const router = useRouter()
const route = useRoute()
const userId = Number(route.params.id)
const videos = ref<VideoInfo[]>([])
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const currentPage = ref(1)
const loadVideos = async (isRefresh = false) => {
if (isRefresh) {
currentPage.value = 1
videos.value = []
finished.value = false
}
loading.value = true
try {
const res = await videoApi.getUserVideos(userId, { page: currentPage.value, size: 20 })
if (isRefresh) {
videos.value = res.records
} else {
videos.value.push(...res.records)
}
if (videos.value.length >= res.total || res.records.length < 20) {
finished.value = true
}
currentPage.value++
} catch (error) {
showToast('加载失败')
} finally {
loading.value = false
refreshing.value = false
}
}
const loadMore = () => {
if (!finished.value) loadVideos()
}
const onRefresh = () => {
loadVideos(true)
}
const goToVideo = (id: number) => {
router.push(`/video/${id}`)
}
onMounted(() => {
loadVideos(true)
})
</script>
<style scoped lang="scss">
.user-videos-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 60px;
}
.video-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
padding: 12px;
}
</style>