布局与组件 #
一、布局系统 #
1.1 布局概述 #
布局是页面的框架模板,定义了公共的页面结构,如头部、导航、页脚等。
text
布局模板
├── HTML头部
├── 导航栏
├── 主内容区 (@inner_block)
└── 页脚
1.2 默认布局 #
elixir
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
def app(assigns) do
~H"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><%= assigns[:page_title] || "Hello" %></title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-gray-100">
<header>
<.navigation />
</header>
<main class="container mx-auto">
<%= @inner_content %>
</main>
<footer>
<.footer />
</footer>
</body>
</html>
"""
end
end
1.3 根布局 #
elixir
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
def root(assigns) do
~H"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "Hello" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body>
<%= @inner_content %>
</body>
</html>
"""
end
end
二、布局使用 #
2.1 默认布局配置 #
elixir
pipeline :browser do
plug :put_root_layout, {HelloWeb.Layouts, :root}
end
2.2 控制器指定布局 #
elixir
defmodule HelloWeb.AdminController do
use HelloWeb, :controller
plug :put_layout, {HelloWeb.Layouts, :admin}
def index(conn, _params) do
render(conn, :index)
end
end
2.3 动作级别布局 #
elixir
def show(conn, %{"id" => id}) do
user = Hello.Accounts.get_user!(id)
render(conn, :show, user: user, layout: {HelloWeb.Layouts, :print})
end
2.4 禁用布局 #
elixir
def pdf(conn, %{"id" => id}) do
user = Hello.Accounts.get_user!(id)
render(conn, :show, user: user, layout: false)
end
三、多布局系统 #
3.1 应用布局 #
elixir
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
def app(assigns) do
~H"""
<.root>
<header>
<.navbar />
</header>
<main>
<.flash_messages flash={@flash} />
<%= @inner_content %>
</main>
<footer>
<.footer />
</footer>
</.root>
"""
end
end
3.2 管理后台布局 #
elixir
def admin(assigns) do
~H"""
<.root>
<div class="admin-layout">
<aside class="sidebar">
<.admin_menu />
</aside>
<main class="content">
<.admin_header />
<.flash_messages flash={@flash} />
<%= @inner_content %>
</main>
</div>
</.root>
"""
end
3.3 认证布局 #
elixir
def auth(assigns) do
~H"""
<.root>
<div class="auth-layout">
<div class="auth-container">
<.logo />
<.flash_messages flash={@flash} />
<%= @inner_content %>
</div>
</div>
</.root>
"""
end
四、组件基础 #
4.1 函数组件 #
elixir
defmodule HelloWeb.CoreComponents do
use Phoenix.Component
attr :title, :string, required: true
attr :class, :string, default: ""
def card(assigns) do
~H"""
<div class={"card #{@class}"}>
<div class="card-header">
<h3><%= @title %></h3>
</div>
<div class="card-body">
<%= render_slot(@inner_block) %>
</div>
</div>
"""
end
end
4.2 使用组件 #
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
4.3 组件属性 #
elixir
attr :type, :atom, values: [:primary, :secondary, :danger, :success], default: :primary
attr :size, :atom, values: [:sm, :md, :lg], default: :md
attr :disabled, :boolean, default: false
attr :loading, :boolean, default: false
def button(assigns) do
~H"""
<button
class={button_classes(@type, @size)}
disabled={@disabled or @loading}
>
<%= if @loading do %>
<.spinner />
<% end %>
<%= render_slot(@inner_block) %>
</button>
"""
end
defp button_classes(type, size) do
base = "btn"
type_class = "btn-#{type}"
size_class = "btn-#{size}"
[base, type_class, size_class]
end
五、插槽系统 #
5.1 默认插槽 #
elixir
attr :title, :string, required: true
def panel(assigns) do
~H"""
<div class="panel">
<div class="panel-header">
<h3><%= @title %></h3>
</div>
<div class="panel-body">
<%= render_slot(@inner_block) %>
</div>
</div>
"""
end
使用:
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
5.2 命名插槽 #
elixir
slot :header
slot :footer
def card(assigns) do
~H"""
<div class="card">
<%= if @header do %>
<div class="card-header">
<%= render_slot(@header) %>
</div>
<% end %>
<div class="card-body">
<%= render_slot(@inner_block) %>
</div>
<%= if @footer do %>
<div class="card-footer">
<%= render_slot(@footer) %>
</div>
<% end %>
</div>
"""
end
使用:
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
5.3 插槽参数 #
elixir
attr :items, :list, required: true
slot :item, required: true
slot :empty
def list(assigns) do
~H"""
<%= if Enum.empty?(@items) do %>
<%= if @empty do %>
<%= render_slot(@empty) %>
<% else %>
<p>No items</p>
<% end %>
<% else %>
<ul>
<%= for item <- @items do %>
<li><%= render_slot(@item, item) %></li>
<% end %>
</ul>
<% end %>
"""
end
使用:
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
六、常用组件 #
6.1 导航组件 #
elixir
attr :current_user, :map, default: nil
def navbar(assigns) do
~H"""
<nav class="navbar">
<div class="navbar-brand">
<.link navigate={~p"/"} class="logo">
MyApp
</.link>
</div>
<div class="navbar-menu">
<.link navigate={~p"/posts"} class="navbar-item">
Posts
</.link>
<.link navigate={~p"/about"} class="navbar-item">
About
</.link>
</div>
<div class="navbar-end">
<%= if @current_user do %>
<.dropdown>
<:trigger>
<span><%= @current_user.email %></span>
</:trigger>
<:content>
<.link navigate={~p"/settings"}>Settings</.link>
<.link href={~p"/logout"} method="delete">Logout</.link>
</:content>
</.dropdown>
<% else %>
<.link navigate={~p"/login"} class="btn btn-primary">
Login
</.link>
<% end %>
</div>
</nav>
"""
end
6.2 Flash消息组件 #
elixir
attr :flash, :map, required: true
def flash_messages(assigns) do
~H"""
<div class="flash-messages">
<%= if msg = Phoenix.Flash.get(@flash, :info) do %>
<div class="alert alert-info">
<%= msg %>
</div>
<% end %>
<%= if msg = Phoenix.Flash.get(@flash, :error) do %>
<div class="alert alert-error">
<%= msg %>
</div>
<% end %>
<%= if msg = Phoenix.Flash.get(@flash, :warning) do %>
<div class="alert alert-warning">
<%= msg %>
</div>
<% end %>
</div>
"""
end
6.3 分页组件 #
elixir
attr :current_page, :integer, required: true
attr :total_pages, :integer, required: true
attr :path, :string, required: true
def pagination(assigns) do
~H"""
<nav class="pagination">
<%= if @current_page > 1 do %>
<.link href={"#{@path}?page=#{@current_page - 1}"} class="pagination-prev">
Previous
</.link>
<% end %>
<%= for page <- pagination_range(@current_page, @total_pages) do %>
<%= if page == :ellipsis do %>
<span class="pagination-ellipsis">...</span>
<% else %>
<.link
href={"#{@path}?page=#{page}"}
class={"pagination-page #{if page == @current_page, do: "active"}"}
>
<%= page %>
</.link>
<% end %>
<% end %>
<%= if @current_page < @total_pages do %>
<.link href={"#{@path}?page=#{@current_page + 1}"} class="pagination-next">
Next
</.link>
<% end %>
</nav>
"""
end
defp pagination_range(current, total) do
cond do
total <= 7 -> 1..total
current <= 4 -> [1, 2, 3, 4, 5, :ellipsis, total]
current >= total - 3 -> [1, :ellipsis, total - 4, total - 3, total - 2, total - 1, total]
true -> [1, :ellipsis, current - 1, current, current + 1, :ellipsis, total]
end
end
6.4 表格组件 #
elixir
attr :rows, :list, required: true
slot :col, required: true
def table(assigns) do
~H"""
<table class="table">
<thead>
<tr>
<%= for col <- @col do %>
<th><%= col[:label] %></th>
<% end %>
</tr>
</thead>
<tbody>
<%= for row <- @rows do %>
<tr>
<%= for col <- @col do %>
<td><%= render_slot(col, row) %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
"""
end
使用:
heex
defmodule HelloWeb.Layouts do
use HelloWeb, :html
embed_templates "layouts/*"
end
七、组件组织 #
7.1 目录结构 #
text
lib/hello_web/
├── components/
│ ├── core_components.ex # 核心组件
│ ├── form_components.ex # 表单组件
│ ├── layout_components.ex # 布局组件
│ └── ui_components.ex # UI组件
└── layouts/
└── layouts.ex # 布局模板
7.2 导入组件 #
elixir
defmodule HelloWeb do
def html do
quote do
use Phoenix.Component
import HelloWeb.CoreComponents
import HelloWeb.FormComponents
import HelloWeb.LayoutComponents
end
end
end
八、总结 #
8.1 核心概念 #
| 概念 | 说明 |
|---|---|
| 布局 | 页面框架模板 |
| 组件 | 可复用的UI单元 |
| 插槽 | 组件内容注入 |
| 属性 | 组件输入参数 |
8.2 最佳实践 #
- 将公共结构放在布局中
- 将可复用UI封装为组件
- 使用属性验证组件输入
- 使用插槽提供灵活性
8.3 下一步 #
现在你已经了解了布局与组件,接下来让我们学习 Ecto简介,深入了解数据库操作!
最后更新:2026-03-28