入门

使用 Nuxt 3 构建用户管理应用


本教程演示如何构建一个基本的用户管理应用。该应用对用户进行身份验证和识别,将他们的个人资料信息存储在数据库中,并允许用户登录、更新他们的个人资料详细信息以及上传个人资料照片。该应用使用

Supabase User Management example

项目设置#

在开始构建之前,您需要设置数据库和 API。您可以通过在 Supabase 中启动一个新项目,然后在数据库中创建一个“schema”来完成此操作。

创建项目#

  1. 在 Supabase 控制面板中创建一个新项目
  2. 输入您的项目详细信息。
  3. 等待新的数据库启动。

设置数据库 schema#

现在设置数据库 schema。您可以使用 SQL 编辑器中的“用户管理 Starter”快速入门,或者您可以复制/粘贴下面的 SQL 并运行它。

  1. 转到控制面板中的 SQL 编辑器 页面。
  2. 点击 社区 > 快速入门 选项卡下的 用户管理 Starter
  3. 点击 运行

获取 API 详细信息#

现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。

为此,您需要从 项目的 Connect 对话框中获取项目 URL 和密钥。

阅读 API 密钥文档 以全面了解所有密钥类型及其用途。

构建应用#

让我们从头开始构建 Vue 3 应用。

初始化 Nuxt 3 应用#

我们可以使用 nuxi init 创建一个名为 nuxt-user-management 的应用

1
npx nuxi init nuxt-user-management
2
3
cd nuxt-user-management

然后让我们安装唯一的附加依赖项:Nuxt Supabase。 我们只需要将 Nuxt Supabase 作为开发依赖项导入。

1
npm install @nuxtjs/supabase --save-dev

最后,我们希望将环境变量保存在一个 .env 文件中。 我们只需要您之前复制的 API URL 和密钥 此处

1
SUPABASE_URL="YOUR_SUPABASE_URL"
2
SUPABASE_KEY="YOUR_SUPABASE_PUBLISHABLE_KEY"

这些变量将在浏览器上公开,这完全没问题,因为我们在数据库上启用了 行级别安全Nuxt Supabase 的神奇之处在于,设置环境变量是我们开始使用 Supabase 所需做的全部工作。 无需初始化 Supabase。 该库将自动处理它。

应用样式 (可选)#

一个可选步骤是更新 CSS 文件 assets/main.css 以使应用看起来不错。 您可以在 此处找到此文件的完整内容。

1
import { defineNuxtConfig } from 'nuxt'
2
3
// https://v3.nuxtjs.org/api/configuration/nuxt.config
4
export default defineNuxtConfig({
5
modules: ['@nuxtjs/supabase'],
6
css: ['@/assets/main.css'],
7
})

设置 Auth 组件#

让我们设置一个 Vue 组件来管理登录和注册。 我们将使用 Magic Links,以便用户无需使用密码即可通过电子邮件登录。

1
<script setup>
2
const supabase = useSupabaseClient()
3
4
const loading = ref(false)
5
const email = ref('')
6
7
const handleLogin = async () => {
8
try {
9
loading.value = true
10
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
11
if (error) throw error
12
alert('Check your email for the login link!')
13
} catch (error) {
14
alert(error.error_description || error.message)
15
} finally {
16
loading.value = false
17
}
18
}
19
</script>
20
21
<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
<input
31
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>
2
const supabase = useSupabaseClient()
3
4
const loading = ref(true)
5
const username = ref('')
6
const website = ref('')
7
const avatar_path = ref('')
8
9
loading.value = true
10
const user = useSupabaseUser()
11
12
const { data } = await supabase
13
.from('profiles')
14
.select(`username, website, avatar_url`)
15
.eq('id', user.value.id)
16
.single()
17
18
if (data) {
19
username.value = data.username
20
website.value = data.website
21
avatar_path.value = data.avatar_url
22
}
23
24
loading.value = false
25
26
async function updateProfile() {
27
try {
28
loading.value = true
29
const user = useSupabaseUser()
30
31
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
}
38
39
const { error } = await supabase.from('profiles').upsert(updates, {
40
returning: 'minimal', // Don't return the value after inserting
41
})
42
if (error) throw error
43
} catch (error) {
44
alert(error.message)
45
} finally {
46
loading.value = false
47
}
48
}
49
50
async function signOut() {
51
try {
52
loading.value = true
53
const { error } = await supabase.auth.signOut()
54
if (error) throw error
55
user.value = null
56
} catch (error) {
57
alert(error.message)
58
} finally {
59
loading.value = false
60
}
61
}
62
</script>
63
64
<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>
78
79
<div>
80
<input
81
type="submit"
82
class="button primary block"
83
:value="loading ? 'Loading ...' : 'Update'"
84
:disabled="loading"
85
/>
86
</div>
87
88
<div>
89
<button class="button block" @click="signOut" :disabled="loading">Sign Out</button>
90
</div>
91
</form>
92
</template>

