关于 blade-conventions
blade-conventions 是用于创建和编辑 Bagisto Blade 视图的编码技能,涵盖组件用法、Blade 与 Vue 绑定规则、包命名空间和格式规范,帮助新模板符合代码库惯例。
主要功能
- 01说明 Blade 组件模式
- 02解释 Blade 与 Vue 绑定
- 03涵盖包命名空间和翻译
- 04介绍表单与扩展钩子
- 05规定模板格式规范
适用场景
- 01创建 Admin 或 Shop 视图
- 02构建可复用的 Blade 组件
- 03连接 Vue 组件与表单
- 04使自定义模块符合格式惯例
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ bagisto/ agent-skills --skill blade-conventions安装后在 Claude Code 中输入/blade-conventions确认已加载。
详细介绍
Bagisto Blade 约定
概览
Bagisto 的 UI 是 Blade 模板,大量使用 Blade 组件 和内联的 Vue 3 层。新视图必须与周围代码风格一致:采用相同的组件惯例、相同的 : 与 :: 绑定规则,以及相同的缩进和属性格式。本指南总结了这些约定,确保生成的 Blade 与代码库完全一致。
所有包都遵循相同的约定——Admin、Shop 以及任何自定义的 Webkul 风格模块。不同包之间只有一组 命名空间标记 不同(x-<ns>:: 组件前缀、<ns>:: 翻译命名空间和 view_render_event 名称);详见下文的“各包命名空间”。如有疑问,请打开同一包中的相邻视图并参照其写法。
核心 UI 位于 packages/Webkul/Admin/src/Resources/views/ 和 packages/Webkul/Shop/src/Resources/views/ 下;其他包(包括你自己的包)也会在各自的 src/Resources/views/ 下采用相同的目录结构。每个提供组件的包都有一个 components/example.blade.php,用于演示组件用法——请将其视为权威参考。
各包命名空间(Admin、Shop、自定义模块)
包的标记来自其 ServiceProvider::boot() 中的两个调用:
// Registers the `<ns>::` view + translation namespace → view('<ns>::…'), @lang('<ns>::…')
$this->loadViewsFrom(__DIR__.'/../Resources/views', '<ns>');
// Registers the `x-<ns>::` anonymous component prefix → <x-<ns>::button />
Blade::anonymousComponentPath(__DIR__.'/../Resources/views/components', '<ns>');
因此,Admin 注册 admin(→ x-admin::、@lang('admin::…')),Shop 注册 shop,而你自己的包(例如 RMA)注册 rma(→ x-rma::、@lang('rma::…'))。
在 Admin/Shop 之外开发时,这意味着:
- 可自由复用共享组件。
x-admin::和x-shop::组件/布局已全局注册,因此 RMA 管理页面仍使用<x-admin::layouts>包裹页面,并使用<x-admin::datagrid>、<x-admin::form.control-group.*>、<x-admin::modal>等组件。面向客户的页面则使用<x-shop::layouts>和 Shop 组件。无需重新实现这些组件。 - 仅为自己的新组件添加包命名空间前缀:
<x-rma::return-request-card>。 - 翻译使用自己的命名空间:
@lang('rma::app.return-requests.index.title'),对应的键位于packages/Webkul/RMA/src/Resources/lang/下。 view_render_event名称遵循包/区域命名规则,例如bagisto.rma.return-requests.list.before(参照该包中相邻事件的命名方式)。- 其他一切保持不变:
:/::绑定规则、Vue<v-x>+ x-template 模式、@props、格式、ACL 和脚本堆叠方式都原样适用。
切换包时可快速参照以下替换表:
| 标记 | Admin | Shop | 你的包(示例:RMA) |
|---|---|---|---|
| 自有组件前缀 | x-admin:: | x-shop:: | x-rma:: |
| 翻译命名空间 | admin::app.… | shop::app.… | rma::app.… |
| 外层布局 | x-admin::layouts | x-shop::layouts | 复用 x-admin::/x-shop:: 布局 |
| 事件前缀 | bagisto.admin.… | bagisto.shop.… | bagisto.rma.… |
适用场景
以下情况请启用本指南:
- 在 Admin 或 Shop 包下创建或编辑任何
.blade.php文件 - 构建可复用的 Blade 组件(匿名组件或 Vue 驱动组件)
- 配置表单、数据网格、模态框、抽屉、标签页或布局
- 遵循项目的属性绑定、缩进和空行风格
目录与组件结构
Admin 和 Shop 的目录结构相互对应。页面位于功能文件夹下(catalog/、sales/、checkout/、customers/ 等);共享组件位于 components/ 下。两者常见的组件包括:accordion, button, datagrid, drawer, dropdown, form, layouts, media, modal, quantity-changer, shimmer, table, tabs, tinymce。
命名空间调用形式:<x-admin::name> / <x-shop::name>;嵌套组件使用点号:<x-admin::form.control-group.control>、<x-admin::charts.bar>。
组件调用与数据绑定(最重要的规则)
三种不同的属性形式——请谨慎选择:
| 语法 | 解析为 | 用于 | 示例 |
|---|---|---|---|
attr="text" | 静态字符串 | 字面值 | name="quantity" |
:attr="expr" | Blade/PHP 表达式 | PHP 值、路由、trans()、old() | :src="route('admin.sales.orders.index')" |
::attr="expr" | 将 : 转义为 → Vue 的字面量 :attr | 传入 Vue 组件的数据 | ::value="item?.quantity"、::labels="chartLabels" |
::(双冒号)是 Blade 对单个 : 的转义,因此渲染后的 HTML 中会包含供 Vue 在运行时绑定的 :attr="expr"。正确区分 : 和 :: 是最常见的错误来源。
:: 仅适用于 Blade 组件标签——<x-admin::…>、<x-shop::…>、<x-marketplace::…>。Blade 在编译组件时会去掉多余的冒号。普通自定义元素(<v-quantity-changer>、<v-field>,以及你自己编写的任何 <v-…>)会作为字面量 HTML 原样传递,因此 ::attr 会以两个冒号的形式到达浏览器,Vue 无法识别它是绑定,属性也就不会传入。对于普通的 <v-…> 元素,请使用单个冒号:
<x-admin::quantity-changer ::value="item.qty" /> {{-- component → renders :value --}}
<v-quantity-changer :value="item.qty"></v-quantity-changer> {{-- plain element → write : --}}
这种故障不会报错,很容易被误判:属性值为 undefined,因此任何遍历它的计算属性都会在渲染时抛出错误,组件会停留在上一次绘制的内容——通常是加载动画。如果某个 <v-…> 组件一直显示占位内容,请先检查冒号。
命名插槽使用 <x-slot:name> … </x-slot>:
<x-admin::drawer>
<x-slot:toggle>Toggle</x-slot>
<x-slot:content>Body</x-slot>
</x-admin::drawer>
两种组件类型
1. 匿名 Blade 组件(@props + $attributes)
@props([
'isActive' => false,
'position' => 'right',
])
<div {{ $attributes->merge(['class' => 'box-shadow rounded bg-white dark:bg-gray-900']) }}>
{{ $slot }}
</div>
- 使用
@props([...])声明输入。 - 使用
$attributes->merge([...])转发额外属性。 - 使用
{{ $slot }}接收默认插槽,使用{{ $toggle }}等接收命名插槽。
2. Vue 驱动的组件(主要模式)
一个精简的自定义元素包装器 + 内联 x-template + 在全局 app 上注册:
@props([
'name' => '',
'value' => 1,
])
<v-quantity-changer
{{ $attributes->merge(['class' => 'flex items-center']) }}
name="{{ $name }}"
value="{{ $value }}"
>
</v-quantity-changer>
@pushOnce('scripts')
<script
type="text/x-template"
id="v-quantity-changer-template"
>
<div>
<span
class="icon-minus cursor-pointer"
role="button"
@click="decrease"
></span>
<p>@{{ quantity }}</p>
</div>
</script>
<script type="module">
app.component("v-quantity-changer", {
template: '#v-quantity-changer-template',
props: ['name', 'value'],
data() {
return {
quantity: this.value,
};
},
methods: {
decrease() {
this.$emit('change', --this.quantity);
},
},
});
</script>
@endPushOnce
此模式的规则:
- 包装元素为
<v-name>;模板 ID 为#v-name-template。 - 使用
app.component("v-name", { template: '#v-name-template', ... })注册。 - 使用
@pushOnce('scripts')…@endPushOnce包裹脚本(布局会渲染@stack('scripts'),因此无论组件使用多少次,该代码块都只会输出一次)。 - 使用
@{{ expr }}输出 Vue 原样的大括号表达式,避免 Blade 尝试渲染它们。 - 通过
::attr(Vue 绑定)或attr="{{ $php }}"(服务端值)传入数据。
页面骨架
<x-admin::layouts>
<x-slot:title>
@lang('admin::app.catalog.attributes.index.title')
</x-slot>
<div class="flex items-center justify-between">
<p class="text-xl font-bold text-gray-800 dark:text-white">
@lang('admin::app.catalog.attributes.index.title')
</p>
@if (bouncer()->hasPermission('catalog.attributes.create'))
<a href="{{ route('admin.catalog.attributes.create') }}">
<div class="primary-button">
@lang('admin::app.catalog.attributes.index.create-btn')
</div>
</a>
@endif
</div>
{!! view_render_event('bagisto.admin.catalog.attributes.list.before') !!}
<x-admin::datagrid :src="route('admin.catalog.attributes.index')" />
{!! view_render_event('bagisto.admin.catalog.attributes.list.after') !!}
</x-admin::layouts>
商城页面使用 <x-shop::layouts>,并可通过 @push('meta') 添加 SEO 标签,或通过 @inject('helper', '...') 注入视图辅助工具。
通用惯用模式
- 可扩展性钩子 — 使用
{!! view_render_event('bagisto.<area>.<path>.before') !!}和.after将有意义的内容括起来。以点分隔的名称遵循视图路径。大多数页面中都有这些钩子。 - 翻译 — 切勿将界面文本硬编码。使用
@lang('admin::app.…')/@lang('shop::app.…')或trans('…'),并始终使用包命名空间。添加键时,请将其加入Resources/lang/下的所有语言区域文件。 - ACL(仅限管理后台) — 使用
@if (bouncer()->hasPermission('resource.action'))对创建/编辑/删除按钮和数据网格操作进行权限控制。 - 表单 —
<x-admin::form :action="route(...)" method="POST">会包装一个 VeeValidatev-form。使用以下控件组组合构建字段:
验证在客户端进行;错误组件上的<x-admin::form.control-group> <x-admin::form.control-group.label class="required"> @lang('...') </x-admin::form.control-group.label> <x-admin::form.control-group.control type="text" name="admin_name" rules="required" :value="old('admin_name')" :label="trans('...')" /> <x-admin::form.control-group.error control-name="admin_name" /> </x-admin::form.control-group>control-name必须与字段的name匹配。 - 数据网格 —
<x-admin::datagrid :src="route('…')" />。列、筛选器和操作在 PHPDataGrid类中定义;Blade 标签只需指向 JSON 端点。 - Blade ↔ Vue 转义 —
@{{ vueVar }}会输出字面量 Vue 大括号表达式;将代码块放在v-pre中,可避免 Blade 处理其中的@/{{ }}。 - 主题 / 国际化外壳 — 布局会设置
class="… dark …"和dir="{{ core()->getCurrentLocale()->direction }}";使用 Tailwind 的dark:和ltr:/rtl:变体来适配主题和 RTL 布局。
格式与样式规则
这些规则在 .blade.php 中手动执行(Pint 不会格式化 Blade),因此请严格遵守。
缩进
- 使用 4 个空格,不用制表符。每增加一层嵌套,统一增加 4 个空格。
属性布局
- 属性超过一个 → 每行一个属性,每个属性相对于标签缩进 +4 个空格,闭合
>(或/>)单独占一行,并与开头的<tag对齐:<x-admin::form.control-group.control type="text" name="admin_name" :value="old('admin_name')" /> - 单个属性 → 放在行内:
<x-admin::form.control-group.error control-name="admin_name" />。 - 属性行必须连续排列——同一个标签内的属性之间不能有空行。
- 属性顺序可灵活安排:控制流/Vue 指令(
v-if、v-else)→class/:class→ 普通属性和aria-*属性 → 事件处理器(@click、@change)放在最后。
@props — 对齐 =>
@props([
'name' => '',
'value' => 1,
'minValue' => 1,
'removable' => false,
])
调整键的填充空格,使 => 箭头对齐。@props 代码块是供读者像查看表格一样浏览的声明,因此这里是唯一值得对齐的地方。
@php … @endphp 中的 PHP — 手动遵循 Pint 规则
在 @php 代码块中编写的是纯 PHP,因此必须严格遵循 vendor/bin/pint 格式化 .php 文件时的输出——最明显的是,=> 两侧各有一个空格,绝不通过填充空格来对齐:
@php
$steps = [
'label' => trans($prefix.'.source'),
'hint' => trans($prefix.'.source-hint'),
'validate' => ['file'],
];
@endphp
Pint 不会格式化 .blade.php 文件,因此没有任何机制会强制执行此规则——必须手动遵守。若不确定某个结构的格式,请在临时 .php 文件中编写,运行 Pint,再复制结果。Pint 输出的其他部分也同样适用:空格、运算符、尾随逗号和短闭包。
@php 之外的内容——@props、指令参数、行内 {{ }} 表达式——都属于 Blade 自身的层级,并遵循本页的 Blade 规则。
空行
- 同级代码块/元素之间,以及
view_render_event钩子周围,各留一个空行。 @props([...])代码块后、标记之前留一个空行。
指令大小写 / 命名
- 标准的 push 代码块:
@pushOnce('scripts')…@endPushOnce(注意@endPushOnce中的P大写)。'styles'很少用于 CSS。
注释
package-development 技能中的两条规则也适用于 Blade,且看似相互矛盾——两者都要遵循:
- “每个方法都要有文档块”——
<script>代码块中的 Vuemethod或computed都是方法。应添加/** … */,用首字母大写且以句号结尾的句子描述其功能。@php代码块中的任何函数也同样如此。 - “只注释代码本身无法说明的内容”——在方法体内部和标记中,默认不写注释。仅说明局部代码中不可见的约束、变通方案,或看似可删除却不可或缺的代码行;不要重复说明标记或调用的含义。
以下是各层应使用的语法。
每种注释语法都属于其自身的层级——根据注释所在的位置选择语法,然后遵循该层级的大小写和标点规则。
-
Blade
{{-- --}}— 用于 Blade/PHP 层的注释(文件头、@php逻辑、控制流说明)。永远不会发送到浏览器。- 如果注释是句子(或多个句子),请将其写成散文,并以大写字母开头、以标点符号结尾。多行文件头应说明视图是什么以及原因:
{{-- The friendly "What kind of product?" create funnel (stepped scratch page only). It maps a plain kind + a "Does it have variations?" answer to a Bagisto product type behind the scenes. --}} - 如果是简短标签/标题(不是句子),使用标题式大小写,末尾不加句号:
{{-- Media --}}、{{-- Type-Specific Controls --}}。 - 不要放在
@php … @endphp内。 Blade 不会剥离其中的 Blade 注释,而是将其原样复制到编译后的 PHP 中,从而导致解析错误。在@php块内编写的是普通 PHP,因此应使用 PHP 注释语法及相应的 PHP 规范;如果需要说明整个代码块,请在代码块上方添加{{-- --}}文件头:{{-- Everything the wizard needs to describe its steps. --}} @php // The upload-size config is stored in MB; VeeValidate's size rule expects KB. $maxUploadKb = $maxUploadMb * 1024; @endphp
- 如果注释是句子(或多个句子),请将其写成散文,并以大写字母开头、以标点符号结尾。多行文件头应说明视图是什么以及原因:
-
HTML `` — 用作 x-template / 标记内部的分节符。保持简短:使用标题式大小写的标签或自然的问题——与该代码块渲染的标题保持一致。标签末尾不加句号;问题末尾可以加
?: -
JS
/** … */(JSDoc 块) — 位于<script>内。这是 Vue 层唯一使用的注释形式——不要使用//行注释。每个computed/method都要添加一条,以大写字母开头并以句号结尾。方法内部的语句只有在确实难以理解时才需要注释:/** * The axis checkboxes only appear for a variable kind, once the seller has said "yes" * and picked a family. */ showAxes() { return this.isVariable && this.hasVariations === true && !! this.familyId; }即使是简单的一行代码也要添加文档块——但只写一句话即可,不要对已经清晰易懂的语句堆叠额外注释。
-
CSS
/** … */(JSDoc 块) — 位于<style>内,形式与 JS 相同:说明规则存在的原因(尤其是:checked驱动的状态或隐藏滚动条等不明显的规则),并写成带标点的句子:/** * Hide the navigation radios; they only drive the :checked state. */ #rma-steps .rma-step-radio { … }
经验法则:句子 → 首字母大写并加标点;纯标题/标签 → 标题式大小写,不加句号。 在 <script>/<style> 中始终使用 /** … */ 块形式,绝不使用 // 或裸露的 /* */。
配方:新建 Vue 组件
- 创建
views/components/<name>/index.blade.php。 - 使用
@props([...])定义服务端输入(=>对齐)。 - 渲染
<v-<name> {{ $attributes->merge([...]) }} …>,通过attr="{{ $php }}"传递数据。 - 使用
@pushOnce('scripts'):添加<script type="text/x-template" id="v-<name>-template">和<script type="module"> app.component("v-<name>", { template: '#v-<name>-template', props, data, computed, methods }) </script>;以@endPushOnce结束。 - 使用
@{{ … }}输出运行时值;通过::attr绑定父级数据。 - 按
<x-{admin|shop}::<name> ::value="…" @change="…" />的形式使用。
配方:新建页面
- 使用
<x-admin::layouts>/<x-shop::layouts>包裹,并添加<x-slot:title>。 - 添加标题行(标题 + 通过
bouncer()控制权限的操作按钮)。 - 使用
view_render_event的.before/.after事件包围主要内容。 - 所有字符串都使用带有包命名空间的
@lang/trans。
应做 / 不应做
- 应复用现有的
<x-{admin|shop}::…>组件和插槽;不要手写组件已有的标记。 - 对 Vue 层使用的内容,应使用
::;对 PHP 值,应使用:。 - 在
@props块中,应对齐=>;不要在@php … @endphp或.php文件中对齐——那是纯 PHP,因此适用 Pint 的单空格规则。 - 应使用
@pushOnce('scripts')…@endPushOnce包裹组件脚本。 - 应通过
@lang/trans为所有字符串添加命名空间,并将新键添加到每种语言环境中。 - 不要在标签的属性之间插入空行;应在同级块之间插入空行。
- 应使用
bouncer()->hasPermission(...)控制后台操作的访问权限。