fix(frontend): adapt to updated profile APIs
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { NAlert, NButton, NCard, NInput, useMessage } from 'naive-ui'
|
||||
import { NAlert, NButton, NCard, NForm, NInput, useMessage } from 'naive-ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@@ -148,19 +148,27 @@ async function savePassword() {
|
||||
修改后下次登录需要使用新密码。建议使用至少 8 位且不易猜测的密码。
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-3">
|
||||
<NForm class="grid gap-3" @submit.prevent="savePassword">
|
||||
<NInput
|
||||
v-model:value="newPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
autocomplete="new-password"
|
||||
:input-props="{
|
||||
autocomplete: 'new-password',
|
||||
name: 'new-password',
|
||||
required: true,
|
||||
}"
|
||||
placeholder="新密码"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="confirmNewPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
autocomplete="new-password"
|
||||
:input-props="{
|
||||
autocomplete: 'new-password',
|
||||
name: 'confirm-new-password',
|
||||
required: true,
|
||||
}"
|
||||
placeholder="确认新密码"
|
||||
/>
|
||||
<div class="flex justify-end">
|
||||
@@ -168,14 +176,14 @@ async function savePassword() {
|
||||
secondary
|
||||
strong
|
||||
type="default"
|
||||
attr-type="submit"
|
||||
class="oc-panel-button oc-panel-button--teal"
|
||||
:loading="passwordSaving"
|
||||
@click="savePassword"
|
||||
>
|
||||
保存密码
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</NForm>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { NAlert, NButton, NCard, NDropdown, NEmpty, NIcon, NSkeleton, useMessage } from 'naive-ui'
|
||||
import EncyclopediaProgressCard from '@/components/cloud/EncyclopediaProgressCard.vue'
|
||||
import { Settings } from '@vicons/tabler'
|
||||
import CloudEditModal, { type CloudEditFormValue } from '@/components/cloud/CloudEditModal.vue'
|
||||
import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue'
|
||||
@@ -10,7 +9,6 @@ import MiniLocationMap from '@/components/cloud/MiniLocationMap.vue'
|
||||
import ContributionHeatmap from '@/components/profile/ContributionHeatmap.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCloudsStore } from '@/stores/clouds'
|
||||
import { useEncyclopediaStore } from '@/stores/encyclopedia'
|
||||
import { useProfileStore, type ProfileCloudItem } from '@/stores/profile'
|
||||
import type { CloudType, Profile } from '@/types/database'
|
||||
|
||||
@@ -23,7 +21,6 @@ interface TimelineGroup {
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const cloudsStore = useCloudsStore()
|
||||
const encyclopediaStore = useEncyclopediaStore()
|
||||
const profileStore = useProfileStore()
|
||||
const message = useMessage()
|
||||
|
||||
@@ -65,6 +62,7 @@ const errorMsg = computed(() => {
|
||||
})
|
||||
const profileData = computed<Profile | null>(() => profileStore.getProfile(viewedUserId.value))
|
||||
const clouds = computed<ProfileCloudItem[]>(() => profileStore.getClouds(viewedUserId.value, isOwnProfile.value))
|
||||
const receivedLikes = computed(() => profileStore.getLikeStats(viewedUserId.value)?.total_likes || 0)
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
return isOwnProfile.value ? '我的天空日志' : `${profileData.value?.username || '这位用户'}的天空日志`
|
||||
@@ -83,10 +81,6 @@ const approvedShots = computed(() => {
|
||||
return clouds.value.filter(item => item.status === 'approved').length
|
||||
})
|
||||
|
||||
const pendingShots = computed(() => {
|
||||
return clouds.value.filter(item => item.status === 'pending').length
|
||||
})
|
||||
|
||||
const selectedCloudCount = computed(() => selectedCloudIds.value.size)
|
||||
|
||||
const cloudActionOptions = computed(() => {
|
||||
@@ -184,44 +178,6 @@ const activeUploadDays = computed(() => {
|
||||
return contributionCells.value.filter(cell => cell.count > 0).length
|
||||
})
|
||||
|
||||
const currentUploadStreak = computed(() => {
|
||||
let streak = 0
|
||||
const cursor = startOfDay(new Date())
|
||||
|
||||
while (true) {
|
||||
const key = toLocalDayKey(cursor)
|
||||
if ((uploadDayCounts.value.get(key) || 0) <= 0) break
|
||||
streak += 1
|
||||
cursor.setDate(cursor.getDate() - 1)
|
||||
}
|
||||
|
||||
return streak
|
||||
})
|
||||
|
||||
const longestUploadStreak = computed(() => {
|
||||
let best = 0
|
||||
let current = 0
|
||||
let previousDate: Date | null = null
|
||||
|
||||
for (const cell of contributionCells.value) {
|
||||
if (cell.count <= 0) continue
|
||||
|
||||
const currentDate = startOfDay(new Date(cell.date))
|
||||
|
||||
if (!previousDate) {
|
||||
current = 1
|
||||
} else {
|
||||
const diffDays = Math.round((currentDate.getTime() - previousDate.getTime()) / 86400000)
|
||||
current = diffDays === 1 ? current + 1 : 1
|
||||
}
|
||||
|
||||
if (current > best) best = current
|
||||
previousDate = currentDate
|
||||
}
|
||||
|
||||
return best
|
||||
})
|
||||
|
||||
function toDayKey(iso: string) {
|
||||
const date = new Date(iso)
|
||||
return toLocalDayKey(date)
|
||||
@@ -233,10 +189,6 @@ function toLocalDayKey(date: Date) {
|
||||
return `${date.getFullYear()}-${month}-${day}`
|
||||
}
|
||||
|
||||
function startOfDay(date: Date) {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
}
|
||||
|
||||
function formatDate(iso: string | null) {
|
||||
if (!iso) return '未知时间'
|
||||
return new Date(iso).toLocaleDateString('zh-CN', {
|
||||
@@ -448,8 +400,6 @@ async function loadProfilePage(force = false) {
|
||||
|
||||
if (isOwnProfile.value) {
|
||||
await cloudsStore.fetchCloudTypes()
|
||||
await encyclopediaStore.fetchCloudTypes(force)
|
||||
await encyclopediaStore.fetchMyCollection(force)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,77 +425,98 @@ watch(selectedUploadDate, async newValue => {
|
||||
<div class="relative">
|
||||
<div class="oc-page-hero">
|
||||
<div class="oc-container oc-container--content oc-page-hero__inner">
|
||||
<div v-if="loading" class="grid gap-6 lg:grid-cols-[1.25fr_0.95fr]">
|
||||
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="grid gap-6 lg:grid-cols-[1.25fr_0.95fr]"
|
||||
>
|
||||
<NCard class="oc-panel-card h-full border border-slate-200 shadow-sm" :bordered="false">
|
||||
<NSkeleton class="h-6 w-1/3" />
|
||||
<NSkeleton class="mt-4 h-10 w-2/3" />
|
||||
<NSkeleton class="mt-5 h-4 w-full" :repeat="2" />
|
||||
</NCard>
|
||||
<NCard class="oc-panel-card-soft border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
||||
<div v-if="isOwnProfile" class="grid grid-cols-2 gap-3 lg:h-full lg:grid-rows-2">
|
||||
<NCard v-for="n in 4" :key="n" class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
||||
<NSkeleton class="h-3 w-2/3" />
|
||||
<NSkeleton class="mt-3 h-7 w-1/2" />
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<NCard v-else class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
||||
<NSkeleton class="h-5 w-1/3" />
|
||||
<NSkeleton class="mt-4 h-8 w-1/2" />
|
||||
<NSkeleton class="mt-5 h-3 w-full" />
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid gap-6 lg:grid-cols-[1.25fr_0.95fr] lg:items-start">
|
||||
<div class="flex gap-5">
|
||||
<div
|
||||
class="flex h-20 w-20 shrink-0 items-center justify-center border border-slate-300 bg-[linear-gradient(135deg,#eef9f3_0%,#d9efe3_100%)] text-3xl font-bold text-slate-900 shadow-[4px_4px_0_0_rgba(15,23,42,0.08)]"
|
||||
>
|
||||
<img
|
||||
v-if="profileData?.avatar_url"
|
||||
:src="profileData.avatar_url"
|
||||
:alt="profileData.username"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ getProfileInitial(profileData) }}
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="grid gap-6 lg:grid-cols-[1.25fr_0.95fr] lg:items-stretch"
|
||||
>
|
||||
<NCard class="oc-panel-card h-full border border-slate-200 bg-white/90 shadow-sm backdrop-blur" :bordered="false">
|
||||
<div class="flex gap-5">
|
||||
<div
|
||||
class="flex h-20 w-20 shrink-0 items-center justify-center border border-slate-300 bg-[linear-gradient(135deg,#eef9f3_0%,#d9efe3_100%)] text-3xl font-bold text-slate-900 shadow-[4px_4px_0_0_rgba(15,23,42,0.08)]"
|
||||
>
|
||||
<img
|
||||
v-if="profileData?.avatar_url"
|
||||
:src="profileData.avatar_url"
|
||||
:alt="profileData.username"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ getProfileInitial(profileData) }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<p class="oc-page-eyebrow">Sky Journal</p>
|
||||
<h1 class="oc-page-title">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-3">
|
||||
<p class="text-lg font-medium text-slate-700">
|
||||
@{{ profileData?.username || '未知用户' }}
|
||||
</p>
|
||||
<RouterLink v-if="isOwnProfile" to="/profile/settings">
|
||||
<button
|
||||
type="button"
|
||||
class="oc-icon-button oc-icon-button--small"
|
||||
title="个人资料设置"
|
||||
aria-label="个人资料设置"
|
||||
>
|
||||
<NIcon size="17">
|
||||
<Settings />
|
||||
</NIcon>
|
||||
</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<!-- <p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600">
|
||||
{{ profileSubtitle }}
|
||||
</p> -->
|
||||
<div class="mt-5 flex flex-wrap items-center gap-3 text-sm text-slate-500">
|
||||
<span>注册于 {{ formatDate(profileData?.created_at || null) }}</span>
|
||||
<!-- <NTag v-if="isOwnProfile" type="primary" :bordered="false">你的主页</NTag>
|
||||
<NTag v-else :bordered="false">公开展示</NTag> -->
|
||||
<div class="min-w-0">
|
||||
<p class="oc-page-eyebrow">Sky Journal</p>
|
||||
<h1 class="oc-page-title">
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-3">
|
||||
<p class="text-lg font-medium text-slate-700">
|
||||
@{{ profileData?.username || '未知用户' }}
|
||||
</p>
|
||||
<RouterLink v-if="isOwnProfile" to="/profile/settings">
|
||||
<button
|
||||
type="button"
|
||||
class="oc-icon-button oc-icon-button--small"
|
||||
title="个人资料设置"
|
||||
aria-label="个人资料设置"
|
||||
>
|
||||
<NIcon size="17">
|
||||
<Settings />
|
||||
</NIcon>
|
||||
</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="mt-5 flex flex-wrap items-center gap-3 text-sm text-slate-500">
|
||||
<span>注册于 {{ formatDate(profileData?.created_at || null) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<div v-if="isOwnProfile" class="grid grid-cols-2 gap-3 lg:h-full lg:grid-rows-2">
|
||||
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
||||
<p class="oc-stat-label">总拍摄数</p>
|
||||
<p class="oc-stat-value">{{ totalShots }}</p>
|
||||
</NCard>
|
||||
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
||||
<p class="oc-stat-label">最常拍云型</p>
|
||||
<p class="mt-2 truncate text-xl font-bold text-slate-900">{{ mostCommonCloudType.name }}</p>
|
||||
</NCard>
|
||||
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
||||
<p class="oc-stat-label">拍摄天数</p>
|
||||
<p class="oc-stat-value">{{ shootingDays }}</p>
|
||||
</NCard>
|
||||
<NCard class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
||||
<p class="oc-stat-label">被点赞数</p>
|
||||
<p class="oc-stat-value">{{ receivedLikes }}</p>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<EncyclopediaProgressCard
|
||||
v-if="isOwnProfile"
|
||||
:unlocked-count="encyclopediaStore.unlockedCount"
|
||||
:total-count="encyclopediaStore.cloudTypes.length || 10"
|
||||
:percent="encyclopediaStore.unlockPercent"
|
||||
:is-logged-in="true"
|
||||
/>
|
||||
|
||||
<NCard v-else class="oc-panel-card-soft border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
||||
<NCard v-if="!isOwnProfile" class="oc-panel-card-soft h-full border border-white/80 bg-white/85 shadow-sm backdrop-blur" :bordered="false">
|
||||
<p class="text-sm text-slate-500">公开贡献</p>
|
||||
<p class="mt-3 text-3xl font-bold text-slate-900">{{ approvedShots }}</p>
|
||||
<p class="mt-2 text-sm text-slate-500">当前公开可见的云图数量</p>
|
||||
@@ -577,50 +548,12 @@ watch(selectedUploadDate, async newValue => {
|
||||
</NAlert>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="loading" class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<NCard v-for="n in 4" :key="n">
|
||||
<NSkeleton class="h-4 w-1/3" />
|
||||
<NSkeleton class="mt-4 h-8 w-2/3" />
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
||||
<p class="text-sm text-slate-500">总拍摄数</p>
|
||||
<p class="mt-2 text-3xl font-bold text-slate-900">{{ totalShots }}</p>
|
||||
</NCard>
|
||||
|
||||
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
||||
<p class="text-sm text-slate-500">最常拍云型</p>
|
||||
<p class="oc-stat-value">{{ mostCommonCloudType.name }}</p>
|
||||
<p class="mt-2 text-sm text-slate-500" v-if="mostCommonCloudType.count">
|
||||
共 {{ mostCommonCloudType.count }} 张
|
||||
</p>
|
||||
</NCard>
|
||||
|
||||
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
||||
<p class="text-sm text-slate-500">拍摄天数</p>
|
||||
<p class="mt-2 text-3xl font-bold text-slate-900">{{ shootingDays }}</p>
|
||||
</NCard>
|
||||
|
||||
<NCard class="oc-panel-card border border-slate-200 shadow-sm" :bordered="false">
|
||||
<p class="text-sm text-slate-500">{{ isOwnProfile ? '已通过 / 待审核' : '公开通过数' }}</p>
|
||||
<p class="oc-stat-value">
|
||||
<template v-if="isOwnProfile">{{ approvedShots }} / {{ pendingShots }}</template>
|
||||
<template v-else>{{ approvedShots }}</template>
|
||||
</p>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<section ref="timelineSectionRef" class="mt-8">
|
||||
<section ref="timelineSectionRef">
|
||||
<ContributionHeatmap
|
||||
:cells="contributionCells"
|
||||
:total-count="totalShots"
|
||||
:active-days="activeUploadDays"
|
||||
:current-streak="currentUploadStreak"
|
||||
:best-streak="longestUploadStreak"
|
||||
:active-date="selectedUploadDate"
|
||||
:subtitle="isOwnProfile ? '按上传日期统计' : '按公开上传日期统计'"
|
||||
:title="isOwnProfile ? '上传活跃度' : '公开活跃度'"
|
||||
@select-date="toggleSelectedUploadDate"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user