ow
This commit is contained in:
1 parent
eb7a9e9b88
commit
fd51982804
7 files changed
+242
-11
No files matched your search
@@ -19,6 +19,14 @@
|
|||||||
<PackageReference Include="Fido2" Version="4.0.1" />
|
<PackageReference Include="Fido2" Version="4.0.1" />
|
||||||
<PackageReference Include="Fido2.AspNet" Version="4.0.1" />
|
<PackageReference Include="Fido2.AspNet" Version="4.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
||||||
|
<!--
|
||||||
|
必须显式对齐:JwtBearer 8.0.0 传递引入 System.IdentityModel.Tokens.Jwt 7.0.3,
|
||||||
|
而 Fido2 把 Microsoft.IdentityModel.Tokens / JsonWebTokens 提升到了 8.2.0。
|
||||||
|
两个版本混用时旧版 JwtSecurityTokenHandler 读令牌会错乱(issuer 解析为空),
|
||||||
|
抛 IDX10211,导致 AuthService.RefreshToken 永远失败 → 访问令牌 2 小时过期时
|
||||||
|
用户被提示「无效的令牌,请重新登录.」。这里把 legacy 包也对齐到 8.2.0。
|
||||||
|
-->
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||||
|
|||||||
@@ -417,20 +417,28 @@ namespace CloudBuilder.Security.Service
|
|||||||
ValidateLifetime = false,//不验证过期时间
|
ValidateLifetime = false,//不验证过期时间
|
||||||
};
|
};
|
||||||
|
|
||||||
// 验证 Token
|
// 验证 Token。
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
// 必须用 JsonWebTokenHandler(.NET 8 的 JwtBearer 鉴权、ClaimPolicy.GetClaimValue 用的都是它):
|
||||||
SecurityToken vTocken;
|
// legacy 的 JwtSecurityTokenHandler 与 Microsoft.IdentityModel.Tokens 8.x 版本混用时会错乱解析令牌
|
||||||
|
// (issuer 读成空 → IDX10211),于是刷新恒失败、访问令牌两小时一到期用户就被踢下线。
|
||||||
|
var tokenHandler = new JsonWebTokenHandler();
|
||||||
string token = jwtInfo.AccessToken.Replace("Bearer ", "");
|
string token = jwtInfo.AccessToken.Replace("Bearer ", "");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tokenValidationResult = tokenHandler.ValidateToken(token, parameters, out vTocken);
|
var tokenValidationResult = tokenHandler.ValidateTokenAsync(token, parameters).GetAwaiter().GetResult();
|
||||||
|
|
||||||
if (tokenValidationResult == null || !tokenValidationResult.Identities.Any() || !tokenValidationResult.Identities.First().IsAuthenticated)
|
if (tokenValidationResult == null || !tokenValidationResult.IsValid
|
||||||
throw new ServiceErrorException("无效的令牌,请重新登录.");
|
|| tokenValidationResult.ClaimsIdentity == null || !tokenValidationResult.ClaimsIdentity.IsAuthenticated)
|
||||||
|
throw new ServiceErrorException($"无效的令牌,请重新登录.({tokenValidationResult?.Exception?.Message})");
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (ServiceErrorException)
|
||||||
{
|
{
|
||||||
throw new ServiceErrorException("无效的令牌,请重新登录.");
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 带上原始异常:以前直接吞掉,出问题时只看到"无效的令牌",无法定位
|
||||||
|
throw new ServiceErrorException($"无效的令牌,请重新登录.({ex.Message})");
|
||||||
}
|
}
|
||||||
|
|
||||||
string clainmName = ClaimPolicy.GetClaimValue(token, ClaimPolicy.CLAINM_NAME);
|
string clainmName = ClaimPolicy.GetClaimValue(token, ClaimPolicy.CLAINM_NAME);
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
<PackageReference Include="Fido2" Version="4.0.1" />
|
<PackageReference Include="Fido2" Version="4.0.1" />
|
||||||
<PackageReference Include="Fido2.AspNet" Version="4.0.1" />
|
<PackageReference Include="Fido2.AspNet" Version="4.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
||||||
|
<!-- 与 Fido2 提升上来的 Microsoft.IdentityModel.Tokens 8.2.0 对齐;
|
||||||
|
否则输出目录里会同时存在 Jwt 7.0.3 + Tokens 8.2.0,legacy 校验器会错乱解析令牌。
|
||||||
|
本项目对同级工程用的是 <Reference>+HintPath(不会传递依赖),所以这里必须单独声明。 -->
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2</_PublishTargetUrl>
|
<_PublishTargetUrl>C:\inetpub\wwwroot\dockwebapi2</_PublishTargetUrl>
|
||||||
<History>True|2026-09-22T15:03:13.6509491Z||;True|2026-09-22T16:07:15.9177123+08:00||;True|2026-09-22T11:19:19.6087109+08:00||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||;</History>
|
<History>True|2026-09-23T05:19:13.0148919Z||;True|2026-09-23T13:16:07.8584304+08:00||;True|2026-09-22T23:03:13.6509491+08:00||;True|2026-09-22T16:07:15.9177123+08:00||;True|2026-09-22T11:19:19.6087109+08:00||;True|2026-09-22T11:09:24.9165065+08:00||;True|2026-09-21T22:40:07.1232044+08:00||;True|2026-09-21T22:35:06.9446571+08:00||;True|2026-09-21T22:27:42.1146825+08:00||;False|2026-09-21T22:27:17.6066573+08:00||;True|2026-09-21T21:01:22.8888913+08:00||;True|2026-09-21T19:15:03.4753747+08:00||;True|2026-09-21T18:08:30.0866986+08:00||;True|2026-09-21T16:51:19.8137872+08:00||;True|2026-09-21T16:30:01.0950896+08:00||;True|2026-09-21T15:09:00.2326737+08:00||;True|2026-09-21T14:47:09.6120256+08:00||;True|2026-09-21T14:16:35.3194409+08:00||;True|2026-09-21T13:54:58.2829712+08:00||;True|2026-09-21T13:36:45.3134499+08:00||;True|2026-09-19T09:48:52.0085130+08:00||;False|2026-09-19T09:47:56.1658214+08:00||;True|2026-09-17T18:50:15.9493380+08:00||;True|2026-09-17T18:11:11.5364547+08:00||;True|2026-09-17T18:07:27.7830758+08:00||;True|2026-09-17T17:58:16.5798560+08:00||;True|2026-09-17T15:37:30.9025614+08:00||;True|2026-09-17T15:25:46.9116292+08:00||;True|2026-09-17T15:05:18.6592403+08:00||;True|2026-09-17T14:57:28.7997437+08:00||;True|2026-09-17T14:36:00.6339386+08:00||;True|2026-09-17T13:56:43.2376873+08:00||;True|2026-09-16T13:56:51.8724546+08:00||;True|2026-09-15T20:44:46.6559899+08:00||;True|2026-09-13T19:02:59.5654245+08:00||;True|2026-09-10T09:14:56.2914193+08:00||;True|2026-09-09T16:40:16.6461154+08:00||;True|2026-09-08T13:55:23.1502435+08:00||;True|2026-09-08T08:04:53.4279263+08:00||;True|2026-09-07T08:48:17.4682304+08:00||;True|2026-09-04T14:39:08.7878670+08:00||;True|2026-09-04T14:35:48.2937877+08:00||;True|2026-09-04T11:45:16.1372876+08:00||;True|2026-09-04T11:42:59.9356436+08:00||;True|2026-09-03T16:05:52.5270232+08:00||;True|2026-09-03T14:52:56.8559144+08:00||;True|2026-09-02T11:08:23.8720038+08:00||;True|2026-09-02T10:58:16.8081797+08:00||;True|2026-08-26T13:34:12.3805892+08:00||;True|2026-08-25T08:31:30.7787234+08:00||;True|2026-08-20T13:10:46.4725468+08:00||;True|2026-08-20T13:10:28.3142576+08:00||;True|2026-08-19T16:10:27.3362887+08:00||;True|2026-08-19T16:01:12.6234971+08:00||;True|2026-08-19T15:42:46.6576265+08:00||;True|2026-08-19T13:39:59.9363522+08:00||;True|2026-08-19T13:36:53.0379313+08:00||;True|2026-08-19T13:31:36.0841398+08:00||;True|2026-08-19T11:44:37.5319626+08:00||;True|2026-08-19T09:36:45.6000047+08:00||;True|2026-08-19T09:34:36.8468631+08:00||;True|2026-08-19T08:35:41.2202300+08:00||;True|2026-08-18T16:20:59.0170916+08:00||;</History>
|
||||||
<LastFailureDetails />
|
<LastFailureDetails />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>C:\inetpub\wwwroot\windows</_PublishTargetUrl>
|
<_PublishTargetUrl>C:\inetpub\wwwroot\windows</_PublishTargetUrl>
|
||||||
<History>True|2026-09-22T15:02:08.0902556Z||;True|2026-09-22T16:47:49.2530282+08:00||;True|2026-09-22T15:18:53.4935889+08:00||;True|2026-09-22T11:24:43.8593406+08:00||;True|2026-09-19T09:55:09.0653660+08:00||;True|2026-09-19T09:53:13.8933944+08:00||;</History>
|
<History>True|2026-09-23T05:17:58.5542095Z||;True|2026-09-22T23:02:08.0902556+08:00||;True|2026-09-22T16:47:49.2530282+08:00||;True|2026-09-22T15:18:53.4935889+08:00||;True|2026-09-22T11:24:43.8593406+08:00||;True|2026-09-19T09:55:09.0653660+08:00||;True|2026-09-19T09:53:13.8933944+08:00||;</History>
|
||||||
<LastFailureDetails />
|
<LastFailureDetails />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"Issuer": "Owen Chen",
|
"Issuer": "Owen Chen",
|
||||||
"Audience": "Cloud Builder",
|
"Audience": "Cloud Builder",
|
||||||
"Secret": "{8619F7C3-B53C-4B85-99F0-983D351ECD82}",
|
"Secret": "{8619F7C3-B53C-4B85-99F0-983D351ECD82}",
|
||||||
"AccessExpireMins": 120, //2小时过期
|
"AccessExpireMins": 2, //2小时过期
|
||||||
"RefreshExpireMins": 86400, // 60天重刷新过期时间
|
"RefreshExpireMins": 86400, // 60天重刷新过期时间
|
||||||
"IsRefreshAbsoluteExpired": false
|
"IsRefreshAbsoluteExpired": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-inner">
|
||||||
|
<div class="hero-top">
|
||||||
|
<div class="brand" :class="{ 'is-clickable': clickable }" @click="onBrandClick">
|
||||||
|
<a-avatar
|
||||||
|
shape="square"
|
||||||
|
:size="36"
|
||||||
|
src="/youjia.svg"
|
||||||
|
class="brand-mark"
|
||||||
|
/>
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
</div>
|
||||||
|
<a-dropdown placement="bottomRight" :trigger="['click']">
|
||||||
|
<div class="avatar" :title="userName">{{ avatarText }}</div>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="onUserMenuClick">
|
||||||
|
<a-menu-item key="amentUserLocalName">
|
||||||
|
<UserOutlined />
|
||||||
|
{{ t("layout.header.amentUserLocalName") }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="amentPassword">
|
||||||
|
<LockOutlined />
|
||||||
|
{{ t("layout.header.amentPassword") }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="biometricButton">
|
||||||
|
<LockOutlined />
|
||||||
|
{{ biometricButtonLabel }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-divider />
|
||||||
|
<a-menu-item key="logout">
|
||||||
|
<LogoutOutlined />
|
||||||
|
{{ t("layout.header.logout") }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<changePassword v-model:showModel="showChangePassword" />
|
||||||
|
<changeLocalName v-model:showModel="showChangeUserLocalName" />
|
||||||
|
<changeBiometricEnrollCard v-model:showModel="showBiometricEnroll" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { notification } from "ant-design-vue";
|
||||||
|
import type { MenuProps } from "ant-design-vue";
|
||||||
|
import { LockOutlined, LogoutOutlined, UserOutlined } from "@ant-design/icons-vue";
|
||||||
|
import authService from "@/service/security/authService";
|
||||||
|
import { useLoginProfileStore } from "@/store/modules/loginProfile";
|
||||||
|
import i18n from "@/locales";
|
||||||
|
import changePassword from "@/views/login/changePassword.vue";
|
||||||
|
import changeLocalName from "@/views/login/changeLocalName.vue";
|
||||||
|
import changeBiometricEnrollCard from "@/views/login/changeBiometricEnrollCard.vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 欢迎页顶部那一条「品牌 + 头像」的渐变头图。
|
||||||
|
*
|
||||||
|
* 欢迎页(DashboardHero)与后台布局(layout/index.vue、layout/master.vue)共用它,
|
||||||
|
* 两处顶栏才会长得一模一样。
|
||||||
|
*/
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** 品牌名 */
|
||||||
|
title?: string;
|
||||||
|
/** 品牌名是否可点(后台布局点了回工作台) */
|
||||||
|
clickable?: boolean;
|
||||||
|
}>(),
|
||||||
|
{ title: "有家相册", clickable: false },
|
||||||
|
);
|
||||||
|
const emit = defineEmits<{ (e: "brand-click"): void }>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const profile = useLoginProfileStore();
|
||||||
|
const { t } = i18n.global;
|
||||||
|
|
||||||
|
const userName = computed(
|
||||||
|
() => profile.getUserLocalName() || profile.getUsername() || "新朋友",
|
||||||
|
);
|
||||||
|
const avatarText = computed(() => {
|
||||||
|
const u = (userName.value || "U").trim();
|
||||||
|
return u ? u.slice(-1).toUpperCase() : "U";
|
||||||
|
});
|
||||||
|
|
||||||
|
const onBrandClick = () => {
|
||||||
|
if (!props.clickable) return;
|
||||||
|
emit("brand-click");
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ==================== 头像菜单 ==================== */
|
||||||
|
const showChangePassword = ref<boolean>(false);
|
||||||
|
const showChangeUserLocalName = ref<boolean>(false);
|
||||||
|
const showBiometricEnroll = ref<boolean>(false);
|
||||||
|
|
||||||
|
// Coarse platform detection so the button label is friendly on each device.
|
||||||
|
const biometricButtonLabel = computed(() => {
|
||||||
|
const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
||||||
|
if (/iPhone|iPad/.test(ua)) return "Sign in with Face ID / Touch ID";
|
||||||
|
if (/Android/.test(ua)) return "Sign in with fingerprint";
|
||||||
|
if (/Windows/.test(ua)) return "Sign in with Windows Hello";
|
||||||
|
if (/Mac OS X/.test(ua)) return "Sign in with Touch ID";
|
||||||
|
return "Sign in with fingerprint / face";
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUserMenuClick: MenuProps["onClick"] = (menuInfo) => {
|
||||||
|
if (menuInfo.key === "amentUserLocalName") {
|
||||||
|
showChangeUserLocalName.value = true;
|
||||||
|
} else if (menuInfo.key === "amentPassword") {
|
||||||
|
showChangePassword.value = true;
|
||||||
|
} else if (menuInfo.key === "biometricButton") {
|
||||||
|
showBiometricEnroll.value = true;
|
||||||
|
} else if (menuInfo.key === "logout") {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
await authService.Logout();
|
||||||
|
profile.clearStorage();
|
||||||
|
notification.info({
|
||||||
|
message: t("layout.notification.message"),
|
||||||
|
description: t("layout.notification.description"),
|
||||||
|
placement: "topRight",
|
||||||
|
});
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
/* 渐变头图:与欢迎页 hero 完全一致 */
|
||||||
|
.hero {
|
||||||
|
/* 通栏:不管父容器是 block 还是 flex,渐变条都要铺满整宽 */
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 18px 16px 36px;
|
||||||
|
border-radius: 0 0 22px 22px;
|
||||||
|
background: linear-gradient(135deg, #1677ff 0%, #36cfc9 100%);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 6px 18px rgba(22, 119, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-inner {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.is-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
color: #1677ff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.hero {
|
||||||
|
padding: 22px 20px 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.hero-inner {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding: 24px 24px 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user