使用 Nuxt 3 构建用户管理应用
本教程展示如何构建一个基本的用户管理应用程序。该应用程序对用户进行身份验证和识别,将其个人资料信息存储在数据库中,并允许用户登录、更新个人资料详细信息以及上传个人资料照片。该应用程序使用:
- Supabase数据库 - 一个Postgres数据库,用于存储用户数据,并通过行级安全性来保护数据,确保用户只能访问自己的信息。
- Supabase身份验证 - 允许用户注册和登录。
- Supabase存储 - 用户可以上传个人资料照片。
如果在学习本指南时遇到问题,请参考GitHub上的完整示例。
项目设置
在开始构建之前,我们需要设置数据库和 API。这就像在 Supabase 中创建一个新项目,然后在数据库中创建一个 “模式” 一样简单。
创建项目
- 在 Supabase 仪表板中创建一个新项目。
- 输入项目详细信息。
- 等待新数据库启动。
设置数据库模式
现在我们要设置数据库模式。我们可以在 SQL 编辑器中使用 “用户管理入门” 快速启动模板,或者您也可以直接复制/粘贴下面的 SQL 并自行运行。
获取 API 密钥
既然您已经创建了一些数据库表,就可以使用自动生成的 API 插入数据了。
我们只需要从 API 设置中获取项目 URL 和 anon
密钥。
- 前往仪表板中的 API 设置 页面。
- 在该页面找到您的项目
URL
、anon
和service_role
密钥。
构建应用
让我们从零开始构建Vue 3应用。
初始化Nuxt 3应用
使用nuxi init
创建一个名为nuxt-user-management
的应用:
123npx nuxi init nuxt-user-managementcd nuxt-user-management
然后安装唯一的额外依赖:Nuxt Supabase。我们只需要将Nuxt Supabase作为开发依赖导入。
1npm install @nuxtjs/supabase --save-dev
最后将环境变量保存在.env
文件中。
我们只需要之前复制的API URL和anon
密钥。
12SUPABASE_URL="YOUR_SUPABASE_URL"SUPABASE_KEY="YOUR_SUPABASE_ANON_KEY"
这些变量将在浏览器中公开,这完全没问题,因为我们的数据库已启用行级安全。 Nuxt Supabase最棒的地方在于,我们只需要设置环境变量就可以开始使用Supabase。 无需初始化Supabase,该库会自动处理。
应用样式(可选)
可选步骤是更新CSS文件assets/main.css
来美化应用外观。
您可以在这里找到该文件的完整内容。
1234567import { defineNuxtConfig } from 'nuxt'// https://v3.nuxtjs.org/api/configuration/nuxt.configexport default defineNuxtConfig({ modules: ['@nuxtjs/supabase'], css: ['@/assets/main.css'],})
设置认证组件
让我们设置一个Vue组件来管理登录和注册。我们将使用Magic Links(魔法链接),这样用户无需密码即可通过电子邮件登录。
123456789101112131415161718192021222324252627282930313233343536373839<script setup>const supabase = useSupabaseClient()const loading = ref(false)const email = ref('')const handleLogin = async () => { try { loading.value = true const { error } = await supabase.auth.signInWithOtp({ email: email.value }) if (error) throw error alert('请检查您的邮箱获取登录链接!') } catch (error) { alert(error.error_description || error.message) } finally { loading.value = false }}</script><template> <form class="row flex-center flex" @submit.prevent="handleLogin"> <div class="col-6 form-widget"> <h1 class="header">Supabase + Nuxt 3</h1> <p class="description">通过下方邮箱接收魔法链接登录</p> <div> <input class="inputField" type="email" placeholder="您的邮箱" v-model="email" /> </div> <div> <input type="submit" class="button block" :value="loading ? '加载中' : '发送魔法链接'" :disabled="loading" /> </div> </div> </form></template>
用户状态
要访问用户信息,请使用Supabase Nuxt模块提供的组合式API useSupabaseUser
。
账户组件
用户登录后,我们可以允许他们编辑个人资料信息和管理账户。让我们创建一个名为Account.vue
的新组件来实现这个功能。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192<script setup>const supabase = useSupabaseClient()const loading = ref(true)const username = ref('')const website = ref('')const avatar_path = ref('')loading.value = trueconst user = useSupabaseUser()const { data } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.value.id) .single()if (data) { username.value = data.username website.value = data.website avatar_path.value = data.avatar_url}loading.value = falseasync function updateProfile() { try { loading.value = true const user = useSupabaseUser() const updates = { id: user.value.id, username: username.value, website: website.value, avatar_url: avatar_path.value, updated_at: new Date(), } const { error } = await supabase.from('profiles').upsert(updates, { returning: 'minimal', // 插入后不返回值 }) if (error) throw error } catch (error) { alert(error.message) } finally { loading.value = false }}async function signOut() { try { loading.value = true const { error } = await supabase.auth.signOut() if (error) throw error user.value = null } catch (error) { alert(error.message) } finally { loading.value = false }}</script><template> <form class="form-widget" @submit.prevent="updateProfile"> <div> <label for="email">邮箱</label> <input id="email" type="text" :value="user.email" disabled /> </div> <div> <label for="username">用户名</label> <input id="username" type="text" v-model="username" /> </div> <div> <label for="website">网站</label> <input id="website" type="url" v-model="website" /> </div> <div> <input type="submit" class="button primary block" :value="loading ? '加载中...' : '更新'" :disabled="loading" /> </div> <div> <button class="button block" @click="signOut" :disabled="loading">退出登录</button> </div> </form></template>
启动应用!
现在我们已经准备好了所有组件,让我们更新 app.vue
文件:
12345678910<script setup>const user = useSupabaseUser()</script><template> <div class="container" style="padding: 50px 0 100px 0"> <Account v-if="user" /> <Auth v-else /> </div></template>
完成后,在终端窗口中运行以下命令:
1npm run dev
然后在浏览器中打开 localhost:3000,您应该能看到完整的应用程序。
额外功能:个人头像
每个 Supabase 项目都配置了存储功能,用于管理照片和视频等大型文件。
创建上传组件
让我们为用户创建一个头像组件,使他们能够上传个人资料照片。我们可以从创建一个新组件开始:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384<script setup>const props = defineProps(['path'])const { path } = toRefs(props)const emit = defineEmits(['update:path', 'upload'])const supabase = useSupabaseClient()const uploading = ref(false)const src = ref('')const files = ref()const downloadImage = async () => { try { const { data, error } = await supabase.storage.from('avatars').download(path.value) if (error) throw error src.value = URL.createObjectURL(data) } catch (error) { console.error('下载图片错误: ', error.message) }}const uploadAvatar = async (evt) => { files.value = evt.target.files try { uploading.value = true if (!files.value || files.value.length === 0) { throw new Error('请选择要上传的图片。') } const file = files.value[0] const fileExt = file.name.split('.').pop() const fileName = `${Math.random()}.${fileExt}` const filePath = `${fileName}` const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) throw uploadError emit('update:path', filePath) emit('upload') } catch (error) { alert(error.message) } finally { uploading.value = false }}downloadImage()watch(path, () => { if (path.value) { downloadImage() }})</script><template> <div> <img v-if="src" :src="src" alt="头像" class="avatar image" style="width: 10em; height: 10em;" /> <div v-else class="avatar no-image" :style="{ height: size, width: size }" /> <div style="width: 10em; position: relative;"> <label class="button primary block" for="single"> {{ uploading ? '上传中...' : '上传' }} </label> <input style="position: absolute; visibility: hidden;" type="file" id="single" accept="image/*" @change="uploadAvatar" :disabled="uploading" /> </div> </div></template>
添加新组件
然后我们可以将这个组件添加到账户页面:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293<script setup>const supabase = useSupabaseClient()const loading = ref(true)const username = ref('')const website = ref('')const avatar_path = ref('')loading.value = trueconst user = useSupabaseUser()const { data } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.value.id) .single()if (data) { username.value = data.username website.value = data.website avatar_path.value = data.avatar_url}loading.value = falseasync function updateProfile() { try { loading.value = true const user = useSupabaseUser() const updates = { id: user.value.id, username: username.value, website: website.value, avatar_url: avatar_path.value, updated_at: new Date(), } const { error } = await supabase.from('profiles').upsert(updates, { returning: 'minimal', // 插入后不返回值 }) if (error) throw error } catch (error) { alert(error.message) } finally { loading.value = false }}async function signOut() { try { loading.value = true const { error } = await supabase.auth.signOut() if (error) throw error } catch (error) { alert(error.message) } finally { loading.value = false }}</script><template> <form class="form-widget" @submit.prevent="updateProfile"> <Avatar v-model:path="avatar_path" @upload="updateProfile" /> <div> <label for="email">邮箱</label> <input id="email" type="text" :value="user.email" disabled /> </div> <div> <label for="username">姓名</label> <input id="username" type="text" v-model="username" /> </div> <div> <label for="website">网站</label> <input id="website" type="url" v-model="website" /> </div> <div> <input type="submit" class="button primary block" :value="loading ? '加载中...' : '更新'" :disabled="loading" /> </div> <div> <button class="button block" @click="signOut" :disabled="loading">退出登录</button> </div> </form></template>
就是这样!现在您应该能够将个人资料照片上传到 Supabase 存储,并且拥有一个功能完整的应用程序。