mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-11 11:59:11 +00:00
refactor: 将组件前缀从Art统一替换为Fa
- 更新所有FA前缀的组件文件 - 新增Fa前缀的对应组件 - 更新所有引用路径和组件名 - 修改相关样式和变量名 - 调整部分组件目录结构
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user