feat: add admin console and account settings

Implement the admin dashboard with review, user, and image management workflows. Add profile settings, password reset, pending upload defaults, community placeholder routing, Vercel SPA rewrites, refreshed header styling, and the OpenCloud color-system skill.
This commit is contained in:
2026-05-22 21:04:49 +08:00
parent 599c8107d1
commit 9150a17097
15 changed files with 1410 additions and 50 deletions
+181
View File
@@ -0,0 +1,181 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { NAlert, NButton, NCard, NInput, useMessage } from 'naive-ui'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const message = useMessage()
const usernameDraft = ref('')
const newPassword = ref('')
const confirmNewPassword = ref('')
const error = ref('')
const success = ref('')
const usernameSaving = ref(false)
const passwordSaving = ref(false)
onMounted(() => {
usernameDraft.value = authStore.profile?.username || ''
})
async function saveUsername() {
const nextUsername = usernameDraft.value.trim()
error.value = ''
success.value = ''
if (!nextUsername) {
error.value = '昵称不能为空。'
return
}
if (nextUsername.length < 2) {
error.value = '昵称至少需要 2 个字符。'
return
}
if (nextUsername === authStore.profile?.username) {
success.value = '昵称没有变化。'
return
}
usernameSaving.value = true
try {
await authStore.updateUsername(nextUsername)
success.value = '昵称已更新。'
message.success('昵称已更新')
} catch (e) {
error.value = e instanceof Error ? e.message : '昵称更新失败'
} finally {
usernameSaving.value = false
}
}
async function savePassword() {
error.value = ''
success.value = ''
if (newPassword.value.length < 6) {
error.value = '新密码至少需要 6 位。'
return
}
if (newPassword.value !== confirmNewPassword.value) {
error.value = '两次输入的新密码不一致。'
return
}
passwordSaving.value = true
try {
await authStore.updatePassword(newPassword.value)
newPassword.value = ''
confirmNewPassword.value = ''
success.value = '密码已更新。'
message.success('密码已更新')
} catch (e) {
error.value = e instanceof Error ? e.message : '密码更新失败'
} finally {
passwordSaving.value = false
}
}
</script>
<template>
<div class="min-h-[calc(100vh-4rem)] bg-[linear-gradient(180deg,#e0f2fe_0%,#f8fafc_100%)] px-4 py-10">
<div class="mx-auto max-w-4xl">
<RouterLink to="/profile">
<NButton text type="primary"> 返回个人主页</NButton>
</RouterLink>
<div class="mt-6">
<p class="text-sm font-semibold uppercase tracking-[0.24em] text-sky-700">Account Settings</p>
<h1 class="mt-3 text-4xl font-bold text-slate-900">个人资料设置</h1>
<p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600">
修改公开昵称和登录密码昵称会显示在画廊图鉴和地图中且不能与其他用户重复
</p>
</div>
<div class="mt-8 grid gap-6">
<NAlert
v-if="error"
type="error"
:show-icon="false"
:bordered="false"
>
{{ error }}
</NAlert>
<NAlert
v-if="success"
type="success"
:show-icon="false"
:bordered="false"
>
{{ success }}
</NAlert>
<NCard class="border border-slate-200 bg-white shadow-sm" :bordered="false">
<div class="grid gap-5 lg:grid-cols-[0.8fr_1.2fr] lg:items-start">
<div>
<h2 class="text-2xl font-bold text-slate-900">公开昵称</h2>
<p class="mt-2 text-sm leading-6 text-slate-500">
用于公开展示你的上传记录保存前会检查是否与其他用户重复
</p>
</div>
<div class="grid gap-3 sm:grid-cols-[minmax(0,1fr)_auto]">
<NInput
v-model:value="usernameDraft"
placeholder="输入新的昵称"
maxlength="32"
show-count
/>
<NButton
type="primary"
secondary
strong
:loading="usernameSaving"
@click="saveUsername"
>
保存昵称
</NButton>
</div>
</div>
</NCard>
<NCard class="border border-slate-200 bg-white shadow-sm" :bordered="false">
<div class="grid gap-5 lg:grid-cols-[0.8fr_1.2fr] lg:items-start">
<div>
<h2 class="text-2xl font-bold text-slate-900">登录密码</h2>
<p class="mt-2 text-sm leading-6 text-slate-500">
修改后下次登录需要使用新密码建议使用至少 6 位且不易猜测的密码
</p>
</div>
<div class="grid gap-3">
<NInput
v-model:value="newPassword"
type="password"
show-password-on="click"
autocomplete="new-password"
placeholder="新密码"
/>
<NInput
v-model:value="confirmNewPassword"
type="password"
show-password-on="click"
autocomplete="new-password"
placeholder="确认新密码"
/>
<div class="flex justify-end">
<NButton
secondary
strong
:loading="passwordSaving"
@click="savePassword"
>
保存密码
</NButton>
</div>
</div>
</div>
</NCard>
</div>
</div>
</div>
</template>
+17 -3
View File
@@ -507,9 +507,23 @@ watch(selectedUploadDate, async newValue => {
<h1 class="mt-3 text-4xl font-bold text-slate-900">
{{ pageTitle }}
</h1>
<p class="mt-3 text-lg font-medium text-slate-700">
@{{ profileData?.username || '未知用户' }}
</p>
<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="inline-flex h-8 w-8 items-center justify-center border border-slate-300 bg-white text-slate-600 transition-colors hover:border-slate-900 hover:text-slate-900"
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>