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
+136
View File
@@ -0,0 +1,136 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { NAlert, NButton, NCard, NForm, NFormItem, NInput, NResult } from 'naive-ui'
import { createClient } from '@supabase/supabase-js'
const router = useRouter()
const password = ref('')
const confirmPassword = ref('')
const error = ref('')
const success = ref(false)
const loading = ref(false)
const canSubmit = computed(() => password.value.length >= 6 && password.value === confirmPassword.value)
function getRecoveryTokens() {
const hash = new URLSearchParams(window.location.hash.slice(1))
return {
accessToken: hash.get('access_token'),
refreshToken: hash.get('refresh_token'),
type: hash.get('type'),
error: hash.get('error'),
}
}
async function handleResetPassword() {
error.value = ''
if (password.value.length < 6) {
error.value = '新密码至少需要 6 位。'
return
}
if (password.value !== confirmPassword.value) {
error.value = '两次输入的密码不一致。'
return
}
const { accessToken, refreshToken, type, error: recoveryError } = getRecoveryTokens()
if (recoveryError || type !== 'recovery' || !accessToken || !refreshToken) {
error.value = '密码重置链接无效或已过期,请重新发送邮件。'
return
}
loading.value = true
try {
const resetClient = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY,
)
const { error: sessionError } = await resetClient.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken,
})
if (sessionError) throw sessionError
const { error: updateError } = await resetClient.auth.updateUser({ password: password.value })
if (updateError) throw updateError
await resetClient.auth.signOut()
window.history.replaceState(null, '', window.location.pathname)
success.value = true
} catch (e) {
error.value = e instanceof Error ? e.message : '密码重置失败,请稍后重试。'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="min-h-[calc(100vh-4rem)] px-4 py-10">
<div class="mx-auto max-w-2xl">
<NCard class="shadow-[12px_12px_0_0_rgba(15,23,42,0.08)]">
<NResult
v-if="success"
status="success"
title="密码已重置"
description="现在可以使用新密码登录。"
>
<template #footer>
<NButton type="primary" @click="router.push('/login')">返回登录</NButton>
</template>
</NResult>
<template v-else>
<div class="mb-8">
<div class="text-sm uppercase tracking-[0.22em] text-slate-500">Password Recovery</div>
<h1 class="mt-3 text-3xl font-bold text-slate-900">设置新密码</h1>
<p class="mt-2 text-sm text-slate-500">请输入新的登录密码,提交后当前重置链接会失效。</p>
</div>
<NForm @submit.prevent="handleResetPassword">
<NFormItem label="新密码">
<NInput
v-model:value="password"
type="password"
required
show-password-on="click"
autocomplete="new-password"
placeholder="至少 6 位"
/>
</NFormItem>
<NFormItem label="确认新密码">
<NInput
v-model:value="confirmPassword"
type="password"
required
show-password-on="click"
autocomplete="new-password"
placeholder="再次输入新密码"
/>
</NFormItem>
<NAlert v-if="error" type="error" class="mb-4">
{{ error }}
</NAlert>
<NButton
attr-type="submit"
type="primary"
block
size="large"
:disabled="!canSubmit"
:loading="loading"
>
保存新密码
</NButton>
</NForm>
</template>
</NCard>
</div>
</div>
</template>