使用 Vue 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 插入数据。
为此,您需要从 项目的 连接 对话框 获取项目 URL 和密钥。
API 密钥的更改
Supabase 正在更改密钥的工作方式,以提高项目安全性和开发人员体验。您可以 阅读完整的公告,但在过渡期间,您可以使用当前的 anon 和 service_role 密钥以及新的可发布密钥,格式为 sb_publishable_xxx,它将取代旧的密钥。
在大多数情况下,您可以从 项目的 连接 对话框 获取正确的密钥,但如果您需要特定的密钥,可以在 项目设置页面的 API 密钥部分 找到所有密钥
- 对于旧版密钥,从 旧版 API 密钥 选项卡中复制
anon密钥用于客户端操作,并复制service_role密钥用于服务器端操作。 - 对于新密钥,打开 API 密钥 选项卡,如果您还没有可发布密钥,请单击 创建新的 API 密钥,并复制 可发布密钥 部分中的值。
阅读 API 密钥文档 以全面了解所有密钥类型及其用途。
构建应用#
让我们从头开始构建 Vue 3 应用。
初始化 Vue 3 应用#
我们可以快速使用 Vite 与 Vue 3 模板 初始化一个名为 supabase-vue-3 的应用
1# npm 6.x2npm create vite@latest supabase-vue-3 --template vue34# npm 7+, extra double-dash is needed:5npm create vite@latest supabase-vue-3 -- --template vue67cd supabase-vue-3然后让我们安装唯一的附加依赖项:supabase-js
1npm install @supabase/supabase-js最后,我们想将环境变量保存在一个 .env 文件中。我们只需要您之前复制的 API URL 和密钥 此处。
1VITE_SUPABASE_URL=YOUR_SUPABASE_URL2VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEYAPI 凭据就绪后,创建一个 src/supabase.js 辅助文件来初始化 Supabase 客户端。这些变量在浏览器上公开,这完全没问题,因为我们在数据库上启用了 行级别安全。
1import { createClient } from '@supabase/supabase-js'23const supabaseUrl = import.meta.env.VITE_SUPABASE_URL4const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY56export const supabase = createClient(supabaseUrl, supabasePublishableKey)可选地,更新 src/style.css 以设置应用样式。
设置登录组件#
设置一个 src/components/Auth.vue 组件来管理登录和注册。我们将使用 Magic Links,以便用户无需使用密码即可通过电子邮件登录。
1<script setup>2import { ref } from 'vue'3import { supabase } from '../supabase'45const loading = ref(false)6const email = ref('')78const handleLogin = async () => {9 try {10 loading.value = true11 const { error } = await supabase.auth.signInWithOtp({12 email: email.value,13 })14 if (error) throw error15 alert('Check your email for the login link!')16 } catch (error) {17 if (error instanceof Error) {18 alert(error.message)19 }20 } finally {21 loading.value = false22 }23}24</script>2526<template>27 <form class="row flex-center flex" @submit.prevent="handleLogin">28 <div class="col-6 form-widget">29 <h1 class="header">Supabase + Vue 3</h1>30 <p class="description">Sign in via magic link with your email below</p>31 <div>32 <input class="inputField" required type="email" placeholder="Your email" v-model="email" />33 </div>34 <div>35 <input36 type="submit"37 class="button block"38 :value="loading ? 'Loading' : 'Send magic link'"39 :disabled="loading"40 />41 </div>42 </div>43 </form>44</template>账户页面#
用户登录后,我们可以允许他们编辑个人资料详细信息并管理他们的帐户。创建一个新的 src/components/Account.vue 组件来处理此操作。
1<script setup>2import { supabase } from '../supabase'3import { onMounted, ref, toRefs } from 'vue'45const props = defineProps(['session'])6const { session } = toRefs(props)78const loading = ref(true)9const username = ref('')10const website = ref('')11const avatar_url = ref('')1213onMounted(() => {14 getProfile()15})1617async function getProfile() {18 try {19 loading.value = true20 const { user } = session.value2122 const { data, error, status } = await supabase23 .from('profiles')24 .select(`username, website, avatar_url`)25 .eq('id', user.id)26 .single()2728 if (error && status !== 406) throw error2930 if (data) {31 username.value = data.username32 website.value = data.website33 avatar_url.value = data.avatar_url34 }35 } catch (error) {36 alert(error.message)37 } finally {38 loading.value = false39 }40}4142async function updateProfile() {43 try {44 loading.value = true45 const { user } = session.value4647 const updates = {48 id: user.id,49 username: username.value,50 website: website.value,51 avatar_url: avatar_url.value,52 updated_at: new Date(),53 }5455 const { error } = await supabase.from('profiles').upsert(updates)5657 if (error) throw error58 } catch (error) {59 alert(error.message)60 } finally {61 loading.value = false62 }63}6465async function signOut() {66 try {67 loading.value = true68 const { error } = await supabase.auth.signOut()69 if (error) throw error70 } catch (error) {71 alert(error.message)72 } finally {73 loading.value = false74 }75}76</script>7778<template>79 <form class="form-widget" @submit.prevent="updateProfile">80 <div>81 <label for="email">Email</label>82 <input id="email" type="text" :value="session.user.email" disabled />83 </div>84 <div>85 <label for="username">Name</label>86 <input id="username" type="text" v-model="username" />87 </div>88 <div>89 <label for="website">Website</label>90 <input id="website" type="url" v-model="website" />91 </div>9293 <div>94 <input95 type="submit"96 class="button primary block"97 :value="loading ? 'Loading ...' : 'Update'"98 :disabled="loading"99 />100 </div>101102 <div>103 <button class="button block" @click="signOut" :disabled="loading">Sign Out</button>104 </div>105 </form>106</template>启动!#
现在我们已经准备好所有组件,让我们更新 App.vue
1<script setup>2import { onMounted, ref } from 'vue'3import Account from './components/Account.vue'4import Auth from './components/Auth.vue'5import { supabase } from './supabase'67const session = ref()89onMounted(() => {10 supabase.auth.getSession().then(({ data }) => {11 session.value = data.session12 })1314 supabase.auth.onAuthStateChange((_, _session) => {15 session.value = _session16 })17})18</script>1920<template>21 <div class="container" style="padding: 50px 0 100px 0">22 <Account v-if="session" :session="session" />23 <Auth v-else />24 </div>25</template>完成此操作后,在终端窗口中运行以下命令
1npm run dev然后打开浏览器到 localhost:5173,您应该会看到完成的应用。