启动!#

现在我们已经准备好所有组件,让我们更新 app.vue

1
<script setup>
2
const user = useSupabaseUser()
3
</script>
4
5
<template>
6
<div class="container" style="padding: 50px 0 100px 0">
7
<Account v-if="user" />
8
<Auth v-else />
9
</div>
10
</template>

完成此操作后,在终端窗口中运行以下命令

1
npm run dev

然后在浏览器中打开 localhost:3000,您应该会看到完成的应用。

Supabase Nuxt 3

奖励:个人资料照片#

每个 Supabase 项目都配置了 存储,用于管理大型文件,如照片和视频。

创建一个上传小部件#

让我们为用户创建一个头像,以便他们上传个人资料照片。 我们可以从创建一个新组件开始

1
<script setup>
2
const props = defineProps(['path'])
3
const { path } = toRefs(props)
4
5
const emit = defineEmits(['update:path', 'upload'])
6
7
const supabase = useSupabaseClient()
8
9
const uploading = ref(false)
10
const src = ref('')
11
const files = ref()
12
13
const downloadImage = async () => {
14
try {
15
const { data, error } = await supabase.storage.from('avatars').download(path.value)
16
if (error) throw error
17
src.value = URL.createObjectURL(data)
18
} catch (error) {
19
console.error('Error downloading image: ', error.message)
20
}
21
}
22
23
const uploadAvatar = async (evt) => {
24
files.value = evt.target.files
25
try {
26
uploading.value = true
27
28
if (!files.value || files.value.length === 0) {
29
throw new Error('You must select an image to upload.')
30
}
31
32
const file = files.value[0]
33
const fileExt = file.name.split('.').pop()
34
const fileName = `${Math.random()}.${fileExt}`
35
const filePath = `${fileName}`
36
37
const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
38
39
if (uploadError) throw uploadError
40
41
emit('update:path', filePath)
42
emit('upload')
43
} catch (error) {
44
alert(error.message)
45
} finally {
46
uploading.value = false
47
}
48
}
49
50
downloadImage()
51
52
watch(path, () => {
53
if (path.value) {
54
downloadImage()
55
}
56
})
57
</script>
58
59
<template>
60
<div>
61
<img
62
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 }" />
69
70
<div style="width: 10em; position: relative;">
71
<label class="button primary block" for="single">
72
{{ uploading ? 'Uploading ...' : 'Upload' }}
73
</label>
74
<input
75
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>
2
const supabase = useSupabaseClient()
3
4
const loading = ref(true)
5
const username = ref('')
6
const website = ref('')
7
const avatar_path = ref('')
8
9
loading.value = true
10
const user = useSupabaseUser()
11
12
const { data } = await supabase
13
.from('profiles')
14
.select(`username, website, avatar_url`)
15
.eq('id', user.value.id)
16
.single()
17
18
if (data) {
19
username.value = data.username
20
website.value = data.website
21
avatar_path.value = data.avatar_url
22
}
23
24
loading.value = false
25
26
async function updateProfile() {
27
try {
28
loading.value = true
29
const user = useSupabaseUser()
30
31
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
}
38
39
const { error } = await supabase.from('profiles').upsert(updates, {
40
returning: 'minimal', // Don't return the value after inserting
41
})
42
43
if (error) throw error
44
} catch (error) {
45
alert(error.message)
46
} finally {
47
loading.value = false
48
}
49
}
50
51
async function signOut() {
52
try {
53
loading.value = true
54
const { error } = await supabase.auth.signOut()
55
if (error) throw error
56
} catch (error) {
57
alert(error.message)
58
} finally {
59
loading.value = false
60
}
61
}
62
</script>
63
64
<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>
79
80
<div>
81
<input
82
type="submit"
83
class="button primary block"
84
:value="loading ? 'Loading ...' : 'Update'"
85
:disabled="loading"
86
/>
87
</div>
88
89
<div>
90
<button class="button block" @click="signOut" :disabled="loading">Sign Out</button>
91
</div>
92
</form>
93
</template>

就是这样! 您现在应该能够将个人资料照片上传到 Supabase Storage,并且您拥有一个功能齐全的应用程序。