使用 Nuxt 3 构建用户管理应用
探索适用于你的 Supabase 应用的即用型 UI 组件。
基于 shadcn/ui 构建的 UI 组件,通过单个命令连接到 Supabase。
探索组件本教程演示如何构建一个基本的用户管理应用。该应用对用户进行身份验证和识别,将他们的个人资料信息存储在数据库中,并允许用户登录、更新他们的个人资料详细信息以及上传个人资料照片。该应用使用
- Supabase 数据库 - 一个用于存储用户数据的 Postgres 数据库,以及 行级别安全,以保护数据并确保用户只能访问他们自己的信息。
- Supabase Auth - 允许用户注册和登录。
- Supabase Storage - 允许用户上传个人资料照片。

如果在阅读本指南时遇到问题,请参考 GitHub 上的完整示例。
项目设置#
在开始构建之前,您需要设置数据库和 API。您可以通过在 Supabase 中启动一个新项目,然后在数据库中创建一个“schema”来完成此操作。
创建项目#
- 在 Supabase 控制面板中创建一个新项目。
- 输入您的项目详细信息。
- 等待新的数据库启动。
设置数据库 schema#
现在设置数据库 schema。您可以使用 SQL 编辑器中的“用户管理 Starter”快速入门,或者您可以复制/粘贴下面的 SQL 并运行它。
获取 API 详细信息#
现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。
为此,您需要从 项目的 Connect 对话框中获取项目 URL 和密钥。
API 密钥的更改
Supabase 正在更改密钥的工作方式,以提高项目安全性和开发人员体验。您可以 阅读完整的公告,但在过渡期间,您可以使用当前的 anon 和 service_role 密钥以及新的可发布密钥,格式为 sb_publishable_xxx,它将取代旧的密钥。
在大多数情况下,您可以从 项目的 Connect 对话框中获取正确的密钥,但如果您需要特定的密钥,可以在 项目设置页面的 API 密钥部分中找到所有密钥
- 对于旧版密钥,从 旧版 API 密钥 选项卡中复制
anon密钥用于客户端操作,并复制service_role密钥用于服务器端操作。 - 对于新密钥,打开 API 密钥 选项卡,如果您还没有可发布密钥,请单击 创建新的 API 密钥,并复制 可发布密钥 部分中的值。
阅读 API 密钥文档 以全面了解所有密钥类型及其用途。
构建应用#
让我们从头开始构建 Vue 3 应用。
初始化 Nuxt 3 应用#
我们可以使用 nuxi init 创建一个名为 nuxt-user-management 的应用
1npx nuxi init nuxt-user-management23cd nuxt-user-management然后让我们安装唯一的附加依赖项:Nuxt Supabase。 我们只需要将 Nuxt Supabase 作为开发依赖项导入。
1npm install @nuxtjs/supabase --save-dev最后,我们希望将环境变量保存在一个 .env 文件中。 我们只需要您之前复制的 API URL 和密钥 此处。
1SUPABASE_URL="YOUR_SUPABASE_URL"2SUPABASE_KEY="YOUR_SUPABASE_PUBLISHABLE_KEY"这些变量将在浏览器上公开,这完全没问题,因为我们在数据库上启用了 行级别安全。 Nuxt Supabase 的神奇之处在于,设置环境变量是我们开始使用 Supabase 所需做的全部工作。 无需初始化 Supabase。 该库将自动处理它。
应用样式 (可选)#
一个可选步骤是更新 CSS 文件 assets/main.css 以使应用看起来不错。 您可以在 此处找到此文件的完整内容。
1import { defineNuxtConfig } from 'nuxt'23// https://v3.nuxtjs.org/api/configuration/nuxt.config4export default defineNuxtConfig({5 modules: ['@nuxtjs/supabase'],6 css: ['@/assets/main.css'],7})设置 Auth 组件#
让我们设置一个 Vue 组件来管理登录和注册。 我们将使用 Magic Links,以便用户无需使用密码即可通过电子邮件登录。
1<script setup>2const supabase = useSupabaseClient()34const loading = ref(false)5const email = ref('')67const handleLogin = async () => {8 try {9 loading.value = true10 const { error } = await supabase.auth.signInWithOtp({ email: email.value })11 if (error) throw error12 alert('Check your email for the login link!')13 } catch (error) {14 alert(error.error_description || error.message)15 } finally {16 loading.value = false17 }18}19</script>2021<template>22 <form class="row flex-center flex" @submit.prevent="handleLogin">23 <div class="col-6 form-widget">24 <h1 class="header">Supabase + Nuxt 3</h1>25 <p class="description">Sign in via magic link with your email below</p>26 <div>27 <input class="inputField" type="email" placeholder="Your email" v-model="email" />28 </div>29 <div>30 <input31 type="submit"32 class="button block"33 :value="loading ? 'Loading' : 'Send magic link'"34 :disabled="loading"35 />36 </div>37 </div>38 </form>39</template>用户状态#
要访问用户信息,请使用 Supabase Nuxt 模块提供的 composable useSupabaseUser。
Account 组件#
用户登录后,我们可以允许他们编辑个人资料详细信息并管理他们的帐户。 让我们创建一个名为 Account.vue 的新组件。
1<script setup>2const supabase = useSupabaseClient()34const loading = ref(true)5const username = ref('')6const website = ref('')7const avatar_path = ref('')89loading.value = true10const user = useSupabaseUser()1112const { data } = await supabase13 .from('profiles')14 .select(`username, website, avatar_url`)15 .eq('id', user.value.id)16 .single()1718if (data) {19 username.value = data.username20 website.value = data.website21 avatar_path.value = data.avatar_url22}2324loading.value = false2526async function updateProfile() {27 try {28 loading.value = true29 const user = useSupabaseUser()3031 const updates = {32 id: user.value.id,33 username: username.value,34 website: website.value,35 avatar_url: avatar_path.value,36 updated_at: new Date(),37 }3839 const { error } = await supabase.from('profiles').upsert(updates, {40 returning: 'minimal', // Don't return the value after inserting41 })42 if (error) throw error43 } catch (error) {44 alert(error.message)45 } finally {46 loading.value = false47 }48}4950async function signOut() {51 try {52 loading.value = true53 const { error } = await supabase.auth.signOut()54 if (error) throw error55 user.value = null56 } catch (error) {57 alert(error.message)58 } finally {59 loading.value = false60 }61}62</script>6364<template>65 <form class="form-widget" @submit.prevent="updateProfile">66 <div>67 <label for="email">Email</label>68 <input id="email" type="text" :value="user.email" disabled />69 </div>70 <div>71 <label for="username">Username</label>72 <input id="username" type="text" v-model="username" />73 </div>74 <div>75 <label for="website">Website</label>76 <input id="website" type="url" v-model="website" />77 </div>7879 <div>80 <input81 type="submit"82 class="button primary block"83 :value="loading ? 'Loading ...' : 'Update'"84 :disabled="loading"85 />86 </div>8788 <div>89 <button class="button block" @click="signOut" :disabled="loading">Sign Out</button>90 </div>91 </form>92</template>启动!#
现在我们已经准备好所有组件,让我们更新 app.vue
1<script setup>2const user = useSupabaseUser()3</script>45<template>6 <div class="container" style="padding: 50px 0 100px 0">7 <Account v-if="user" />8 <Auth v-else />9 </div>10</template>完成此操作后,在终端窗口中运行以下命令
1npm run dev然后在浏览器中打开 localhost:3000,您应该会看到完成的应用。

奖励:个人资料照片#
每个 Supabase 项目都配置了 存储,用于管理大型文件,如照片和视频。
创建一个上传小部件#
让我们为用户创建一个头像,以便他们上传个人资料照片。 我们可以从创建一个新组件开始
1<script setup>2const props = defineProps(['path'])3const { path } = toRefs(props)45const emit = defineEmits(['update:path', 'upload'])67const supabase = useSupabaseClient()89const uploading = ref(false)10const src = ref('')11const files = ref()1213const downloadImage = async () => {14 try {15 const { data, error } = await supabase.storage.from('avatars').download(path.value)16 if (error) throw error17 src.value = URL.createObjectURL(data)18 } catch (error) {19 console.error('Error downloading image: ', error.message)20 }21}2223const uploadAvatar = async (evt) => {24 files.value = evt.target.files25 try {26 uploading.value = true2728 if (!files.value || files.value.length === 0) {29 throw new Error('You must select an image to upload.')30 }3132 const file = files.value[0]33 const fileExt = file.name.split('.').pop()34 const fileName = `${Math.random()}.${fileExt}`35 const filePath = `${fileName}`3637 const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)3839 if (uploadError) throw uploadError4041 emit('update:path', filePath)42 emit('upload')43 } catch (error) {44 alert(error.message)45 } finally {46 uploading.value = false47 }48}4950downloadImage()5152watch(path, () => {53 if (path.value) {54 downloadImage()55 }56})57</script>5859<template>60 <div>61 <img62 v-if="src"63 :src="src"64 alt="Avatar"65 class="avatar image"66 style="width: 10em; height: 10em;"67 />68 <div v-else class="avatar no-image" :style="{ height: size, width: size }" />6970 <div style="width: 10em; position: relative;">71 <label class="button primary block" for="single">72 {{ uploading ? 'Uploading ...' : 'Upload' }}73 </label>74 <input75 style="position: absolute; visibility: hidden;"76 type="file"77 id="single"78 accept="image/*"79 @change="uploadAvatar"80 :disabled="uploading"81 />82 </div>83 </div>84</template>添加新的小部件#
然后我们可以将小部件添加到 Account 页面
1<script setup>2const supabase = useSupabaseClient()34const loading = ref(true)5const username = ref('')6const website = ref('')7const avatar_path = ref('')89loading.value = true10const user = useSupabaseUser()1112const { data } = await supabase13 .from('profiles')14 .select(`username, website, avatar_url`)15 .eq('id', user.value.id)16 .single()1718if (data) {19 username.value = data.username20 website.value = data.website21 avatar_path.value = data.avatar_url22}2324loading.value = false2526async function updateProfile() {27 try {28 loading.value = true29 const user = useSupabaseUser()3031 const updates = {32 id: user.value.id,33 username: username.value,34 website: website.value,35 avatar_url: avatar_path.value,36 updated_at: new Date(),37 }3839 const { error } = await supabase.from('profiles').upsert(updates, {40 returning: 'minimal', // Don't return the value after inserting41 })4243 if (error) throw error44 } catch (error) {45 alert(error.message)46 } finally {47 loading.value = false48 }49}5051async function signOut() {52 try {53 loading.value = true54 const { error } = await supabase.auth.signOut()55 if (error) throw error56 } catch (error) {57 alert(error.message)58 } finally {59 loading.value = false60 }61}62</script>6364<template>65 <form class="form-widget" @submit.prevent="updateProfile">66 <Avatar v-model:path="avatar_path" @upload="updateProfile" />67 <div>68 <label for="email">Email</label>69 <input id="email" type="text" :value="user.email" disabled />70 </div>71 <div>72 <label for="username">Name</label>73 <input id="username" type="text" v-model="username" />74 </div>75 <div>76 <label for="website">Website</label>77 <input id="website" type="url" v-model="website" />78 </div>7980 <div>81 <input82 type="submit"83 class="button primary block"84 :value="loading ? 'Loading ...' : 'Update'"85 :disabled="loading"86 />87 </div>8889 <div>90 <button class="button block" @click="signOut" :disabled="loading">Sign Out</button>91 </div>92 </form>93</template>就是这样! 您现在应该能够将个人资料照片上传到 Supabase Storage,并且您拥有一个功能齐全的应用程序。