feat(frontend): Align admin and user API integration
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="about-page">
|
||||
<van-nav-bar title="关于" fixed placeholder left-arrow @click-left="router.back()" />
|
||||
<section class="about-card">
|
||||
<h2>HAOVP 在线视频平台</h2>
|
||||
<p>一个面向内容上传、播放互动与平台治理场景构建的在线视频系统。</p>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="当前版本" value="1.0.0" />
|
||||
<van-cell title="主要能力" value="上传、播放、评论、推荐、后台治理" />
|
||||
<van-cell title="技术栈" value="Vue 3 + Vite + Vant" />
|
||||
</van-cell-group>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.about-page { min-height: 100vh; background: #f6f7fb; }
|
||||
.about-card {
|
||||
margin: 20px 16px;
|
||||
padding: 24px;
|
||||
border-radius: 24px;
|
||||
background: #fff;
|
||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
</style>
|
||||
@@ -35,8 +35,9 @@
|
||||
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 { userApi } from '@/api/user.ts'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import type { UserFollow } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -46,8 +47,6 @@ 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 () => {
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
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 { userApi } from '@/api/user.ts'
|
||||
import type { UserFollow } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -86,7 +87,9 @@ onMounted(() => {
|
||||
<style scoped lang="scss">
|
||||
.follow-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(20, 184, 166, 0.08), transparent 180px),
|
||||
#f4f7fb;
|
||||
padding-bottom: 60px;
|
||||
|
||||
.nickname {
|
||||
@@ -97,5 +100,12 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
:deep(.van-cell-group) {
|
||||
margin: 12px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,6 +39,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import { formatDuration } from '@/utils'
|
||||
import { mockVideos } from '@/mock/data'
|
||||
|
||||
interface HistoryItem {
|
||||
videoId: number
|
||||
@@ -60,7 +61,13 @@ const loadVideos = async (isRefresh = false) => {
|
||||
finished.value = false
|
||||
}
|
||||
loading.value = true
|
||||
// TODO: 实际从API获取播放历史
|
||||
videos.value = mockVideos.slice(0, 4).map((item, index) => ({
|
||||
videoId: item.id,
|
||||
title: item.title,
|
||||
coverUrl: item.coverUrl,
|
||||
progress: Math.max(60, Math.floor(item.duration * (0.2 + index * 0.12))),
|
||||
duration: item.duration
|
||||
}))
|
||||
finished.value = true
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
|
||||
@@ -58,14 +58,14 @@ const finished = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const currentPage = ref(1)
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
const types: Record<number, string> = {
|
||||
const getStatusType = (status: number): 'warning' | 'success' | 'danger' | 'primary' => {
|
||||
const types: Record<number, 'warning' | 'success' | 'danger' | 'primary'> = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
2: 'danger',
|
||||
3: 'default'
|
||||
3: 'primary'
|
||||
}
|
||||
return types[status] || 'default'
|
||||
return types[status] || 'primary'
|
||||
}
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
@@ -89,7 +89,7 @@ const loadVideos = async (isRefresh = false) => {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await videoApi.getUserVideos(userStore.userId, { page: currentPage.value, size: 20 })
|
||||
const res = await videoApi.getUserVideos(userStore.userId, { pageNum: currentPage.value, pageSize: 20 })
|
||||
if (isRefresh) {
|
||||
videos.value = res.records
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="settings-subpage">
|
||||
<van-nav-bar title="通知设置" fixed placeholder left-arrow @click-left="router.back()" />
|
||||
<van-cell-group inset>
|
||||
<van-cell title="评论通知">
|
||||
<template #right-icon><van-switch v-model="form.commentNotify" :active-value="1" :inactive-value="0" /></template>
|
||||
</van-cell>
|
||||
<van-cell title="点赞通知">
|
||||
<template #right-icon><van-switch v-model="form.likeNotify" :active-value="1" :inactive-value="0" /></template>
|
||||
</van-cell>
|
||||
<van-cell title="关注通知">
|
||||
<template #right-icon><van-switch v-model="form.followNotify" :active-value="1" :inactive-value="0" /></template>
|
||||
</van-cell>
|
||||
<van-cell title="系统通知">
|
||||
<template #right-icon><van-switch v-model="form.systemNotify" :active-value="1" :inactive-value="0" /></template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
<div class="submit">
|
||||
<van-button type="primary" block @click="save">保存设置</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showSuccessToast } from 'vant'
|
||||
import { userApi } from '@/api/user.ts'
|
||||
|
||||
const router = useRouter()
|
||||
const form = reactive({
|
||||
commentNotify: 1,
|
||||
likeNotify: 1,
|
||||
followNotify: 1,
|
||||
systemNotify: 1
|
||||
})
|
||||
|
||||
const save = async () => {
|
||||
await userApi.updateSettings(form)
|
||||
showSuccessToast('已保存')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
Object.assign(form, await userApi.getSettings())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-subpage { min-height: 100vh; background: #f6f7fb; }
|
||||
.submit { padding: 20px 16px; }
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="settings-subpage">
|
||||
<van-nav-bar title="隐私设置" fixed placeholder left-arrow @click-left="router.back()" />
|
||||
<van-cell-group inset>
|
||||
<van-cell title="主页公开范围">
|
||||
<template #right-icon>
|
||||
<van-dropdown-menu>
|
||||
<van-dropdown-item v-model="form.privacyLevel" :options="privacyOptions" />
|
||||
</van-dropdown-menu>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="个性签名可见" value="公开" />
|
||||
<van-cell title="关注列表可见" value="仅自己可见" />
|
||||
</van-cell-group>
|
||||
<div class="submit">
|
||||
<van-button type="primary" block @click="save">保存设置</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showSuccessToast } from 'vant'
|
||||
import { userApi } from '@/api/user.ts'
|
||||
|
||||
const router = useRouter()
|
||||
const form = reactive({ privacyLevel: 1 })
|
||||
const privacyOptions = [
|
||||
{ text: '公开', value: 1 },
|
||||
{ text: '仅关注可见', value: 2 },
|
||||
{ text: '仅自己可见', value: 3 }
|
||||
]
|
||||
|
||||
const save = async () => {
|
||||
await userApi.updateSettings({ privacyLevel: form.privacyLevel })
|
||||
showSuccessToast('已保存')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const settings = await userApi.getSettings()
|
||||
form.privacyLevel = settings.privacyLevel
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-subpage { min-height: 100vh; background: #f6f7fb; }
|
||||
.submit { padding: 20px 16px; }
|
||||
</style>
|
||||
@@ -42,10 +42,8 @@ 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)
|
||||
@@ -137,11 +135,16 @@ onMounted(() => {
|
||||
<style scoped lang="scss">
|
||||
.profile-edit-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(59, 130, 246, 0.08), transparent 180px),
|
||||
#f4f7fb;
|
||||
padding-bottom: 60px;
|
||||
|
||||
.van-cell-group {
|
||||
margin: 12px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,65 +1,74 @@
|
||||
<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 class="profile-shell">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<van-loading size="24px">加载中...</van-loading>
|
||||
</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)" />
|
||||
<template v-else-if="userInfo">
|
||||
<div class="profile-banner">
|
||||
<div class="banner-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div class="user-header">
|
||||
<div class="user-info">
|
||||
<van-image :src="userInfo.avatar || defaultAvatar" round width="104" height="104" />
|
||||
<div class="info">
|
||||
<h2 class="nickname">{{ userInfo.nickname }}</h2>
|
||||
<div class="meta-row">
|
||||
<div class="level">Lv.{{ userInfo.level }}</div>
|
||||
<div class="region">{{ userInfo.region }}</div>
|
||||
</div>
|
||||
<div class="signature">{{ userInfo.signature || '这个人很懒,什么都没写' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="收藏" name="favorites">
|
||||
<van-empty description="暂无收藏" />
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</template>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item" @click="goToUserVideos">
|
||||
<div class="value">{{ userInfo.videoCount }}</div>
|
||||
<div class="label">视频</div>
|
||||
</div>
|
||||
<div class="stat-item" @click="goToFollowers">
|
||||
<div class="value">{{ userInfo.followerCount }}</div>
|
||||
<div class="label">粉丝</div>
|
||||
</div>
|
||||
<div class="stat-item" @click="goToFollowing">
|
||||
<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" round @click="toggleFollow">
|
||||
{{ isFollowed ? '已关注' : '关注' }}
|
||||
</van-button>
|
||||
<van-button v-else type="primary" size="small" round @click="router.push('/settings/profile')">
|
||||
编辑资料
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-tabs v-model:active="activeTab" sticky class="profile-tabs">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { showToast, showSuccessToast } from 'vant'
|
||||
import VideoCard from '@/components/video/VideoCard.vue'
|
||||
@@ -72,7 +81,7 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userId = computed(() => Number(route.params.id))
|
||||
const userId = computed(() => Number(route.params.id || userStore.userId || userStore.userInfo?.id || 0))
|
||||
const isSelf = computed(() => userStore.userId === userId.value)
|
||||
|
||||
const loading = ref(true)
|
||||
@@ -89,17 +98,17 @@ const loadUserInfo = async () => {
|
||||
if (!isSelf.value && userStore.isLoggedIn) {
|
||||
isFollowed.value = await userApi.checkFollow(userId.value)
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
showToast('加载用户信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadUserVideos = async () => {
|
||||
try {
|
||||
const res = await videoApi.getUserVideos(userId.value, { page: 1, size: 20 })
|
||||
const res = await videoApi.getUserVideos(userId.value, { pageNum: 1, pageSize: 20 })
|
||||
videos.value = res.records
|
||||
} catch (error) {
|
||||
console.error('加载视频失败:', error)
|
||||
} catch {
|
||||
showToast('加载视频失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +127,7 @@ const toggleFollow = async () => {
|
||||
isFollowed.value = true
|
||||
showSuccessToast('关注成功')
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
showToast('操作失败')
|
||||
}
|
||||
}
|
||||
@@ -127,96 +136,203 @@ const goToVideo = (id: number) => {
|
||||
router.push(`/video/${id}`)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const goToFollowers = () => {
|
||||
router.push(`/user/${userId.value}/followers`)
|
||||
}
|
||||
|
||||
const goToFollowing = () => {
|
||||
router.push(`/user/${userId.value}/following`)
|
||||
}
|
||||
|
||||
const goToUserVideos = () => {
|
||||
router.push(`/user/${userId.value}/videos`)
|
||||
}
|
||||
|
||||
const initPage = async () => {
|
||||
loading.value = true
|
||||
await Promise.all([loadUserInfo(), loadUserVideos()])
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(initPage)
|
||||
watch(() => route.params.id, initPage)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
background: #f6f7fb;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.profile-shell {
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 12px 28px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
height: 220px;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(135deg, #60a5fa, #8b5cf6 52%, #fb7185);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 24px 52px rgba(99, 102, 241, 0.18);
|
||||
}
|
||||
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(circle at right top, rgba(255, 255, 255, 0.3), transparent 24%),
|
||||
radial-gradient(circle at left bottom, rgba(255, 255, 255, 0.18), transparent 28%);
|
||||
}
|
||||
|
||||
.user-header {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
padding: 24px 26px;
|
||||
border-radius: 28px;
|
||||
margin-top: -48px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
.user-info {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
.info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.nickname {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
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;
|
||||
}
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.signature {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
.level {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(to right, #fb7299, #ff8fab);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.region {
|
||||
color: #9499a0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.signature {
|
||||
font-size: 15px;
|
||||
color: #61666d;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
padding: 18px 0;
|
||||
border-top: 1px solid #f1f2f3;
|
||||
border-bottom: 1px solid #f1f2f3;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
min-width: 64px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 18px;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid #eee;
|
||||
border-bottom: 1px solid #eee;
|
||||
.value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #9499a0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.profile-tabs {
|
||||
margin-top: 18px;
|
||||
background: #fff;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.video-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 18px 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-shell {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
height: 160px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.user-header {
|
||||
margin-top: -28px;
|
||||
padding: 18px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.video-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<van-nav-bar title="设置" fixed placeholder left-arrow @click-left="router.back()" />
|
||||
<div class="settings-shell">
|
||||
<section class="settings-hero">
|
||||
<h2>账号设置</h2>
|
||||
<p>管理个人资料、隐私选项、通知偏好和应用信息。</p>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<div class="settings-grid">
|
||||
<div class="settings-column">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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="settings-column">
|
||||
<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 class="logout-btn">
|
||||
<van-button type="danger" block @click="handleLogout">退出登录</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -54,15 +65,54 @@ const handleLogout = async () => {
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
background: #f6f7fb;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.van-cell-group {
|
||||
margin: 12px;
|
||||
.settings-shell {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 12px 28px;
|
||||
}
|
||||
|
||||
.settings-hero {
|
||||
padding: 24px 26px;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: #fff;
|
||||
margin-bottom: 18px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 20px 16px;
|
||||
p {
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.settings-column :deep(.van-cell-group) {
|
||||
margin: 0;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -44,7 +44,7 @@ const loadVideos = async (isRefresh = false) => {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await videoApi.getUserVideos(userId, { page: currentPage.value, size: 20 })
|
||||
const res = await videoApi.getUserVideos(userId, { pageNum: currentPage.value, pageSize: 20 })
|
||||
if (isRefresh) {
|
||||
videos.value = res.records
|
||||
} else {
|
||||
@@ -82,14 +82,16 @@ onMounted(() => {
|
||||
<style scoped lang="scss">
|
||||
.user-videos-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
background: #f6f7fb;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.video-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 18px 16px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 12px 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user