使用 Phoenix LiveView 构建 Instagram (2)
使用PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的Instagram Web应用程序
在第 1 部分中,我们已完成所有设置并准备好基本布局,让我们开始处理用户设置。您可以赶上Instagram 克隆 GitHub Repo。
让我们首先创建路由,打开lib/instagram_clone_web/router.ex
并在范围下添加以下 2 条路由:require_authenticated_user
:
scope "/", InstagramCloneWeb do pipe_through [:browser, :require_authenticated_user] get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email live "/accounts/edit", UserLive.Settings live "/accounts/password/change", UserLive.PassSettings end
然后我们需要创建这些 liveview 文件,在该文件夹内创建一个名为user_live
under 的文件夹lib/instagram_clone_web/live
,添加以下 4 个文件:
lib/instagram_clone_web/live/user_live/settings.ex
lib/instagram_clone_web/live/user_live/settings.html.leex
lib/instagram_clone_web/live/user_live/pass_settings.ex
lib/instagram_clone_web/live/user_live/pass_settings.html.leex
在我们的导航标题中,我们需要链接到该新路线,lib/instagram_clone_web/live/header_nav_component.html.leex在第 60 行打开,将以下内容添加到Settings live_patch to
:
<%= live_patch to: Routes.live_path(@socket, InstagramCloneWeb.UserLive.Settings) do %> <li class="py-2 px-4 hover:bg-gray-50">Settings</li> <% end %>
现在,当我们访问该链接时,我们应该会出现错误,因为文件是空的,因此打开lib/instagram_clone_web/live/user_live/settings.ex
并添加以下内容:
defmodule InstagramCloneWeb.UserLive.Settings do use InstagramCloneWeb, :live_view @impl true def mount(_params, session, socket) do socket = assign_defaults(session, socket) {:ok, socket} end end
现在我们应该有一个空白页面,只有顶部导航栏,所以让我们开始工作吧。
我们将需要Accounts
和User
上下文,我们将为它们添加别名并分配变更集,我们的文件应如下所示:
defmodule InstagramCloneWeb.UserLive.Settings do use InstagramCloneWeb, :live_view alias InstagramClone.Accounts alias InstagramClone.Accounts.User @impl true def mount(_params, session, socket) do socket = assign_defaults(session, socket) changeset = Accounts.change_user(socket.assigns.current_user) {:ok, socket |> assign(changeset: changeset)} end end
我们需要将change_user()
函数添加到Accounts
上下文中,打开lib/instagram_clone/accounts.ex
并在change_user_registration()
函数下面添加以下内容:
... def change_user(user, attrs \\ %{}) do User.registration_changeset(user, attrs, register_user: false) end ...
打开lib/instagram_clone_web/live/user_live/settings.html.leex
并将表单添加到模板中:
<section class="border-2 flex" x-data="{username: '<%= @current_user.username %>'}"> <div class="w-full py-8"> <!-- Profile Photo --> <div class="flex items-center"> <div class="w-1/3"> <%= img_tag @current_user.avatar_url, class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %> </div> <div class="w-full pl-8"> <h1 x-text="username" class="leading-none font-semibold text-lg truncate text-gray-500"></h1> </div> </div> <!-- END PROFILE PHOTO --> <%= f = form_for @changeset, "#", phx_change: "validate", phx_submit: "save", class: "space-y-8 md:space-y-10" %> <div class="flex items-center"> <%= label f, :full_name, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= text_input f, :full_name, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %> <%= error_tag f, :full_name, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <%= label f, :username, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= text_input f, :username, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", x_model: "username", autocomplete: "off" %> <%= error_tag f, :username, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <%= label f, :website, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= text_input f, :website, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %> <%= error_tag f, :website, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <%= label f, :bio, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= textarea f, :bio, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", rows: 3, autocomplete: "off" %> <%= error_tag f, :bio, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <%= label f, :email, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= email_input f, :email, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 shadow-sm focus:ring-transparent focus:border-black", autocomplete: "off" %> <%= error_tag f, :email, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <label class="block w-1/3 font-semibold text-right"></label> <div class="w-full pl-8 pr-20"> <%= submit "Submit", phx_disable_with: "Saving...", class: "w-16 py-1 px-1 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %> </div> </div> </form> </div> </section>
我们添加了表单的基本布局,当您使用 AlpineJs 输入用户名时,用户名标题会更新。现在我们需要将validate()
和save()
函数添加到我们的lib/instagram_clone_web/live/user_live/settings.ex
实时查看文档中,但我们首先将我们分配:page_title
给我们的挂载函数:
@impl true def mount(_params, session, socket) do socket = assign_defaults(session, socket) changeset = Accounts.change_user(socket.assigns.current_user) {:ok, socket |> assign(changeset: changeset) |> assign(page_title: "Edit Profile")} #This was added end
然后打开lib/instagram_clone_web/templates/layout/root.html.leex
并更新页面标题后缀:
<%= live_title_tag assigns[:page_title] || "InstagramClone", suffix: " · InstagramClone" %>
现在让我们将处理表单的函数添加到我们的lib/instagram_clone_web/live/user_live/settings.ex
:
@impl true def handle_event("validate", %{"user" => user_params}, socket) do changeset = socket.assigns.current_user |> Accounts.change_user(user_params) |> Map.put(:action, :validate) {:noreply, socket |> assign(changeset: changeset)} end @impl true def handle_event("save", %{"user" => user_params}, socket) do case Accounts.update_user(socket.assigns.current_user, user_params) do {:ok, _user} -> {:noreply, socket |> put_flash(:info, "User updated successfully") |> push_redirect(to: Routes.live_path(socket, InstagramWeb.UserLive.Settings))} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, :changeset, changeset)} end end
现在我们需要将update_user()
函数添加到Accounts
上下文中:
... def update_user(user, attrs) do user |> User.registration_changeset(attrs, register_user: false) |> Repo.update() end ...
我们对用户名的唯一约束不起作用,因为我们没有在迁移中添加唯一索引,所以我们现在就这样做。在我们的终端中,让我们生成一个迁移$ mix ecto.gen.migration add_users_unique_username_index
,然后打开生成的迁移priv/repo/migrations/20210414220125_add_users_unique_username_index.exs
并添加以下内容:
defmodule InstagramClone.Repo.Migrations.AddUsersUniqueUsernameIndex do use Ecto.Migration def change do create unique_index(:users, [:username]) end end
然后返回我们的终端并运行迁移$ mix ecto.migrate
现在让我们更新我们的注册变更lib/instagram_clone/accounts/user.ex
集unsafe_validate_unique(:username, InstagramClone.Repo)
... def registration_changeset(user, attrs, opts \\ []) do user |> cast(attrs, [:email, :password, :username, :full_name, :avatar_url, :bio, :website]) |> validate_required([:username, :full_name]) |> validate_length(:username, min: 5, max: 30) |> validate_format(:username, ~r/^[a-zA-Z0-9_.-]*$/, message: "Please use letters and numbers without space(only characters allowed _ . -)") |> unique_constraint(:username) |> unsafe_validate_unique(:username, InstagramClone.Repo) # --> This was added |> validate_length(:full_name, min: 4, max: 30) |> validate_email() |> validate_password(opts) end ...
:timer.sleep(9000)
另外,在测试时,我意识到我在尝试延迟实时验证时犯了一个错误,lib/instagram_clone_web/live/page_live.ex
所以让我们从validate()
函数中删除该行,因为它会与表单产生冲突:
@impl true def handle_event("validate", %{"user" => user_params}, socket) do changeset = %User{} |> User.registration_changeset(user_params) |> Map.put(:action, :validate) #:timer.sleep(9000) <-- REMOVE THIS LINE {:noreply, socket |> assign(changeset: changeset)} end
完成后,我们应该能够毫无问题地编辑配置文件,所以现在让我们开始上传头像文件。
头像上传
打开lib/instagram_clone_web/live/user_live/settings.ex
并允许在实时视图中上传,新的更新文件应如下所示:
defmodule InstagramCloneWeb.UserLive.Settings do use InstagramCloneWeb, :live_view alias InstagramClone.Accounts alias InstagramClone.Accounts.User #Files extensions accepted to be uploaded @extension_whitelist ~w(.jpg .jpeg .png) @impl true def mount(_params, session, socket) do socket = assign_defaults(session, socket) changeset = Accounts.change_user(socket.assigns.current_user) {:ok, socket |> assign(changeset: changeset) |> assign(page_title: "Edit Profile") |> allow_upload(:avatar_url, accept: @extension_whitelist, max_file_size: 9_000_000, progress: &handle_progress/3,#Function that will handle automatic uploads auto_upload: true)} end @impl true def handle_event("validate", %{"user" => user_params}, socket) do changeset = socket.assigns.current_user |> Accounts.change_user(user_params) |> Map.put(:action, :validate) {:noreply, socket |> assign(changeset: changeset)} end # Updates the socket when the upload form changes, triguers handle_progress() def handle_event("upload_avatar", _params, socket) do {:noreply, socket} end @impl true def handle_event("save", %{"user" => user_params}, socket) do case Accounts.update_user(socket.assigns.current_user, user_params) do {:ok, _user} -> {:noreply, socket |> put_flash(:info, "User updated successfully") |> push_redirect(to: Routes.live_path(socket, InstagramCloneWeb.UserLive.Settings))} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, :changeset, changeset)} end end # This will handle the upload defp handle_progress(:avatar_url, entry, socket) do end end
打开lib/instagram_clone_web/live/user_live/settings.html.leex
并在用户名标题下方添加上传表单,并将该表单@uploads
分配给我们的套接字:
<section class="border-2 flex" x-data="{username: '<%= @current_user.username %>'}"> <div class="w-full py-8"> <%= for {_ref, err} <- @uploads.avatar_url.errors do %> <p class="text-red-500 w-full text-center"> <%= Phoenix.Naming.humanize(err) %> </p> <% end %> <!-- Profile Photo --> <div class="flex items-center"> <div class="w-1/3"> <%= img_tag @current_user.avatar_url, class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %> </div> <div class="w-full pl-8"> <h1 x-text="username" class="leading-none font-semibold text-lg truncate text-gray-500"></h1> <!-- THIS WAS ADDED --> <div class="relative"> <%= form_for @changeset, "#", phx_change: "upload_avatar" %> <%= live_file_input @uploads.avatar_url, class: "cursor-pointer relative block opacity-0 z-40 -left-24" %> <div class="text-center absolute top-0 left-0 m-auto"> <span class="font-semibold text-sm text-light-blue-500"> Change Profile Photo </span> </div> </form> </div> <!-- THIS WAS ADDED END --> </div> </div> <!-- END PROFILE PHOTO --> <%= f = form_for @changeset, "#", phx_change: "validate", phx_submit: "save", class: "space-y-8 md:space-y-10" %> <div class="flex items-center"> <%= label f, :full_name, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= text_input f, :full_name, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %> <%= error_tag f, :full_name, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <%= label f, :username, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= text_input f, :username, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", x_model: "username", autocomplete: "off" %> <%= error_tag f, :username, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <%= label f, :website, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= text_input f, :website, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", autocomplete: "off" %> <%= error_tag f, :website, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <%= label f, :bio, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= textarea f, :bio, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black", rows: 3, autocomplete: "off" %> <%= error_tag f, :bio, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <%= label f, :email, class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= email_input f, :email, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 shadow-sm focus:ring-transparent focus:border-black", autocomplete: "off" %> <%= error_tag f, :email, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <label class="block w-1/3 font-semibold text-right"></label> <div class="w-full pl-8 pr-20"> <%= submit "Submit", phx_disable_with: "Saving...", class: "w-16 py-1 px-1 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %> </div> </div> </form> </div> </section>
现在让我们创建一个模块来帮助我们处理头像上传。在下面lib/instagram_clone_web/live
添加一个名为 的文件夹uploaders
,并在该文件夹内添加一个名为 的文件avatar.ex
。我们将调整头像的大小,因此让我们添加 Mogrify 依赖项来处理它,确保安装了 ImageMagick,打开mix.exs
并添加到我们的项目依赖项{:mogrify, "~> 0.8.0"}
,然后在我们的终端中$ mix deps.get && mix deps.compile
。
现在打开lib/instagram_clone_web/live/uploaders/avatar.ex
并添加以下内容:
defmodule InstagramClone.Uploaders.Avatar do alias InstagramCloneWeb.Router.Helpers, as: Routes # We are going to upload locally so this would be the name of the folder @upload_directory_name "uploads" @upload_directory_path "priv/static/uploads" # Returns the extensions associated with a given MIME type. defp ext(entry) do [ext | _] = MIME.extensions(entry.client_type) ext end # Returns the url path def get_avatar_url(socket, entry) do Routes.static_path(socket, "/#{@upload_directory_name}/#{entry.uuid}.#{ext(entry)}") end def update(socket, old_url, entry) do # Creates the upload directry path if not exists if !File.exists?(@upload_directory_path), do: File.mkdir!(@upload_directory_path) # Consumes an individual uploaded entry Phoenix.LiveView.consume_uploaded_entry(socket, entry, fn %{} = meta -> # Destination paths for avatar thumbs dest = Path.join(@upload_directory_path, "#{entry.uuid}.#{ext(entry)}") dest_thumb = Path.join(@upload_directory_path, "thumb_#{entry.uuid}.#{ext(entry)}") # meta.path is the temporary file path mogrify_thumbnail(meta.path, dest, 300) mogrify_thumbnail(meta.path, dest_thumb, 150) # Removes Old Urls Paths rm_file(old_url) old_url |> get_thumb() |> rm_file() end) :ok end def get_thumb(avatar_url) do file_name = String.replace_leading(avatar_url, "/uploads/", "") ["/#{@upload_directory_name}", "thumb_#{file_name}"] |> Path.join() end def rm_file(old_avatar_url) do url = String.replace_leading(old_avatar_url, "/uploads/", "") path = [@upload_directory_path, url] |> Path.join() if File.exists?(path), do: File.rm!(path) end # Resize the file with a given path, destination, and size defp mogrify_thumbnail(src_path, dst_path, size) do try do Mogrify.open(src_path) |> Mogrify.resize_to_limit("#{size}x#{size}") |> Mogrify.save(path: dst_path) rescue File.Error -> {:error, :invalid_src_path} error -> {:error, error} else _image -> {:ok, dst_path} end end end
在文件顶部打开lib/instagram_clone_web/live/user_live/settings.ex
新创建的模块的别名,并使用以下内容更新我们的函数:Avataralias InstagramClone.Uploaders.Avatarhandle_progress()
defp handle_progress(:avatar_url, entry, socket) do # If file is already uploaded to tmp folder if entry.done? do avatar_url = Avatar.get_avatar_url(socket, entry) user_params = %{"avatar_url" => avatar_url} case Accounts.update_user(socket.assigns.current_user, user_params) do {:ok, _user} -> Avatar.update(socket, socket.assigns.current_user.avatar_url, entry) @doc """ We have to update the current user and assign it back to the socket to get the header nav thumbnail automatically updated """ current_user = Accounts.get_user!(socket.assigns.current_user.id) {:noreply, socket |> put_flash(:info, "Avatar updated successfully") |> assign(current_user: current_user)} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, :changeset, changeset)} end else {:noreply, socket} end end
最后,我们需要提供上传目录中将要创建的文件,打开lib/instagram_clone_web/endpoint.ex
并更新静态插件中的第 27 行:
only: ~w(css fonts images js favicon.ico robots.txt uploads)
现在一切都应该工作得很好,但我们正在上传缩略图,所以让我们在模板中使用它,打开lib/instagram_clone_web/live/header_nav_component.html.leex
并更新第 43 行以使用我们的缩略图 URL:
<%= img_tag InstagramClone.Uploaders.Avatar.get_thumb(@current_user.avatar_url), class: "w-full h-full object-cover object-center" %>
同时打开lib/instagram_clone_web/live/user_live/settings.html.leex
并更新第 7 行以使用我们的缩略图:
<%= img_tag Avatar.get_thumb(@current_user.avatar_url), class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %>
密码更改设置
现在剩下的唯一一件事就是更改密码,我们将需要一个侧面导航栏,因此让我们创建一个组件来处理它,因为它将与密码更改 LiveView 共享。在下面lib/instagram_clone_web/live/user_live
添加以下2个文件:
lib/instagram_clone_web/live/user_live/settings_sidebar_component.ex
lib/instagram_clone_web/live/user_live/settings_sidebar_component.html.leex
添加lib/instagram_clone_web/live/user_live/settings_sidebar_component.ex
以下内容:
defmodule InstagramCloneWeb.UserLive.SettingsSidebarComponent do use InstagramCloneWeb, :live_component end
添加lib/instagram_clone_web/live/user_live/settings_sidebar_component.html.leex
以下内容:
<div class="w-1/4 border-r-2"> <ul> <%= live_patch content_tag(:li, "Edit Profile", class: "p-4 #{selected_link?(@current_uri_path, @settings_path)}"), to: @settings_path %> <%= live_patch content_tag(:li, "Change Password", class: "p-4 #{selected_link?(@current_uri_path, @pass_settings_path)}"), to: @pass_settings_path %> </ul> </div>
render_helpers.ex
在 下创建一个名为lib/instagram_clone_web/live
. 打开lib/instagram_clone_web/live/render_helpers.ex
并执行以下操作::
defmodule InstagramCloneWeb.RenderHelpers do def selected_link?(current_uri, menu_link) when current_uri == menu_link do "border-l-2 border-black -ml-0.5 text-gray-900 font-semibold" end def selected_link?(_current_uri, _menu_link) do "hover:border-l-2 -ml-0.5 hover:border-gray-300 hover:bg-gray-50" end end
这些功能将帮助我们为侧面导航栏中的链接获取正确的样式。现在我们需要在模板中提供这些函数,打开lib/instagram_clone_web.ex
视图助手函数并将其更新为以下内容:
defp view_helpers do quote do # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML # Import LiveView helpers (live_render, live_component, live_patch, etc) import Phoenix.LiveView.Helpers # Import basic rendering functionality (render, render_layout, etc) import Phoenix.View import InstagramCloneWeb.ErrorHelpers import InstagramCloneWeb.Gettext import InstagramCloneWeb.RenderHelpers # <-- THIS LINE WAS ADDED alias InstagramCloneWeb.Router.Helpers, as: Routes end end
让我们将路径分配给套接字,打开lib/instagram_clone_web/live/user_live/settings.ex
并在安装中添加以下内容:
def mount(_params, session, socket) do socket = assign_defaults(session, socket) changeset = Accounts.change_user(socket.assigns.current_user) # THIS WAS ADDED settings_path = Routes.live_path(socket, __MODULE__) pass_settings_path = Routes.live_path(socket, InstagramCloneWeb.UserLive.PassSettings) {:ok, socket |> assign(changeset: changeset) |> assign(page_title: "Edit Profile") |> assign(settings_path: settings_path, pass_settings_path: pass_settings_path)# <-- THIS WAS ADDED |> allow_upload(:avatar_url, accept: @extension_whitelist, max_file_size: 9_000_000, progress: &handle_progress/3, auto_upload: true)} end
lib/instagram_clone_web/live/user_live/settings.html.leex
在标签开头下方顶部的部分标签内打开,让我们插入我们的组件:
<%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent, settings_path: @settings_path, pass_settings_path: @pass_settings_path, current_uri_path: @current_uri_path %>
打开lib/instagram_clone_web/live/user_live/pass_settings.ex
添加以下内容:
defmodule InstagramCloneWeb.UserLive.PassSettings do use InstagramCloneWeb, :live_view def mount(_params, session, socket) do socket = assign_defaults(session, socket) settings_path = Routes.live_path(socket, InstagramCloneWeb.UserLive.Settings) pass_settings_path = Routes.live_path(socket, __MODULE__) {:ok, socket |> assign(settings_path: settings_path, pass_settings_path: pass_settings_path)} end end
然后打开lib/instagram_clone_web/live/user_live/pass_settings.html.leex
添加以下内容:
<section class="border-2 flex"> <%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent, settings_path: @settings_path, pass_settings_path: @pass_settings_path, current_uri_path: @current_uri_path %> </section>
让我们将表单添加到lib/instagram_clone_web/live/user_live/pass_settings.html.leex
:
<section class="border-2 flex"> <%= live_component @socket, InstagramCloneWeb.UserLive.SettingsSidebarComponent, settings_path: @settings_path, pass_settings_path: @pass_settings_path, current_uri_path: @current_uri_path %> <div class="w-full py-5"> <!-- Profile Photo --> <div class="flex items-center"> <div class="w-1/3"> <%= img_tag Avatar.get_thumb(@current_user.avatar_url), class: "ml-auto w-10 h-10 rounded-full object-cover object-center" %> </div> <div class="w-full pl-8"> <h1 class="font-semibold text-xl truncate text-gray-600"><%= @current_user.username %></h1> </div> </div> <!-- End Profile Photo --> <%= f = form_for @changeset, "#", phx_submit: "save", class: "space-y-5 md:space-y-8" %> <div class="md:flex items-center"> <%= label f, :old_password, "Old Password", class: "w-1/3 text-right font-semibold", for: "current_password_for_password" %> <div class="w-full pl-8 pr-20"> <%= password_input f, :current_password, required: true, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black" %> <%= error_tag f, :current_password, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <%= label f, :password, "New Password", class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= password_input f, :password, required: true, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black" %> <%= error_tag f, :password, class: "text-red-700 text-sm block" %> </div> </div> <div class="md:flex items-center"> <%= label f, :password_confirmation, "Confirm New Password", class: "w-1/3 text-right font-semibold" %> <div class="w-full pl-8 pr-20"> <%= password_input f, :password_confirmation, required: true, class: "w-4/6 rounded p-1 text-semibold text-gray-600 border-gray-300 focus:ring-transparent focus:border-black" %> <%= error_tag f, :password_confirmation, class: "text-red-700 text-sm block" %> </div> </div> <div class="flex items-center"> <label class="w-1/3"></label> <div class="w-full pl-8 pr-20"> <%= submit "Change Password", phx_disable_with: "Saving...", class: "py-1 px-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-light-blue-600 bg-light-blue-500 cursor-pointer" %> </div> </div> <div class="flex items-center"> <label class="w-1/3"></label> <div class="w-full pl-8 pr-20 text-right"> <%= link "Forgot Password?", to: Routes.user_reset_password_path(@socket, :new), class: "font-semibold text-xs hover:text-light-blue-600 text-light-blue-500 cursor-pointer hover:underline" %> </div> </div> </form> </div> </section>
最后更新lib/instagram_clone_web/live/user_live/pass_settings.ex
如下:
defmodule InstagramCloneWeb.UserLive.PassSettings do use InstagramCloneWeb, :live_view alias InstagramClone.Accounts alias InstagramClone.Accounts.User alias InstagramClone.Uploaders.Avatar @impl true def mount(_params, session, socket) do socket = assign_defaults(session, socket) settings_path = Routes.live_path(socket, InstagramCloneWeb.UserLive.Settings) pass_settings_path = Routes.live_path(socket, __MODULE__) user = socket.assigns.current_user {:ok, socket |> assign(settings_path: settings_path, pass_settings_path: pass_settings_path) |> assign(:page_title, "Change Password") |> assign(changeset: Accounts.change_user_password(user))} end @impl true def handle_event("save", %{"user" => params}, socket) do %{"current_password" => password} = params case Accounts.update_user_password(socket.assigns.current_user, password, params) do {:ok, _user} -> {:noreply, socket |> put_flash(:info, "Password updated successfully.") |> push_redirect(to: socket.assigns.pass_settings_path)} {:error, changeset} -> {:noreply, assign(socket, :changeset, changeset)} end end end
转到lib/instagram_clone/accounts.ex
第 208 行,并更新update_user_password()
为以下内容:
def update_user_password(user, password, attrs) do user |> User.password_changeset(attrs) |> User.validate_current_password(password) |> Repo.update() end
在下一部分中,我们将处理用户的个人资料。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~