Browse Source

feat: 后台登录页面支持多租户

9.3.4.8
zzzwangjun@gmail.com 5 months ago
parent
commit
68cb4ef31f
  1. 2
      aspnet-core/frameworks/src/Lion.AbpPro.AspNetCore/Microsoft/Extensions/DependencyInjection/ServiceCollectionExtensions.cs
  2. 7
      aspnet-core/modules/BasicManagement/src/Lion.AbpPro.BasicManagement.Application/Roles/RoleAppService.cs
  3. 208
      aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Login.cshtml
  4. 64
      aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Login.cshtml.cs
  5. 294
      aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Monitor.cshtml
  6. 4
      vben28/.env.production

2
aspnet-core/frameworks/src/Lion.AbpPro.AspNetCore/Microsoft/Extensions/DependencyInjection/ServiceCollectionExtensions.cs

@ -62,7 +62,7 @@ public static class ServiceCollectionExtensions
// options.TenantResolvers.Add(new QueryStringTenantResolveContributor()); // options.TenantResolvers.Add(new QueryStringTenantResolveContributor());
// options.TenantResolvers.Add(new RouteTenantResolveContributor()); // options.TenantResolvers.Add(new RouteTenantResolveContributor());
options.TenantResolvers.Add(new HeaderTenantResolveContributor()); options.TenantResolvers.Add(new HeaderTenantResolveContributor());
// options.TenantResolvers.Add(new CookieTenantResolveContributor()); options.TenantResolvers.Add(new CookieTenantResolveContributor());
}); });
return service; return service;

7
aspnet-core/modules/BasicManagement/src/Lion.AbpPro.BasicManagement.Application/Roles/RoleAppService.cs

