入门

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


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

Supabase User Management example

项目设置#

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

创建项目#

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

设置数据库 schema#

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

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

获取 API 详细信息#

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

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

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

构建应用#

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

初始化 Vue 3 应用#

我们可以快速使用 Vite 与 Vue 3 模板 初始化一个名为 supabase-vue-3 的应用

1
# npm 6.x
2
npm create vite@latest supabase-vue-3 --template vue
3
4
# npm 7+, extra double-dash is needed:
5
npm create vite@latest supabase-vue-3 -- --template vue
6
7
cd supabase-vue-3

然后让我们安装唯一的附加依赖项:supabase-js

1
npm install @supabase/supabase-js

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

1
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
2
VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY

API 凭据就绪后,创建一个 src/supabase.js 辅助文件来初始化 Supabase 客户端。这些变量在浏览器上公开,这完全没问题,因为我们在数据库上启用了 行级别安全

1
import { createClient } from '@supabase/supabase-js'
2
3
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
4
const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
5
6
export const supabase = createClient(supabaseUrl, supabasePublishableKey)

可选地,更新 src/style.css 以设置应用样式。

设置登录组件#

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

1
<script setup>
2
import { ref } from 'vue'
3
import { supabase } from '../supabase'
4
5
const loading = ref(false)
6
const email = ref('')
7
8
const handleLogin = async () => {
9
try {
10
loading.value = true
11
const { error } = await supabase.auth.signInWithOtp({
12
email: email.value,
13
})
14
if (error) throw error
15
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 = false
22
}
23
}
24
</script>
25
26
<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
<input
36
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>
2
import { supabase } from '../supabase'
3
import { onMounted, ref, toRefs } from 'vue'
4
5
const props = defineProps(['session'])
6
const { session } = toRefs(props)
7
8
const loading = ref(true)
9
const username = ref('')
10
const website = ref('')
11
const avatar_url = ref('')
12
13
onMounted(() => {
14
getProfile()
15
})
16
17
async function getProfile() {
18
try {
19
loading.value = true
20
const { user } = session.value
21
22
const { data, error, status } = await supabase
23
.from('profiles')
24
.select(`username, website, avatar_url`)
25
.eq('id', user.id)
26
.single()
27
28
if (error && status !== 406) throw error
29
30
if (data) {
31
username.value = data.username
32
website.value = data.website
33
avatar_url.value = data.avatar_url
34
}
35
} catch (error) {
36
alert(error.message)
37
} finally {
38
loading.value = false
39
}
40
}
41
42
async function updateProfile() {
43
try {
44
loading.value = true
45
const { user } = session.value
46
47
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
}
54
55
const { error } = await supabase.from('profiles').upsert(updates)
56
57
if (error) throw error
58
} catch (error) {
59
alert(error.message)
60
} finally {
61
loading.value = false
62
}
63
}
64
65
async function signOut() {
66
try {
67
loading.value = true
68
const { error } = await supabase.auth.signOut()
69
if (error) throw error
70
} catch (error) {
71
alert(error.message)
72
} finally {
73
loading.value = false
74
}
75
}
76
</script>
77
78
<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>
92
93
<div>
94
<input
95
type="submit"
96
class="button primary block"
97
:value="loading ? 'Loading ...' : 'Update'"
98
:disabled="loading"
99
/>
100
</div>
101
102
<div>
103
<button class="button block" @click="signOut" :disabled="loading">Sign Out</button>
104
</div>
105
</form>
106
</template>

启动!#

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

1
<script setup>
2
import { onMounted, ref } from 'vue'
3
import Account from './components/Account.vue'
4
import Auth from './components/Auth.vue'
5
import { supabase } from './supabase'
6
7
const session = ref()
8
9
onMounted(() => {
10
supabase.auth.getSession().then(({ data }) => {
11
session.value = data.session
12
})
13
14
supabase.auth.onAuthStateChange((_, _session) => {
15
session.value = _session
16
})
17
})
18
</script>
19
20
<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>

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

1
npm run dev

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

Supabase Vue 3

奖励:个人资料照片#

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

创建一个上传小部件#

创建一个新的 src/components/Avatar.vue 组件,允许用户上传个人资料照片

1
<script setup>
2
import { ref, toRefs, watchEffect } from 'vue'
3
import { supabase } from '../supabase'
4
5
const prop = defineProps(['path', 'size'])
6
const { path, size } = toRefs(prop)
7
8
const emit = defineEmits(['upload', 'update:path'])
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
if (!files.value || files.value.length === 0) {
28
throw new Error('You must select an image to upload.')
29
}
30
31
const file = files.value[0]
32
const fileExt = file.name.split('.').pop()
33
const filePath = `${Math.random()}.${fileExt}`
34
35
const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
36
37
if (uploadError) throw uploadError
38
emit('update:path', filePath)
39
emit('upload')
40
} catch (error) {
41
alert(error.message)
42
} finally {
43
uploading.value = false
44
}
45
}
46
47
watchEffect(() => {
48
if (path.value) downloadImage()
49
})
50
</script>
51
52
<template>
53
<div>
54
<img
55
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' }" />
62
63
<div :style="{ width: size + 'em' }">
64
<label class="button primary block" for="single">
65
{{ uploading ? 'Uploading ...' : 'Upload' }}
66
</label>
67
<input
68
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 component
3
import Avatar from './Avatar.vue'
4
//...
5
const avatar_url = ref('')
6
//...
7
</script>
8
9
<template>
10
<form class="form-widget" @submit.prevent="updateProfile">
11
<!-- Add to body -->
12
<Avatar v-model:path="avatar_url" @upload="updateProfile" size="10" />
13
14
<!-- Other form elements -->
15
</form>
16
</template>

此时,您已经拥有一个功能齐全的应用!