奖励:个人资料照片#
每个 Supabase 项目都配置了 存储,用于管理大型文件,如照片和视频。
创建一个上传小部件#
创建一个新的 src/components/Avatar.vue 组件,允许用户上传个人资料照片
1<script setup>2import { ref, toRefs, watchEffect } from 'vue'3import { supabase } from '../supabase'45const prop = defineProps(['path', 'size'])6const { path, size } = toRefs(prop)78const emit = defineEmits(['upload', 'update:path'])9const 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 = true27 if (!files.value || files.value.length === 0) {28 throw new Error('You must select an image to upload.')29 }3031 const file = files.value[0]32 const fileExt = file.name.split('.').pop()33 const filePath = `${Math.random()}.${fileExt}`3435 const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)3637 if (uploadError) throw uploadError38 emit('update:path', filePath)39 emit('upload')40 } catch (error) {41 alert(error.message)42 } finally {43 uploading.value = false44 }45}4647watchEffect(() => {48 if (path.value) downloadImage()49})50</script>5152<template>53 <div>54 <img55 v-if="src"56 :src="src"57 alt="Avatar"58 class="avatar image"59 :style="{ height: size + 'em', width: size + 'em' }"60 />61 <div v-else class="avatar no-image" :style="{ height: size + 'em', width: size + 'em' }" />6263 <div :style="{ width: size + 'em' }">64 <label class="button primary block" for="single">65 {{ uploading ? 'Uploading ...' : 'Upload' }}66 </label>67 <input68 style="visibility: hidden; position: absolute"69 type="file"70 id="single"71 accept="image/*"72 @change="uploadAvatar"73 :disabled="uploading"74 />75 </div>76 </div>77</template>添加新的小部件#
然后我们可以将小部件添加到 src/components/Account.vue 中的帐户页面
1<script>2// Import the new component3import Avatar from './Avatar.vue'4//...5const avatar_url = ref('')6//...7</script>89<template>10 <form class="form-widget" @submit.prevent="updateProfile">11 <!-- Add to body -->12 <Avatar v-model:path="avatar_url" @upload="updateProfile" size="10" />1314 <!-- Other form elements -->15 </form>16</template>此时,您已经拥有一个功能齐全的应用!