refactor: 将组件前缀从Art统一替换为Fa

- 更新所有FA前缀的组件文件
- 新增Fa前缀的对应组件
- 更新所有引用路径和组件名
- 修改相关样式和变量名
- 调整部分组件目录结构
This commit is contained in:
zhangtao
2026-05-11 09:23:31 +08:00
parent 68b50359c5
commit 03a8151513
185 changed files with 2912 additions and 1283 deletions
@@ -0,0 +1,56 @@
<template>
<div class="page-content !border-0 !bg-transparent min-h-screen flex-cc">
<div class="flex-cc max-md:!block max-md:text-center">
<FaThemeSvg :src="data.imgUrl" size="100%" class="!w-100" />
<div class="ml-15 w-75 max-md:mx-auto max-md:mt-10 max-md:w-full max-md:text-center">
<p class="text-xl leading-7 text-g-600 max-md:text-lg">{{ data.desc }}</p>
<ElButton type="primary" size="large" @click="backHome" v-ripple class="mt-5">
{{ data.btnText }}
</ElButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useCommon } from "@/hooks/core/useCommon";
import { useUserStore } from "@stores/modules/user.store";
import FaThemeSvg from "@/components/theme/fa-theme-svg/index.vue";
const router = useRouter();
const userStore = useUserStore();
interface ExceptionData {
/** 标题 */
title: string;
/** 描述 */
desc: string;
/** 按钮文本 */
btnText: string;
/** 图片地址 */
imgUrl: string;
}
withDefaults(
defineProps<{
data: ExceptionData;
}>(),
{}
);
const { homePath } = useCommon();
const backHome = () => {
const targetHomePath = homePath.value || "/";
if (!userStore.isLogin) {
router.push({
name: "Login",
query: { redirect: targetHomePath },
});
return;
}
router.push(targetHomePath);
};
</script>
@@ -0,0 +1,324 @@
<!-- 授权页顶栏:左上 Logo / 标题 / 版本(固定),右上操作(固定);切换布局时仅下方主体变化 -->
<template>
<header
class="auth-top-bar pointer-events-none fixed left-0 right-0 top-0 z-[100] flex items-center justify-between gap-3 bg-transparent px-5 py-[1.125rem] md:gap-4 md:px-10"
>
<div class="pointer-events-auto flex min-w-0 flex-1 items-center gap-3">
<FaLogo class="icon shrink-0" size="46" :src="webLogoSrc" />
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<h1 class="auth-top-bar__site-title">{{ siteTitle }}</h1>
<div class="logo-version-badge shrink-0" :title="displayVersion">
<span class="logo-version-pill">{{ displayVersion }}</span>
</div>
</div>
</div>
</div>
<div
class="auth-top-bar-actions-panel pointer-events-auto flex shrink-0 flex-cc gap-1.5 px-2 py-1.5 max-sm:mr-1"
>
<div class="color-picker-expandable relative flex-c max-sm:!hidden">
<div
class="color-dots absolute right-0 rounded-full flex-c gap-2 rounded-5 px-2.5 py-2 pr-9 pl-2.5 opacity-0"
>
<div
v-for="(_color, index) in mainColors"
:key="_color"
class="color-dot relative size-5 c-p flex-cc rounded-full opacity-0"
:class="{ active: _color === systemThemeColor }"
:style="{ background: _color, '--index': index }"
@click="changeThemeColor(_color)"
>
<FaSvgIcon v-if="_color === systemThemeColor" icon="ri:check-fill" class="text-white" />
</div>
</div>
<div
class="btn palette-btn auth-top-bar__action relative z-[2] h-8 w-8 c-p flex-cc tad-300"
>
<FaSvgIcon icon="ri:palette-line" class="text-xl transition-colors duration-300" />
</div>
</div>
<ElDropdown
v-if="panelAlign != null"
@command="onPanelAlign"
popper-class="langDropDownStyle"
>
<div
class="btn layout-align-btn auth-top-bar__action h-8 w-8 c-p flex-cc tad-300"
:title="$t('login.panelAlign.label')"
>
<FaSvgIcon
:icon="panelAlignTriggerIcon"
class="text-xl text-g-800 transition-colors duration-300"
/>
</div>
<template #dropdown>
<ElDropdownMenu>
<div v-for="opt in layoutAlignOptions" :key="opt.value" class="lang-btn-item">
<ElDropdownItem
:command="opt.value"
:class="{ 'is-selected': panelAlign === opt.value }"
>
<FaSvgIcon :icon="opt.icon" class="mr-2 text-base" />
<span class="menu-txt">{{ $t(opt.labelKey) }}</span>
<FaSvgIcon icon="ri:check-fill" class="text-base" v-if="panelAlign === opt.value" />
</ElDropdownItem>
</div>
</ElDropdownMenu>
</template>
</ElDropdown>
<ElDropdown
v-if="shouldShowLanguage"
@command="changeLanguage"
popper-class="langDropDownStyle"
>
<div class="btn language-btn auth-top-bar__action h-8 w-8 c-p flex-cc tad-300">
<FaSvgIcon
icon="ri:translate-2"
class="text-[19px] text-g-800 transition-colors duration-300"
/>
</div>
<template #dropdown>
<ElDropdownMenu>
<div v-for="lang in languageOptions" :key="lang.value" class="lang-btn-item">
<ElDropdownItem
:command="lang.value"
:class="{ 'is-selected': locale === lang.value }"
>
<span class="menu-txt">{{ lang.label }}</span>
<FaSvgIcon icon="ri:check-fill" class="text-base" v-if="locale === lang.value" />
</ElDropdownItem>
</div>
</ElDropdownMenu>
</template>
</ElDropdown>
<div
v-if="shouldShowThemeToggle"
class="btn theme-btn auth-top-bar__action h-8 w-8 c-p flex-cc tad-300"
@click="themeAnimation"
>
<FaSvgIcon
:icon="isDark ? 'ri:sun-fill' : 'ri:moon-line'"
class="text-xl text-g-800 transition-colors duration-300"
/>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useSettingsStore } from "@stores/modules/setting.store";
import { useUserStore } from "@stores/modules/user.store";
import { useHeaderBar } from "@/hooks/core/useHeaderBar";
import { themeAnimation } from "@utils/ui";
import { languageOptions } from "@/locales";
import { LanguageEnum } from "@/enums/appEnum";
import AppConfig from "@/config";
import { useConfigStore } from "@stores/modules/config.store";
import type { LoginPanelAlign } from "@views/module_system/auth/composables/useLoginPanelAlign";
defineOptions({ name: "AuthTopBar" });
const DEFAULT_APP_VERSION = "2.0.0";
const props = defineProps<{
/** 登录区表单水平对齐;未传入时不展示布局切换 */
panelAlign?: LoginPanelAlign | null;
}>();
const emit = defineEmits<{
"update:panelAlign": [value: LoginPanelAlign];
}>();
const layoutAlignOptions: {
value: LoginPanelAlign;
icon: string;
labelKey: string;
}[] = [
{ value: "left", icon: "ri:layout-left-2-line", labelKey: "login.panelAlign.left" },
{ value: "center", icon: "ri:layout-column-line", labelKey: "login.panelAlign.center" },
{ value: "right", icon: "ri:layout-right-2-line", labelKey: "login.panelAlign.right" },
];
/** 与当前选中项同一套 icon,避免触发器与菜单不一致 */
const panelAlignTriggerIcon = computed(() => {
const opt = layoutAlignOptions.find((o) => o.value === props.panelAlign);
return opt?.icon ?? "ri:layout-column-line";
});
function onPanelAlign(cmd: string) {
if (cmd === "left" || cmd === "center" || cmd === "right") {
emit("update:panelAlign", cmd);
}
}
const configStore = useConfigStore();
const settingStore = useSettingsStore();
const userStore = useUserStore();
const { isDark, systemThemeColor } = storeToRefs(settingStore);
const { shouldShowThemeToggle, shouldShowLanguage } = useHeaderBar();
const { locale } = useI18n();
const mainColors = AppConfig.systemMainColor;
/** 与 Element 主题主色同步,供调色盘图标与展开态使用 */
const themeColorForCss = computed(() => systemThemeColor.value);
const webLogoSrc = computed(
() => configStore.configData.sys_web_logo?.config_value?.trim() || undefined
);
const siteTitle = computed(
() => configStore.configData.sys_web_title?.config_value?.trim() || AppConfig.systemInfo.name
);
const displayVersion = computed(() => {
const raw = configStore.configData.sys_web_version?.config_value?.trim();
const ver = raw || DEFAULT_APP_VERSION;
return ver.startsWith("v") || ver.startsWith("V") ? ver : `v${ver}`;
});
const changeLanguage = (lang: LanguageEnum) => {
if (locale.value === lang) return;
locale.value = lang;
userStore.setLanguage(lang);
};
const changeThemeColor = (color: string) => {
if (systemThemeColor.value === color) return;
settingStore.setElementTheme(color);
settingStore.reload();
};
</script>
<style scoped>
.auth-top-bar__site-title {
max-width: min(52vw, 28rem);
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: clamp(1rem, 2.2vw, 1.25rem);
font-weight: 600;
line-height: 1.35;
color: var(--el-text-color-primary);
letter-spacing: -0.02em;
white-space: nowrap;
}
.logo-version-pill {
display: inline-block;
padding: 0.28rem 0.6rem;
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.15;
color: var(--el-color-primary);
letter-spacing: 0.02em;
background: color-mix(in srgb, var(--el-color-primary) 11%, transparent);
border: 1px solid color-mix(in srgb, var(--el-color-primary) 28%, transparent);
border-radius: 999px;
}
/* 右上角操作的整体衬底 */
.auth-top-bar-actions-panel {
background-color: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color-lighter);
/* 胶囊形:左右两端为半圆弧 */
border-radius: 9999px;
box-shadow: 0 2px 12px rgb(0 0 0 / 6%);
backdrop-filter: blur(10px);
}
.dark .auth-top-bar-actions-panel {
background-color: rgb(255 255 255 / 8%);
border-color: rgb(255 255 255 / 12%);
box-shadow: 0 4px 20px rgb(0 0 0 / 35%);
}
/* 右上角三个操作按钮:悬浮抬升 + 浅底 + 图标随主色 */
.auth-top-bar__action {
border-radius: 10px;
transition:
background-color 0.22s ease,
transform 0.22s ease,
box-shadow 0.22s ease;
}
.auth-top-bar__action:hover {
background-color: var(--el-fill-color-light);
box-shadow: 0 4px 14px rgb(0 0 0 / 8%);
transform: translateY(-2px);
}
.auth-top-bar__action:hover :deep(.fa-svg-icon) {
color: var(--el-color-primary);
}
.auth-top-bar__action:active {
box-shadow: 0 2px 6px rgb(0 0 0 / 6%);
transform: translateY(0);
transition-duration: 0.12s;
}
.dark .auth-top-bar__action:hover {
background-color: rgb(255 255 255 / 10%);
box-shadow: 0 4px 18px rgb(0 0 0 / 45%);
}
.color-dots {
pointer-events: none;
box-shadow: 0 2px 12px var(--fa-gray-300);
backdrop-filter: blur(10px);
transform: translateX(10px);
transition:
opacity 0.3s ease,
transform 0.3s ease;
}
.color-dot {
box-shadow: 0 2px 4px rgb(0 0 0 / 15%);
transform: translateX(20px) scale(0.8);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transition-delay: calc(var(--index) * 0.05s);
}
/* 仅展开调色条后,单颗色块悬浮:描边 + 略放大 */
.color-picker-expandable:hover .color-dot:hover {
z-index: 1;
box-shadow:
0 4px 12px rgb(0 0 0 / 28%),
0 0 0 2px rgb(255 255 255 / 88%);
transform: translateX(0) scale(1.16);
}
.color-picker-expandable:hover .color-dots {
pointer-events: auto;
opacity: 1;
transform: translateX(0);
}
.color-picker-expandable:hover .color-dot {
opacity: 1;
transform: translateX(0) scale(1);
}
.dark .color-dots {
background-color: var(--fa-gray-200);
box-shadow: none;
}
/* 调色盘:图标颜色与当前主题主色一致(含单独悬浮、整块调色区悬浮) */
.palette-btn :deep(.fa-svg-icon) {
color: v-bind("themeColorForCss");
}
.auth-top-bar__action.palette-btn:hover :deep(.fa-svg-icon) {
color: v-bind("themeColorForCss");
}
.color-picker-expandable:hover .palette-btn :deep(.fa-svg-icon) {
color: v-bind("themeColorForCss");
}
</style>
@@ -0,0 +1,85 @@
<!-- 居中布局:与左侧栏一致的浅色底 + 中部淡化插画(不参与交互) -->
<template>
<div
class="login-center-backdrop"
:class="{ 'login-center-backdrop--viewport-fixed': viewportFixed }"
aria-hidden="true"
>
<div class="login-center-backdrop__bg" />
<div class="login-center-backdrop__hero-wrap">
<FaThemeSvg :src="loginIcon" size="100%" class="login-center-backdrop__hero" />
</div>
</div>
</template>
<script setup lang="ts">
import loginIcon from "@imgs/svg/login_icon.svg";
import FaThemeSvg from "@/components/theme/fa-theme-svg/index.vue";
defineOptions({ name: "LoginCenterBackdrop" });
withDefaults(
defineProps<{
/** 铺满视口并置于底层(与固定顶栏配合) */
viewportFixed?: boolean;
}>(),
{ viewportFixed: false }
);
</script>
<style scoped lang="scss">
.login-center-backdrop {
position: absolute;
inset: 0;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.login-center-backdrop--viewport-fixed {
position: fixed;
inset: 0;
z-index: 0;
}
.login-center-backdrop__bg {
position: absolute;
inset: 0;
background-color: color-mix(
in srgb,
var(--el-color-primary-light-9) 100%,
var(--default-box-color)
);
}
.login-center-backdrop__hero-wrap {
position: absolute;
top: 12%;
left: 50%;
width: min(420px, 52vw);
height: min(340px, 38vh);
opacity: 0.22;
transform: translateX(-50%);
}
.login-center-backdrop__hero {
width: 100%;
height: 100%;
}
html.dark .login-center-backdrop__bg {
background-color: color-mix(in srgb, var(--el-color-primary-light-9) 60%, #070707);
}
html.dark .login-center-backdrop__hero-wrap {
opacity: 0.14;
}
@media only screen and (width <= 1180px) {
.login-center-backdrop__hero-wrap {
top: 10%;
width: min(320px, 72vw);
height: min(240px, 28vh);
}
}
</style>
@@ -0,0 +1,680 @@
<!-- 登录、注册、忘记密码左侧背景 -->
<template>
<div class="login-left-view">
<div v-if="!hideTopBranding" class="logo">
<FaLogo class="icon" size="46" :src="webLogoSrc" />
<div class="logo-title-wrap">
<div class="logo-title-inline">
<h1 class="title">{{ siteTitle }}</h1>
<div class="logo-version-badge" :title="displayVersion">
<span class="logo-version-pill">{{ displayVersion }}</span>
</div>
</div>
</div>
</div>
<div class="left-img">
<FaThemeSvg :src="loginIcon" size="100%" />
</div>
<div class="text-wrap">
<h1>{{ $t("login.leftView.title") }}</h1>
<p>{{ $t("login.leftView.subTitle") }}</p>
</div>
<!-- 几何装饰元素 -->
<div class="geometric-decorations">
<!-- 基础几何形状 -->
<div class="geo-element circle-outline animate-fade-in-up" style="animation-delay: 0s"></div>
<div
class="geo-element square-rotated animate-fade-in-left"
style="animation-delay: 0s"
></div>
<div class="geo-element circle-small animate-fade-in-up" style="animation-delay: 0.3s"></div>
<div
class="geo-element square-bottom-right animate-fade-in-right"
style="animation-delay: 0s"
></div>
<!-- 背景泡泡 -->
<div class="geo-element bg-bubble animate-scale-in" style="animation-delay: 0.5"></div>
<!-- 太阳/月亮 -->
<div
class="geo-element circle-top-right animate-fade-in-down"
style="animation-delay: 0.5"
@click="themeAnimation"
></div>
<!-- 装饰点 -->
<div class="geo-element dot dot-top-left animate-bounce-in" style="animation-delay: 0s"></div>
<div
class="geo-element dot dot-top-right animate-bounce-in"
style="animation-delay: 0s"
></div>
<div
class="geo-element dot dot-center-right animate-bounce-in"
style="animation-delay: 0s"
></div>
<!-- 叠加方块组 -->
<div class="squares-group">
<i
class="geo-element square square-blue animate-fade-in-left-rotated-blue"
style="animation-delay: 0.2s"
></i>
<i
class="geo-element square square-pink animate-fade-in-left-rotated-pink"
style="animation-delay: 0.4s"
></i>
<i
class="geo-element square square-purple animate-fade-in-left-no-rotation"
style="animation-delay: 0.6s"
></i>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import AppConfig from "@/config";
import loginIcon from "@imgs/svg/login_icon.svg";
import { useConfigStore } from "@stores/modules/config.store";
import { themeAnimation } from "@utils/ui";
import FaThemeSvg from "@/components/theme/fa-theme-svg/index.vue";
defineProps<{
hideContent?: boolean;
/** 顶栏已展示 Logo/标题/版本时隐藏左侧重复顶栏 */
hideTopBranding?: boolean;
}>();
const configStore = useConfigStore();
/** 接口 sys_web_logo,空则 FaLogo 内置默认图 */
const webLogoSrc = computed(
() => configStore.configData.sys_web_logo?.config_value?.trim() || undefined
);
const siteTitle = computed(
() => configStore.configData.sys_web_title?.config_value?.trim() || AppConfig.systemInfo.name
);
const DEFAULT_APP_VERSION = "2.0.0";
const displayVersion = computed(() => {
const raw = configStore.configData.sys_web_version?.config_value?.trim();
const ver = raw || DEFAULT_APP_VERSION;
return ver.startsWith("v") || ver.startsWith("V") ? ver : `v${ver}`;
});
</script>
<style lang="scss" scoped>
// 颜色变量定义
$primary-light-7: var(--el-color-primary-light-7);
$primary-light-8: var(--el-color-primary-light-8);
$primary-light-9: var(--el-color-primary-light-9);
$primary-base: var(--el-color-primary);
$main-bg: var(--default-box-color);
// 混合颜色函数
$bg-mix-light-9: color-mix(in srgb, $primary-light-9 100%, $main-bg);
$bg-mix-light-8: color-mix(in srgb, $primary-light-8 80%, $main-bg);
$bg-mix-light-7: color-mix(in srgb, $primary-light-7 80%, $main-bg);
.login-left-view {
position: relative;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 15px;
overflow: hidden;
background-color: $bg-mix-light-9;
.logo {
position: relative;
z-index: 100;
display: flex;
align-items: center;
.logo-title-wrap {
flex: 1;
min-width: 0;
margin-left: 10px;
}
/** 标题与版本:垂直居中对齐(上下在同一水平中线),长标题可自然换行 */
.logo-title-inline {
display: flex;
flex-flow: row wrap;
gap: 0.45rem;
align-items: center;
min-width: 0;
max-width: 100%;
}
.title {
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
margin: 0;
font-size: clamp(1.05rem, 1.6vw, 1.25rem);
font-weight: 600;
line-height: 1.35;
color: var(--el-text-color-primary);
letter-spacing: -0.02em;
}
.logo-version-badge {
flex-shrink: 0;
}
.logo-version-pill {
display: inline-block;
padding: 0.28rem 0.6rem;
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.15;
color: var(--el-color-primary);
letter-spacing: 0.02em;
background: color-mix(in srgb, var(--el-color-primary) 11%, transparent);
border: 1px solid color-mix(in srgb, var(--el-color-primary) 28%, transparent);
border-radius: 999px;
box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
transition:
border-color 0.2s ease,
background 0.2s ease;
}
}
.left-img {
position: absolute;
inset: 0 0 10.5%;
z-index: 10;
width: 40%;
margin: auto;
animation: slideInLeft 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
}
.text-wrap {
position: absolute;
bottom: 80px;
width: 100%;
text-align: center;
animation: slideInLeft 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
h1 {
font-size: 24px;
font-weight: 400;
color: var(--fa-gray-900) !important;
}
p {
margin-top: 10px;
font-size: 14px;
color: var(--fa-gray-600) !important;
}
}
.geometric-decorations {
.geo-element {
position: absolute;
opacity: 0;
animation-fill-mode: forwards;
animation-duration: 0.8s;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
// 动画 mixin
@mixin fadeAnimation($direction: "", $rotation: 0deg) {
from {
opacity: 0;
@if $direction == "up" {
transform: translateY(30px) rotate($rotation);
}
@else if $direction == "down" {
transform: translateY(-30px) rotate($rotation);
}
@else if $direction == "left" {
transform: translateX(-30px) rotate($rotation);
}
@else if $direction == "right" {
transform: translateX(30px) rotate($rotation);
}
}
to {
opacity: 1;
@if $direction == "up" or $direction == "down" {
transform: translateY(0) rotate($rotation);
}
@else {
transform: translateX(0) rotate($rotation);
}
}
}
// 动画定义
@keyframes fadeInUp {
@include fadeAnimation("up");
}
@keyframes fadeInDown {
@include fadeAnimation("down");
}
@keyframes fadeInLeft {
@include fadeAnimation("left");
}
@keyframes fadeInLeftRotated {
@include fadeAnimation("left", -25deg);
}
@keyframes fadeInRight {
@include fadeAnimation("right");
}
@keyframes fadeInRightRotated {
@include fadeAnimation("right", 45deg);
}
@keyframes fadeInLeftRotatedBlue {
@include fadeAnimation("left", -10deg);
}
@keyframes fadeInLeftRotatedPink {
@include fadeAnimation("left", 10deg);
}
@keyframes fadeInLeftNoRotation {
@include fadeAnimation("left");
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes lineGrow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 动画类
.animate-fade-in-up {
animation-name: fadeInUp;
}
.animate-fade-in-down {
animation-name: fadeInDown;
}
.animate-fade-in-left {
animation-name: fadeInLeft;
}
.animate-fade-in-right {
animation-name: fadeInRight;
}
.animate-scale-in {
animation-name: scaleIn;
animation-duration: 1.2s;
}
.animate-bounce-in {
animation-name: bounceIn;
animation-duration: 0.6s;
}
.animate-fade-in-left-rotated-blue {
animation-name: fadeInLeftRotatedBlue;
}
.animate-fade-in-left-rotated-pink {
animation-name: fadeInLeftRotatedPink;
}
.animate-fade-in-left-no-rotation {
animation-name: fadeInLeftNoRotation;
}
// 基础几何形状
.circle-outline {
top: 10%;
left: 25%;
width: 42px;
height: 42px;
border: 2px solid $primary-light-8;
border-radius: 50%;
}
.square-rotated {
top: 50%;
left: 16%;
width: 60px;
height: 60px;
background-color: $bg-mix-light-8;
&.animate-fade-in-left {
animation-name: fadeInLeftRotated;
}
}
.circle-small {
bottom: 26%;
left: 30%;
width: 18px;
height: 18px;
background-color: $primary-light-8;
border-radius: 50%;
}
// 太阳/月亮效果
.circle-top-right {
top: 3%;
right: 3%;
z-index: 100;
width: 50px;
height: 50px;
cursor: pointer;
background: $bg-mix-light-7;
border-radius: 50%;
transition: all 0.3s;
&::after {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
content: "";
background: linear-gradient(to right, #fcbb04, #fffc00);
border-radius: 50%;
opacity: 0;
transform: translate(-50%, -50%);
transition: all 0.5s;
}
&:hover {
box-shadow: 0 0 36px #fffc00;
&::after {
opacity: 1;
}
}
}
.square-bottom-right {
right: 10%;
bottom: 10%;
width: 50px;
height: 50px;
background-color: $primary-light-8;
&.animate-fade-in-right {
animation-name: fadeInRightRotated;
}
}
// 背景泡泡
.bg-bubble {
top: -120px;
right: -120px;
width: 360px;
height: 360px;
background-color: $bg-mix-light-8;
border-radius: 50%;
}
// 装饰点
.dot {
width: 14px;
height: 14px;
background-color: $primary-light-7;
border-radius: 50%;
&.dot-top-left {
top: 140px;
left: 100px;
}
&.dot-top-right {
top: 140px;
right: 120px;
}
&.dot-center-right {
top: 46%;
right: 22%;
background-color: $primary-light-8;
}
}
// 叠加方块组
.squares-group {
position: absolute;
bottom: 18px;
left: 20px;
width: 140px;
height: 140px;
pointer-events: none;
.square {
position: absolute;
display: block;
border-radius: 8px;
box-shadow: 0 8px 24px rgb(64 87 167 / 12%);
&.square-blue {
top: 12px;
left: 30px;
z-index: 2;
width: 50px;
height: 50px;
background-color: rgb(from $primary-base r g b / 30%);
}
&.square-pink {
top: 30px;
left: 48px;
z-index: 1;
width: 70px;
height: 70px;
background-color: rgb(from $primary-base r g b / 15%);
}
&.square-purple {
top: 66px;
left: 86px;
z-index: 3;
width: 32px;
height: 32px;
background-color: rgb(from $primary-base r g b / 45%);
}
}
// 装饰线条
&::after {
position: absolute;
top: 86px;
left: 72px;
width: 80px;
height: 1px;
content: "";
background: linear-gradient(90deg, var(--el-color-primary-light-6), transparent);
opacity: 0;
transform: rotate(50deg);
animation: lineGrow 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: 1.2s;
}
}
}
@media only screen and (width <= 1600px) {
.text-wrap {
bottom: 40px;
}
}
@media only screen and (width <= 1180px) {
width: auto;
height: auto;
padding: 0;
// 隐藏背景和其他内容,只保留 logo
background: transparent;
.left-img,
.text-wrap,
.geometric-decorations {
display: none;
}
.logo {
display: none;
}
}
}
// 暗色主题
.dark .login-left-view {
background-color: color-mix(in srgb, $primary-light-9 60%, #070707);
@media only screen and (width <= 1180px) {
background: transparent;
}
.geometric-decorations {
// 月亮效果
.circle-top-right {
background-color: $bg-mix-light-8;
box-shadow: 0 0 25px #333 inset;
rotate: -48deg;
transition: all 0.3s ease-in-out 0.1s;
&::before {
position: absolute;
top: 0;
left: 15px;
width: 50px;
height: 50px;
content: "";
background-color: $bg-mix-light-9;
border-radius: 50%;
transition: all 0.3s ease-in-out;
}
&:hover {
background-color: transparent;
box-shadow: 0 40px 25px #ddd inset;
&::before {
left: 18px;
}
&::after {
opacity: 0;
}
}
}
.bg-bubble {
background-color: $bg-mix-light-9;
}
// 其他元素颜色调整
.square-rotated {
background-color: $bg-mix-light-9;
}
.circle-small,
.dot {
background-color: $primary-light-8;
}
.square-bottom-right {
background-color: $primary-light-9;
}
.dot.dot-top-right {
background-color: $primary-light-8;
}
}
// 方块组暗色调整
.squares-group {
.square {
box-shadow: none;
&.square-blue {
background-color: rgb(from $primary-base r g b / 18%);
}
&.square-pink {
background-color: rgb(from $primary-base r g b / 10%);
}
&.square-purple {
background-color: rgb(from $primary-base r g b / 20%);
}
}
&::after {
background: linear-gradient(90deg, $primary-light-8, transparent);
}
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="page-content box-border !px-20 py-3.5 text-center max-md:!px-5" :class="type">
<FaSvgIcon
class="icon size-22 p-2 mt-16 block rounded-full !text-white"
:icon="iconCode"
:class="type === 'success' ? 'bg-[#19BE6B]' : 'bg-[#ED4014]'"
/>
<h1 class="title mt-8 text-3xl font-medium !text-g-900 max-md:mt-2.5 max-md:text-2xl">
{{ title }}
</h1>
<p class="msg mt-5 text-base text-g-600">{{ message }}</p>
<div
class="res mt-7.5 rounded bg-g-200/80 dark:bg-g-300/40 px-7.5 py-5.5 text-left max-md:px-7.5 max-md:py-2.5 [&_p]:flex [&_p]:items-center [&_p]:py-2 [&_p]:text-sm [&_p]:text-[#808695] [&_p_i]:mr-1.5"
>
<slot name="content"></slot>
</div>
<div class="btn-group mt-12.5">
<slot name="buttons"></slot>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: "FaResultPage" });
// 显式声明插槽类型
defineSlots<{
default?: () => void;
content?: () => void;
buttons?: () => void;
}>();
interface ResultPageProps {
/** 成功/失败 */
type: "success" | "fail";
/** 标题 */
title: string;
/** 消息 */
message: string;
/** 图标 */
iconCode: string;
}
withDefaults(defineProps<ResultPageProps>(), {
type: "success",
title: "",
message: "",
iconCode: "",
});
</script>