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

ASP.NET Core 8 MVC中用TailwindCSS实现输入框错误状态样式

问题描述

我正在基于垂直切片架构开发ASP.NET Core 8 MVC应用,当前登录功能的控制器和视图代码如下:

控制器代码

[AllowAnonymous]
[Route("login")]
public class LoginController(IMediator mediator) : Controller
{
  [HttpGet]
  public async Task<IActionResult> Index()
  {
    var viewModel = await mediator.Send(new LoginQuery());

    return View("/Features/Login/Login.cshtml", viewModel);
  }

  [HttpPost]
  public async Task<IActionResult> Login(LoginCommand command)
  {
    if (!ModelState.IsValid)
    {
      return View("/Features/Login/Login.cshtml");
    }
    
    var viewModel = await mediator.Send(command);

    if (string.IsNullOrEmpty(viewModel.Error))
    {
      return RedirectToAction("index", "dashboard");
    }

    return View("/Features/Login/Login.cshtml");
  }
}

视图代码(Login.cshtml)

<form asp-route="login" method="post" class="space-y-6">
  <div>
    <label asp-for="Email" class="block text-sm font-medium leading-6 text-gray-900"></label>
    <div class="mt-2">
      <input asp-for="Email" class="block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
      <span asp-validation-for="Email" class="mt-2 text-sm text-red-600"></span>
    </div>
  </div>

  <div>
    <label asp-for="Password" class="block text-sm font-medium leading-6 text-gray-900"></label>
    <div class="mt-2">
      <input asp-for="Password" class="block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
      <span asp-validation-for="Password" class="mt-2 text-sm text-red-600"></span>
    </div>
  </div>

  <div>
    <button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
  </div>
</form>

@section scripts {
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>
}

当前验证效果

当前显示效果

期望验证效果

期望显示效果

期望效果的参考代码:

<div>
  <label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email</label>
  <div class="relative mt-2 rounded-md shadow-sm">
    <input type="email" name="email" id="email" class="block w-full rounded-md border-0 py-1.5 pr-10 text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-2 focus:ring-inset focus:ring-red-500 sm:text-sm sm:leading-6" placeholder="you@example.com" value="adamwathan" aria-invalid="true" aria-describedby="email-error">
    <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
      <svg class="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
        <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
      </svg>
    </div>
  </div>
  <p class="mt-2 text-sm text-red-600" id="email-error">Not a valid email address.</p>
</div>

我知道用React/Vue等前端框架更容易实现,但现在必须用传统MVC模式,请问这种约束下,实现这类样式的最简单方式是什么?


解决方案

1. 利用MVC内置验证状态类

ASP.NET MVC会在验证失败时自动给输入框添加input-validation-error类,给提示元素添加field-validation-error类,我们可以基于这些类编写样式,同时结合视图结构调整来匹配期望效果。

2. 重构视图结构并添加错误图标

调整HTML结构,把输入框和错误图标放在同一个相对定位容器中,保留asp-for和asp-validation-for标签助手自动处理验证状态:

<form asp-route="login" method="post" class="space-y-6">
  <!-- 邮箱输入组 -->
  <div>
    <label asp-for="Email" class="block text-sm font-medium leading-6 text-gray-900"></label>
    <div class="relative mt-2 rounded-md shadow-sm">
      <input asp-for="Email" class="block w-full rounded-md border-0 py-1.5 pr-10 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
      <!-- 默认隐藏的错误图标 -->
      <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 hidden">
        <svg class="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
        </svg>
      </div>
    </div>
    <span asp-validation-for="Email" class="mt-2 text-sm text-red-600"></span>
  </div>

  <!-- 密码输入组 -->
  <div>
    <label asp-for="Password" class="block text-sm font-medium leading-6 text-gray-900"></label>
    <div class="relative mt-2 rounded-md shadow-sm">
      <input asp-for="Password" class="block w-full rounded-md border-0 py-1.5 pr-10 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"/>
      <!-- 默认隐藏的错误图标 -->
      <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 hidden">
        <svg class="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
        </svg>
      </div>
    </div>
    <span asp-validation-for="Password" class="mt-2 text-sm text-red-600"></span>
  </div>

  <div>
    <button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
  </div>
</form>

@section scripts {
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>
  <script>
    $(document).ready(function() {
      // 处理服务器端验证状态
      syncValidationState();

      // 监听客户端验证事件
      $('form').on('invalid-form.validate', syncValidationState);
      
      // 输入框内容变化时恢复正常状态
      $('input').on('input', function() {
        $(this).removeClass('text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-red-500');
        $(this).siblings('div').addClass('hidden');
      });

      function syncValidationState() {
        $('.input-validation-error').each(function() {
          $(this).addClass('text-red-900 ring-1 ring-inset ring-red-300 placeholder:text-red-300 focus:ring-red-500');
          $(this).siblings('div').removeClass('hidden');
        });
      }
    });
  </script>
}

3. 优化控制器返回逻辑

当前验证失败时返回视图未传递模型,会丢失用户输入,修改Post方法:

[HttpPost]
public async Task<IActionResult> Login(LoginCommand command)
{
  if (!ModelState.IsValid)
  {
    // 传递验证失败的模型,保留用户输入
    return View("/Features/Login/Login.cshtml", command);
  }
  
  var viewModel = await mediator.Send(command);

  if (string.IsNullOrEmpty(viewModel.Error))
  {
    return RedirectToAction("index", "dashboard");
  }

  // 将登录逻辑错误添加到ModelState
  ModelState.AddModelError(string.Empty, viewModel.Error);
  return View("/Features/Login/Login.cshtml", command);
}

4. 补充全局样式(可选)

如果需要更稳定的样式覆盖,可添加以下CSS到全局样式文件:

/* 强制显示错误图标 */
.input-validation-error ~ div {
  display: flex !important;
}

/* 错误状态输入框样式优先级提升 */
.input-validation-error.text-red-900 {
  color: #dc2626 !important;
  box-shadow: 0 0 0 1px #fca5a5 inset !important;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 18:47:32