@ -4,16 +4,18 @@ namespace Lion.AbpPro.BasicManagement.Roles;
public class RoleAppService : BasicManagementAppService, IRoleAppService public class RoleAppService : BasicManagementAppService, IRoleAppService
{ {
private readonly IIdentityRoleAppService _identityRoleAppService; private readonly IIdentityRoleAppService _identityRoleAppService;
private readonly IIdentityRoleRepository _roleRepository; private readonly IIdentityRoleRepository _roleRepository;
private readonly ICurrentTenant _currentTenant;
public RoleAppService( public RoleAppService(
IIdentityRoleAppService identityRoleAppService, IIdentityRoleAppService identityRoleAppService,
IIdentityRoleRepository roleRepository) IIdentityRoleRepository roleRepository,
ICurrentTenant currentTenant)
{ {
_identityRoleAppService = identityRoleAppService; _identityRoleAppService = identityRoleAppService;
_roleRepository = roleRepository; _roleRepository = roleRepository;
_currentTenant = currentTenant;
} }
/// <summary> /// <summary>
@ -60,6 +62,7 @@ public class RoleAppService : BasicManagementAppService, IRoleAppService
[Authorize(IdentityPermissions.Roles.Create)] [Authorize(IdentityPermissions.Roles.Create)]
public virtual async Task<IdentityRoleDto> CreateAsync(IdentityRoleCreateDto input) public virtual async Task<IdentityRoleDto> CreateAsync(IdentityRoleCreateDto input)
{ {
var s = _currentTenant;
return await _identityRoleAppService.CreateAsync(input); return await _identityRoleAppService.CreateAsync(input);
} }

208
aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Login.cshtml

@ -1,3 +1,4 @@
@page @page
@model Lion.AbpPro.Pages.Login @model Lion.AbpPro.Pages.Login
@ -11,19 +12,43 @@
<head> <head>
<title>后台服务登录</title> <title>后台服务登录</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container-fluid d-flex align-items-center justify-content-center min-vh-100">
<div class="row"> <div class="row w-100 justify-content-center">
<div class="col-md-offset-3 col-md-6"> <div class="col-md-6">
<form class="form-horizontal" method="post"> <form class="form-horizontal" method="post" novalidate>
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<!-- 错误提示区域 -->
@if (TempData["ErrorMessage"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>@TempData["ErrorMessage"]
</div>
}
<span class="heading">后台服务登录</span> <span class="heading">后台服务登录</span>
@if (TempData["EnableTenant"] != null && (bool)TempData["EnableTenant"])
{
<div class="form-group">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-building"></i></span>
<input type="text" class="form-control" name="tenantName" placeholder="租户名称(可选)">
</div>
</div>
}
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="userName" placeholder="用户名"> <div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" class="form-control" name="userName" placeholder="用户名" required>
</div>
</div> </div>
<div class="form-group help"> <div class="form-group help">
<input type="password" class="form-control" name="password" placeholder="密码"> <div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" name="password" placeholder="密码" required>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-default">登录</button> <button type="submit" class="btn btn-default">登录</button>
@ -34,141 +59,126 @@
</div> </div>
</body> </body>
</html> </html>
<style> <style> body {
.row { background: linear-gradient(120deg, #3498db, #8e44ad);
width: 800px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
height: auto; margin: 0;
margin: auto; padding: 0;
box-sizing: border-box; height: 100vh;
transform: translate(0, 50%); display: flex;
align-items: center;
justify-content: center;
} }
.form-horizontal { .form-horizontal {
background: #fff; background: rgba(255, 255, 255, 0.9);
padding-bottom: 40px; padding-bottom: 40px;
border-radius: 15px; border-radius: 15px;
text-align: center; text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 400px;
margin: 0 auto;
} }
.form-horizontal .heading { .form-horizontal .heading {
display: block; display: block;
font-size: 35px; font-size: 32px;
font-weight: 700; font-weight: 700;
padding: 35px 0; padding: 35px 0;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
margin-bottom: 30px; margin-bottom: 30px;
color: #2c3e50;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
} }
.form-horizontal .form-group { .form-group {
padding: 0 40px; padding: 0 30px;
margin: 0 0 25px 0; margin: 0 0 25px 0;
position: relative; position: relative;
} }
.form-horizontal .form-control { .input-group {
background: #f0f0f0; border-radius: 30px;
border: none; overflow: hidden;
border-radius: 20px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
box-shadow: none; transition: all 0.3s ease;
padding: 0 20px 0 45px;
height: 40px;
transition: all 0.3s ease 0s;
} }
.form-horizontal .form-control:focus { .input-group:focus-within {
background: #e0e0e0; box-shadow: 0 6px 15px rgba(52, 152, 219, 0.4);
box-shadow: none; transform: translateY(-2px);
outline: 0 none;
} }
.form-horizontal .form-group i { .input-group-text {
position: absolute; background: #3498db;
top: 12px; border: none;
left: 60px; color: white;
font-size: 17px; border-radius: 0;
color: #c8c8c8; width: 45px;
transition: all 0.5s ease 0s; font-size: 16px;
} }
.form-horizontal .form-control:focus + i { .form-control {
color: #00b4ef; border: none;
padding: 12px 15px;
height: 45px;
font-size: 15px;
box-shadow: none;
} }
.form-horizontal .fa-question-circle { .form-control:focus {
display: inline-block; box-shadow: none;
position: absolute; outline: 0 none;
top: 12px; background-color: #f8f9fa;
right: 60px;
font-size: 20px;
color: #808080;
transition: all 0.5s ease 0s;
} }
.form-horizontal .fa-question-circle:hover { .form-control:required:valid {
color: #000; border-left: 3px solid #28a745;
} }
.form-horizontal .main-checkbox { .form-control:required:invalid:not(:placeholder-shown) {
float: left; border-left: 3px solid #dc3545;
width: 20px;
height: 20px;
background: #11a3fc;
border-radius: 50%;
position: relative;
margin: 5px 0 0 5px;
border: 1px solid #11a3fc;
} }
.form-horizontal .main-checkbox label { .form-horizontal .btn {
width: 20px; text-align: center;
height: 20px; font-size: 15px;
position: absolute; font-weight: 600;
top: 0; color: #fff;
left: 0; background: linear-gradient(to right, #3498db, #8e44ad);
border-radius: 30px;
padding: 10px 25px;
border: none;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.4s ease 0s;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.4);
width: 100%;
cursor: pointer; cursor: pointer;
} }
.form-horizontal .main-checkbox label:after { .form-horizontal .btn:hover {
content: ""; transform: translateY(-3px);
width: 10px; box-shadow: 0 6px 20px rgba(52, 152, 219, 0.6);
height: 5px;
position: absolute;
top: 5px;
left: 4px;
border: 3px solid #fff;
border-top: none;
border-right: none;
background: transparent;
opacity: 0;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
} }
.form-horizontal .main-checkbox input[type="checkbox"] { .form-horizontal .btn:active {
visibility: hidden; transform: translateY(0);
} }
.form-horizontal .main-checkbox input[type="checkbox"]:checked + label:after { .container-fluid {
opacity: 1; padding: 10px;
} }
.form-horizontal .text { .form-group {
float: left; padding: 0 15px;
margin-left: 7px; margin: 0 0 20px 0;
line-height: 20px;
padding-top: 5px;
text-transform: capitalize;
} }
.form-horizontal .btn { .form-horizontal .heading {
text-align: center; font-size: 26px;
font-size: 14px; padding: 25px 0;
color: #fff; margin-bottom: 25px;
background: #00b4ef;
border-radius: 30px;
padding: 10px 25px;
border: none;
text-transform: capitalize;
transition: all 0.5s ease 0s;
} }
</style> </style>

64
aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Login.cshtml.cs

@ -1,5 +1,7 @@
using Lion.AbpPro.BasicManagement.ConfigurationOptions; using Lion.AbpPro.BasicManagement.ConfigurationOptions;
using Lion.AbpPro.BasicManagement.Tenants;
using Lion.AbpPro.BasicManagement.Tenants.Dtos;
using Lion.AbpPro.BasicManagement.Users; using Lion.AbpPro.BasicManagement.Users;
using Lion.AbpPro.BasicManagement.Users.Dtos; using Lion.AbpPro.BasicManagement.Users.Dtos;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
@ -15,58 +17,94 @@ namespace Lion.AbpPro.Pages
private readonly IHostEnvironment _hostEnvironment; private readonly IHostEnvironment _hostEnvironment;
private readonly JwtOptions _jwtOptions; private readonly JwtOptions _jwtOptions;
private readonly IClock _clock; private readonly IClock _clock;
private readonly AbpAspNetCoreMultiTenancyOptions _abpAspNetCoreMultiTenancyOptions;
private readonly IVoloTenantAppService _voloTenantAppService;
private readonly AbpProMultiTenancyOptions _abpProMultiTenancyOptions;
public Login(IAccountAppService accountAppService, public Login(IAccountAppService accountAppService,
ILogger<Login> logger, ILogger<Login> logger,
IHostEnvironment hostEnvironment, IHostEnvironment hostEnvironment,
IOptionsSnapshot<JwtOptions> jwtOptions, IOptionsSnapshot<JwtOptions> jwtOptions,
IClock clock) IClock clock,
IOptions<AbpAspNetCoreMultiTenancyOptions> abpAspNetCoreMultiTenancyOptions,
IVoloTenantAppService voloTenantAppService,
IOptions<AbpProMultiTenancyOptions> abpProMultiTenancyOptions)
{ {
_accountAppService = accountAppService; _accountAppService = accountAppService;
_logger = logger; _logger = logger;
_hostEnvironment = hostEnvironment; _hostEnvironment = hostEnvironment;
_clock = clock; _clock = clock;
_voloTenantAppService = voloTenantAppService;
_abpProMultiTenancyOptions = abpProMultiTenancyOptions.Value;
_abpAspNetCoreMultiTenancyOptions = abpAspNetCoreMultiTenancyOptions.Value;
_jwtOptions = jwtOptions.Value; _jwtOptions = jwtOptions.Value;
} }
public void OnGet() public void OnGet()
{ {
ViewData["ErrorMessage"] = null;
TempData["EnableTenant"] = _abpProMultiTenancyOptions.Enabled;
} }
public async Task OnPost() public async Task OnPost()
{ {
TempData["EnableTenant"] = _abpProMultiTenancyOptions.Enabled;
string tenantName = Request.Form["tenantName"];
string userName = Request.Form["userName"]; string userName = Request.Form["userName"];
string password = Request.Form["password"]; string password = Request.Form["password"];
if (userName.IsNullOrWhiteSpace() || password.IsNullOrWhiteSpace()) if (userName.IsNullOrWhiteSpace() || password.IsNullOrWhiteSpace())
{ {
Response.Redirect("/Login"); // 添加错误提示信息
TempData["ErrorMessage"] = "用户名和密码不能为空";
return; return;
} }
try try
{ {
Guid? tenantId = null;
// 判断租户是否存在
if (tenantName.IsNotNullOrWhiteSpace())
{
var tenant = await _voloTenantAppService.FindTenantByNameAsync(new FindTenantByNameInput() { Name = tenantName });
if (!tenant.Success)
{
TempData["ErrorMessage"] = $"租户[{tenantName}]不存在";
return;
}
tenantId = tenant.TenantId;
}
var options = new CookieOptions var options = new CookieOptions
{ {
Expires = _clock.Now.AddHours(_jwtOptions.ExpirationTime), Expires = _clock.Now.AddHours(_jwtOptions.ExpirationTime),
SameSite = SameSiteMode.Unspecified, SameSite = SameSiteMode.Unspecified,
}; };
var result = await _accountAppService.LoginAsync(new LoginInput() { Name = userName, Password = password });
// 设置cookies domain
//options.Domain = "AbpPro.cn"; // 清除现有的认证 cookies
Response.Cookies.Delete(AbpProAspNetCoreConsts.DefaultCookieName);
Response.Cookies.Delete(_abpAspNetCoreMultiTenancyOptions.TenantKey);
var result = await _accountAppService.LoginAsync(new LoginInput() Response.Cookies.Append(AbpProAspNetCoreConsts.DefaultCookieName, result.Token, options);
{ Name = userName, Password = password }); if (tenantId.HasValue)
Response.Cookies.Append(AbpProAspNetCoreConsts.DefaultCookieName, {
result.Token, options); Response.Cookies.Append(_abpAspNetCoreMultiTenancyOptions.TenantKey, tenantId.ToString(), options);
}
}
catch (BusinessException e)
{
_logger.LogError($"登录失败:{e.Message}");
TempData["ErrorMessage"] = $"用户名或者密码错误";
return;
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError($"登录失败:{e.Message}"); _logger.LogError($"登录失败:{e.Message}");
Response.Redirect("/Login"); TempData["ErrorMessage"] = $"登录失败:{e.Message}";
return; return;
} }
Response.Redirect("/monitor"); Response.Redirect("/monitor");
} }
} }

294
aspnet-core/services/host/Lion.AbpPro.HttpApi.Host/Pages/Monitor.cshtml

@ -1,204 +1,228 @@
@page @page
@using Lion.AbpPro @using Lion.AbpPro
@model Lion.AbpPro.Pages.Monitor @model Lion.AbpPro.Pages.Monitor
@{ @{
Layout = null; Layout = null;
} }
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<title>后端服务</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<title>后端服务监控</title>
</head> </head>
<body> <body>
<div class="container projects "> <div class="page-wrapper">
<div class="projects-header page-header"> <div class="container">
<h2>后端服务列表</h2> <div class="page-header">
@* <p>这些项目或者是对Bootstrap进行了有益的补充,或者是基于Bootstrap开发的</p> *@ <h1>后端服务监控面板</h1>
</div> <p class="subtitle">请选择您要访问的服务面板</p>
<div class="row"> </div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="thumbnail" style="height: 180px"> <div class="services-grid">
<a href="@AbpProHttpApiHostConst.SwaggerUiEndPoint" target="_blank"> <div class="service-item">
<img class="lazy" src="/images/swagger.png" width="300" height="150"/> <a href="@AbpProHttpApiHostConst.SwaggerUiEndPoint" target="_blank" class="service-link">
<div class="service-icon">
<i class="fas fa-book"></i>
</div>
<div class="service-info">
<h3>SwaggerUI</h3>
<p class="description">API文档与测试</p>
</div>
</a> </a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.SwaggerUiEndPoint" target="_blank">SwaggerUI</a>
</h3>
</div>
</div> </div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="service-item">
<div class="thumbnail" style="height: 180px"> <a href="@AbpProHttpApiHostConst.CapDashboardEndPoint" target="_blank" class="service-link">
<a href="@AbpProHttpApiHostConst.CapDashboardEndPoint" target="_blank"> <div class="service-icon">
<img class="lazy" src="/images/cap.png" width="300" height="150"/> <i class="fas fa-exchange-alt"></i>
</div>
<div class="service-info">
<h3>CAP面板</h3>
<p class="description">事件总线监控</p>
</div>
</a> </a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.CapDashboardEndPoint" target="_blank">CAP面板</a>
</h3>
</div>
</div> </div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="service-item">
<div class="thumbnail" style="height: 180px"> <a href="@AbpProHttpApiHostConst.HangfireDashboardEndPoint" target="_blank" class="service-link">
<a href="@AbpProHttpApiHostConst.HangfireDashboardEndPoint" target="_blank"> <div class="service-icon">
<img class="lazy" src="/images/hangfire.png" width="300" height="150"/> <i class="fas fa-tasks"></i>
</div>
<div class="service-info">
<h3>Hangfire面板</h3>
<p class="description">后台任务管理</p>
</div>
</a> </a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.HangfireDashboardEndPoint" target="_blank">Hangfire面板</a>
</h3>
</div>
</div> </div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="service-item">
<div class="thumbnail" style="height: 180px"> <a href="@AbpProHttpApiHostConst.MiniprofilerEndPoint" target="_blank" class="service-link">
<a href="@AbpProHttpApiHostConst.MiniprofilerEndPoint" target="_blank"> <div class="service-icon">
<img class="lazy" src="/images/miniprofiler.png" width="300" height="150"/> <i class="fas fa-chart-line"></i>
</div>
<div class="service-info">
<h3>Miniprofiler</h3>
<p class="description">性能分析工具</p>
</div>
</a> </a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.MiniprofilerEndPoint" target="_blank">Miniprofiler</a>
</h3>
</div>
</div> </div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="service-item">
<div class="thumbnail" style="height: 180px"> <a href="@AbpProHttpApiHostConst.MoreEndPoint" target="_blank" class="service-link">
<a href="@AbpProHttpApiHostConst.MoreEndPoint" target="_blank"> <div class="service-icon">
<img class="lazy" src="/images/more.png" width="300" height="150"/> <i class="fas fa-ellipsis-h"></i>
</div>
<div class="service-info">
<h3>了解更多...</h3>
<p class="description">更多服务选项</p>
</div>
</a> </a>
<div class="caption">
<h3>
<a href="@AbpProHttpApiHostConst.MoreEndPoint" target="_blank">了解更多...</a>
</h3>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</body> </body>
</html>
<style> <style>
*:before, * {
*:after { margin: 0;
-webkit-box-sizing: border-box; padding: 0;
-moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
} }
.container { body {
width: 1170px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
padding-right: 15px; background-color: #f5f7fa;
padding-left: 15px; color: #333;
margin-right: auto; line-height: 1.6;
margin-left: auto; min-height: 100vh;
padding: 20px;
} }
.projects-header { .page-wrapper {
width: 60%; display: flex;
text-align: center; align-items: center;
font-weight: 200; justify-content: center;
display: block; min-height: calc(100vh - 40px);
margin: 60px auto 40px !important;
} }
.page-header { .container {
padding-bottom: 9px; max-width: 1000px;
margin: 40px auto; width: 100%;
border-bottom: 1px solid #eee; background: white;
border-radius: 12px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
padding: 40px;
} }
.projects-header h2 { .page-header {
font-size: 42px; text-align: center;
letter-spacing: -1px; margin-bottom: 40px;
} }
h2 { .page-header h1 {
margin-top: 20px; font-size: 28px;
font-weight: 600;
color: #2d3748;
margin-bottom: 10px; margin-bottom: 10px;
font-weight: 500;
line-height: 1.1;
color: inherit;
/* text-align: center; */
} }
p { .subtitle {
margin: 0 0 10px; font-size: 16px;
color: #718096;
} }
.row { .services-grid {
margin-right: -15px; display: grid;
margin-left: -15px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 25px;
} }
.col-lg-3 { .service-item {
width: 25%; border-radius: 10px;
overflow: hidden;
transition: all 0.3s ease;
background: #ffffff;
border: 1px solid #e2e8f0;
} }
.projects .thumbnail { .service-item:hover {
display: block; transform: translateY(-3px);
margin-right: auto; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
margin-left: auto; border-color: #cbd5e0;
text-align: center;
margin-bottom: 30px;
border-radius: 0;
} }
.thumbnail { .service-link {
display: block; text-decoration: none;
padding: 4px; color: inherit;
line-height: 1.42857143; display: flex;
background-color: #fff; align-items: center;
border: 1px solid #ddd; padding: 25px;
.
transition(border 0.2 s ease-in-out);
} }
a { .service-icon {
color: #337ab7; width: 60px;
text-decoration: none; height: 60px;
background-color: transparent; border-radius: 50%;
background: #edf2f7;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
flex-shrink: 0;
} }
.projects .thumbnail img { .service-icon i {
max-width: 100%; font-size: 24px;
height: auto; color: #4299e1;
} }
.thumbnail a > img, .service-info h3 {
.thumbnail > img { font-size: 18px;
margin-right: auto; font-weight: 600;
margin-left: auto; color: #2d3748;
margin-bottom: 5px;
} }
img { .description {
vertical-align: middle; font-size: 14px;
color: #718096;
margin: 0;
} }
/* .projects .thumbnail .caption { /* 使用响应式单位和弹性布局替代媒体查询 */
overflow-y: hidden; .container {
color: #555; padding: clamp(20px, 5vw, 40px);
} */ }
.caption {
padding: 9px; .page-header h1 {
overflow-y: hidden; font-size: clamp(24px, 4vw, 28px);
color: #555; }
.service-link {
padding: clamp(15px, 3vw, 25px);
} }
</style>
.services-grid {
gap: clamp(15px, 3vw, 25px);
}
.service-icon {
width: clamp(50px, 8vw, 60px);
height: clamp(50px, 8vw, 60px);
}
.service-icon i {
font-size: clamp(20px, 4vw, 24px);
}
</style>
</html>

4
vben28/.env.production

@ -36,8 +36,8 @@ VITE_LEGACY = false
# 接口地址 # 接口地址
VITE_API_URL= http://43.139.143.143:8080 VITE_API_URL= http://139.155.114.244:44315
# WEBSOCKE 地址 # WEBSOCKE 地址
VITE_WEBSOCKE_URL= http://43.139.143.143:8080/signalr/notification VITE_WEBSOCKE_URL= http://139.155.114.244:44315/signalr/notification

Loading…
Cancel
Save