You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Filament表单构建器绑定JS实现手机号OTP验证

Laravel Livewire + Filament 实现手机号动态验证与OTP流程

核心问题修正

你当前代码里直接在Filament Form的schema中插入原生<PhoneInput>标签的方式不符合Filament规范,Filament字段应通过其官方API配置,我们可以借助Livewire的wire:model结合Filament字段的live()方法绑定事件,无需直接编写原生HTML。

完整实现步骤

1. 优化Livewire组件代码

<?php

namespace App\Livewire;

use App\Models\AdCondition;
use App\Models\PriceType;
use App\Models\User;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Forms\Components\PhoneInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Illuminate\Support\Facades\Session;
use Livewire\Component;

class YourLivewireComponent extends Component
{
    public $phone_number;
    public $phone_verified = false; // 标记手机号是否验证通过
    public $display_phone = false;

    public function detailForm(Form $form): Form
    {
        return $form->schema([
            ToggleButtons::make('for_sale_by')
                ->label(__('messages.t_for_sale_by'))
                ->live()
                ->grouped()
                ->options([
                    'owner' => __('messages.t_owner_for_sale'),
                    'business' => __('messages.t_business_for_sale'),
                ]),
            MarkdownEditor::make('description')
                ->disableToolbarButtons(['attachFiles'])
                ->label(__('messages.t_description'))
                ->live(onBlur: true)
                ->minLength(20)
                ->required(),
            ToggleButtons::make('condition_id')
                ->label(__('messages.t_condition'))
                ->live()
                ->options(AdCondition::all()->pluck('name', 'id'))
                ->inline(),
            Select::make('price_type_id')
                ->selectablePlaceholder(false)
                ->label(__('messages.t_price'))
                ->live()
                ->required()
                ->options(PriceType::all()->pluck('name', 'id')),
            TextInput::make('price')
                ->helperText(__('messages.t_set_fair_price'))
                ->required()
                ->numeric()
                ->minValue(1)
                ->placeholder(__('messages.t_price_your_ad'))
                ->prefix(config('app.currency_symbol'))
                ->live(onBlur: true)
                ->hidden(fn (Get $get): bool => $get('price_type_id') != 1)
                ->hiddenLabel(),
            ToggleButtons::make('display_phone')
                ->label(__('messages.t_display_phone_number'))
                ->live()
                ->required()
                ->boolean()
                ->grouped()
                ->afterStateUpdated(fn ($state) => $this->display_phone = $state),
            PhoneInput::make('phone_number')
                ->placeholder(__('messages.t_enter_phone_number'))
                ->helperText(__('messages.t_phone_number_helper'))
                ->required()
                ->live()
                ->debounce(500)
                ->hidden(fn () => !$this->display_phone)
                ->hiddenLabel()
                ->afterStateUpdated(function (string $state) {
                    if (strlen($state) === 11) {
                        $this->checkPhoneExistence();
                    }
                }),
            TagsInput::make('tags')
                ->label(__('messages.t_tags'))
                ->helperText(__('messages.t_set_tags'))
                ->live(onBlur: true),
        ]);
    }

    // 检查手机号是否存在于数据库
    public function checkPhoneExistence()
    {
        $exists = User::where('phone_number', $this->phone_number)->exists();

        if (!$exists) {
            $this->dispatch('show-otp-input');
            $this->sendOtpToPhone(); // 触发OTP发送逻辑
        } else {
            $this->phone_verified = true;
        }
    }

    // 发送OTP到手机号(需对接短信服务)
    private function sendOtpToPhone()
    {
        $otp = rand(100000, 999999);
        Session::put([
            'phone_otp' => $otp,
            'target_phone' => $this->phone_number
        ]);
        // 调用短信API发送$otp到$this->phone_number
    }

    // 验证OTP并更新用户信息
    public function verifyOtp($otp)
    {
        $storedOtp = Session::get('phone_otp');
        $targetPhone = Session::get('target_phone');

        if ($storedOtp == $otp && $targetPhone == $this->phone_number) {
            $user = auth()->user();
            $user->update([
                'phone_number' => $this->phone_number,
                'phone_number_verified_at' => now()
            ]);

            $this->phone_verified = true;
            $this->dispatch('show-success-message', ['message' => '手机号验证成功']);
            Session::forget(['phone_otp', 'target_phone']);
        } else {
            $this->dispatch('show-error-message', ['message' => 'OTP验证码错误']);
        }
    }

    // 表单提交前校验手机号验证状态
    public function submitForm()
    {
        if ($this->display_phone && !$this->phone_verified) {
            $this->dispatch('show-error-message', ['message' => '请先完成手机号验证']);
            return;
        }

        // 执行表单保存逻辑
    }

    public function render()
    {
        return view('livewire.your-livewire-component');
    }
}

2. 前端脚本优化(嵌入Livewire视图文件)

<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('livewire:init', function () {
    // 成功提示弹窗
    Livewire.on('show-success-message', ({ message }) => {
        Swal.fire('成功!', message, 'success');
    });

    // 错误提示弹窗
    Livewire.on('show-error-message', ({ message }) => {
        Swal.fire('错误!', message, 'error');
    });

    // OTP输入弹窗
    Livewire.on('show-otp-input', () => {
        Swal.fire({
            title: "输入验证码",
            input: "text",
            inputAttributes: {
                autocapitalize: "off",
                maxlength: 6,
                placeholder: "请输入6位验证码"
            },
            showCancelButton: true,
            confirmButtonText: "验证",
            allowOutsideClick: false,
            preConfirm: (otp) => {
                if (!otp || otp.length !== 6) {
                    Swal.showValidationMessage('请输入有效的6位验证码');
                }
                return otp;
            }
        }).then((result) => {
            if (result.isConfirmed) {
                Livewire.dispatch('verifyOtp', result.value);
            }
        });
    });
});
</script>

3. 移除冗余路由与控制器

原代码中单独的checkPhoneNumberExistence控制器方法可替换为Livewire内部方法,无需额外注册路由,简化架构。

关键逻辑说明

  • Filament字段规范:通过Filament官方API配置PhoneInput,利用live()和afterStateUpdated()实现输入值实时同步与后端触发,避免原生HTML侵入。
  • 验证状态管控:新增phone_verified属性标记验证状态,表单提交前强制校验,确保未验证手机号无法提交。
  • OTP安全存储:使用Session临时存储OTP与待验证手机号,避免直接将OTP写入用户表带来的安全风险。
  • 前后端通信统一:采用Livewire的dispatch与on方法管理事件,替代原生AJAX请求,贴合组件化开发逻辑。

内容的提问来源于stack exchange,提问作者Gilbert Ozioma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 17:53:23