快速开始

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


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

Supabase 用户管理示例

项目设置

在开始构建之前,我们需要设置数据库和 API。这就像在 Supabase 中创建一个新项目,然后在数据库中创建一个 “模式” 一样简单。

创建项目

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

设置数据库模式

现在我们要设置数据库模式。我们可以在 SQL 编辑器中使用 “用户管理入门” 快速启动模板,或者您也可以直接复制/粘贴下面的 SQL 并自行运行。

  1. 转到仪表板中的SQL 编辑器页面。
  2. 点击 用户管理入门
  3. 点击 运行
1
2
3
4
supabase link --project-ref <project-id># 您可以从项目的仪表板 URL 获取 <project-id>:https://supabase.com/dashboard/project/<project-id>supabase db pull

获取 API 密钥

既然您已经创建了一些数据库表,就可以使用自动生成的 API 插入数据了。 我们只需要从 API 设置中获取项目 URL 和 anon 密钥。

  1. 前往仪表板中的 API 设置 页面。
  2. 在该页面找到您的项目 URLanonservice_role 密钥。

构建应用

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

初始化 Vue 3 应用

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

1
2
3
4
5
6
7
# npm 6.xnpm create vite@latest supabase-vue-3 --template vue# 使用 npm 7+ 时,需要额外添加双横线:npm create vite@latest supabase-vue-3 -- --template vuecd supabase-vue-3

接下来安装唯一的额外依赖:supabase-js

1
npm install @supabase/supabase-js

最后我们需要将环境变量保存在 .env 文件中。 只需要之前复制的 API URL 和 anon 密钥。

1
2
VITE_SUPABASE_URL=YOUR_SUPABASE_URLVITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

配置好 API 凭证后,创建一个 src/supabase.js 辅助文件来初始化 Supabase 客户端。这些变量会暴露在浏览器端,这完全没问题,因为我们的数据库已启用行级安全

1
2
3
4
5
6
import { createClient } from '@supabase/supabase-js'const supabaseUrl = import.meta.env.VITE_SUPABASE_URLconst supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEYexport const supabase = createClient(supabaseUrl, supabaseAnonKey)

可选操作:更新 src/style.css 来美化应用样式。

设置登录组件

创建一个 src/components/Auth.vue 组件来管理登录和注册功能。我们将使用 Magic Links(魔法链接)方式,让用户无需密码即可通过邮箱登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<script setup>import { ref } from 'vue'import { supabase } from '../supabase'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) { if (error instanceof Error) { alert(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 + Vue 3</h1> <p class="description">通过下方邮箱接收魔法链接登录</p> <div> <input class="inputField" required type="email" placeholder="您的邮箱" v-model="email" /> </div> <div> <input type="submit" class="button block" :value="loading ? '加载中' : '发送魔法链接'" :disabled="loading" /> </div> </div> </form></template>

账户页面

用户登录后,我们可以允许他们编辑个人资料信息和管理账户。创建一个新的 src/components/Account.vue 组件来处理这些功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<script setup>import { supabase } from '../supabase'import { onMounted, ref, toRefs } from 'vue'const props = defineProps(['session'])const { session } = toRefs(props)const loading = ref(true)const username = ref('')const website = ref('')const avatar_url = ref('')onMounted(() => { getProfile()})async function getProfile() { try { loading.value = true const { user } = session.value const { data, error, status } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.id) .single() if (error && status !== 406) throw error if (data) { username.value = data.username website.value = data.website avatar_url.value = data.avatar_url } } catch (error) { alert(error.message) } finally { loading.value = false }}async function updateProfile() { try { loading.value = true const { user } = session.value const updates = { id: user.id, username: username.value, website: website.value, avatar_url: avatar_url.value, updated_at: new Date(), } const { error } = await supabase.from('profiles').upsert(updates) 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"> <div> <label for="email">邮箱</label> <input id="email" type="text" :value="session.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 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup>import { onMounted, ref } from 'vue'import Account from './components/Account.vue'import Auth from './components/Auth.vue'import { supabase } from './supabase'const session = ref()onMounted(() => { supabase.auth.getSession().then(({ data }) => { session.value = data.session }) supabase.auth.onAuthStateChange((_, _session) => { session.value = _session })})</script><template> <div class="container" style="padding: 50px 0 100px 0"> <Account v-if="session" :session="session" /> <Auth v-else /> </div></template>

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

1
npm run dev

然后在浏览器中打开 localhost:5173,您应该能看到完整的应用。

Supabase Vue 3

额外功能:个人资料照片

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

创建上传组件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<script setup>import { ref, toRefs, watchEffect } from 'vue'import { supabase } from '../supabase'const prop = defineProps(['path', 'size'])const { path, size } = toRefs(prop)const emit = defineEmits(['upload', 'update:path'])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 filePath = `${Math.random()}.${fileExt}` 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 }}watchEffect(() => { if (path.value) downloadImage()})</script><template> <div> <img v-if="src" :src="src" alt="头像" class="avatar image" :style="{ height: size + 'em', width: size + 'em' }" /> <div v-else class="avatar no-image" :style="{ height: size + 'em', width: size + 'em' }" /> <div :style="{ width: size + 'em' }"> <label class="button primary block" for="single"> {{ uploading ? '上传中...' : '上传' }} </label> <input style="visibility: hidden; position: absolute" type="file" id="single" accept="image/*" @change="uploadAvatar" :disabled="uploading" /> </div> </div></template>

添加新组件

接下来我们可以将这个小部件添加到 src/components/Account.vue 文件中的账户页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>// 导入新组件import Avatar from './Avatar.vue'//...const avatar_url = ref('')//...</script><template> <form class="form-widget" @submit.prevent="updateProfile"> <!-- 添加到主体部分 --> <Avatar v-model:path="avatar_url" @upload="updateProfile" size="10" /> <!-- 其他表单元素 --> </form></template>

至此,您已经拥有了一个功能完整的应用程序!