<script setup lang="ts">
|
import type { ToolbarType } from './types';
|
|
import { computed } from 'vue';
|
|
import { preferences, usePreferences } from '../../../../preferences/src';
|
|
import { Copyright } from '../basic/copyright';
|
import AuthenticationFormView from './form.vue';
|
import Toolbar from './toolbar.vue';
|
|
interface Props {
|
appName?: string;
|
logo?: string;
|
logoDark?: string;
|
pageTitle?: string;
|
pageDescription?: string;
|
sloganImage?: string;
|
toolbar?: boolean;
|
copyright?: boolean;
|
toolbarList?: ToolbarType[];
|
clickLogo?: () => void;
|
}
|
|
const props = withDefaults(defineProps<Props>(), {
|
appName: '',
|
copyright: true,
|
logo: '',
|
logoDark: '',
|
pageDescription: '',
|
pageTitle: '',
|
sloganImage: '',
|
toolbar: true,
|
toolbarList: () => ['color', 'language', 'layout', 'theme'],
|
clickLogo: () => {},
|
});
|
|
const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
|
usePreferences();
|
|
/**
|
* @zh_CN 根据主题选择合适的 logo 图标
|
*/
|
const logoSrc = computed(() => {
|
if (isDark.value && props.logoDark) {
|
return props.logoDark;
|
}
|
return props.logo;
|
});
|
|
</script>
|
|
<template>
|
<div
|
:class="[isDark ? 'dark' : '']"
|
class="agri-auth flex min-h-full flex-1 overflow-x-hidden select-none"
|
>
|
<template v-if="toolbar">
|
<slot name="toolbar">
|
<Toolbar :toolbar-list="toolbarList" />
|
</slot>
|
</template>
|
<!-- 左侧认证面板 -->
|
<AuthenticationFormView
|
v-if="authPanelLeft"
|
class="agri-left-panel min-h-full w-2/5 flex-1"
|
data-side="left"
|
>
|
<template v-if="copyright" #copyright>
|
<slot name="copyright">
|
<Copyright
|
v-if="preferences.copyright.enable"
|
v-bind="preferences.copyright"
|
/>
|
</slot>
|
</template>
|
</AuthenticationFormView>
|
|
<slot name="logo">
|
<!-- 头部 Logo 和应用名称(小屏展示,大屏由品牌区代替) -->
|
<div
|
v-if="logoSrc || appName"
|
class="agri-mini-logo absolute top-0 left-0 z-10 flex flex-1"
|
@click="clickLogo"
|
>
|
<div
|
class="mt-4 ml-4 flex flex-1 items-center text-foreground sm:top-6 sm:left-6 lg:text-foreground"
|
>
|
<img
|
v-if="logoSrc"
|
:key="logoSrc"
|
:alt="appName"
|
:src="logoSrc"
|
class="mr-2"
|
width="42"
|
/>
|
<p v-if="appName" class="m-0 text-xl font-medium">
|
{{ appName }}
|
</p>
|
</div>
|
</div>
|
</slot>
|
|
<!-- 系统介绍 -->
|
<div v-if="!authPanelCenter" class="relative hidden w-0 flex-[1.18] lg:block">
|
<div class="agri-promo-layer absolute inset-0 size-full overflow-hidden">
|
<div
|
:key="authPanelLeft ? 'left' : authPanelRight ? 'right' : 'center'"
|
class="agri-promo relative flex h-full flex-col items-end justify-start px-10 pt-16 text-left"
|
>
|
<!-- 品牌区 -->
|
<div class="agri-brand">
|
<div class="flex items-center gap-4">
|
<img
|
src="/images/tahe-logo.png"
|
alt="塔河种业"
|
class="h-16 w-auto shrink-0 rounded-xl bg-white/95 p-1.5 shadow-lg"
|
/>
|
<div>
|
<div class="agri-brand-title text-3xl font-semibold tracking-[0.14em] xl:text-4xl">
|
{{ pageTitle }}
|
</div>
|
<div class="agri-brand-sub mt-2 text-xs tracking-[0.3em] xl:text-sm">
|
AGRICULTURE DIGITAL PLATFORM
|
</div>
|
</div>
|
</div>
|
<div class="agri-brand-tag mt-6 flex items-center justify-end gap-4">
|
<span class="agri-brand-tag-line h-px w-12"></span>
|
<span class="text-base tracking-[0.22em]">{{ pageDescription }}</span>
|
<span class="agri-brand-tag-line h-px w-12"></span>
|
</div>
|
</div>
|
|
</div>
|
|
</div>
|
</div>
|
|
<!-- 中心认证面板 -->
|
<div v-if="authPanelCenter" class="relative flex-center w-full">
|
<div class="login-background absolute top-0 left-0 size-full"></div>
|
<AuthenticationFormView
|
class="w-full rounded-3xl border border-border/60 pb-20 shadow-float shadow-primary/10 md:w-2/3 md:bg-background/95 lg:w-1/2 xl:w-[36%]"
|
data-side="bottom"
|
>
|
<template v-if="copyright" #copyright>
|
<slot name="copyright">
|
<Copyright
|
v-if="preferences.copyright.enable"
|
v-bind="preferences.copyright"
|
/>
|
</slot>
|
</template>
|
</AuthenticationFormView>
|
</div>
|
|
<!-- 右侧认证面板 -->
|
<div v-if="authPanelRight" class="agri-form-wrap relative">
|
<AuthenticationFormView class="agri-form-panel min-h-full w-full" data-side="right" />
|
|
<!-- 卡片右上树叶装饰 -->
|
<svg
|
class="pointer-events-none absolute top-0 right-0 h-36 w-40"
|
viewBox="0 0 180 150"
|
aria-hidden="true"
|
>
|
<path d="M180 4c-26 22-38 52-36 92" fill="none" stroke="#4ade80" stroke-width="2" stroke-linecap="round" />
|
<g fill="#4ade80" opacity="0.85">
|
<path d="M160 22q-16-10-30-2 12 12 30 2Z" />
|
<path d="M150 44q-18-6-30 4 14 10 30-4Z" />
|
<path d="M146 70q-18-2-28 10 16 7 28-10Z" />
|
<path d="M146 96q-16 2-24 14 16 4 24-14Z" />
|
</g>
|
<g fill="#86efac" opacity="0.7">
|
<path d="M164 34q4-16 18-22 2 16-18 22Z" />
|
<path d="M156 60q6-14 20-18 0 16-20 18Z" />
|
<path d="M152 86q8-12 22-14-2 16-22 14Z" />
|
</g>
|
<g fill="#bbf7d0" opacity="0.45">
|
<path d="M96 118q-14-8-26-1 10 10 26 1Z" />
|
<path d="M70 136q-12-6-22 0 9 8 22 0Z" />
|
</g>
|
</svg>
|
|
<!-- 卡片底部农场插画 -->
|
<svg
|
class="agri-farm pointer-events-none absolute inset-x-0 bottom-0 h-24 w-full"
|
viewBox="0 0 560 130"
|
preserveAspectRatio="xMidYMax slice"
|
aria-hidden="true"
|
>
|
<path d="M0 92 Q120 62 260 88 T560 80 V130 H0 Z" fill="#dcfce7" />
|
<path d="M0 112 Q160 88 320 110 T560 104 V130 H0 Z" fill="#bbf7d0" />
|
<g fill="#a7f3d0">
|
<path d="M36 76 L58 58 L80 76 Z" />
|
<rect x="42" y="76" width="32" height="22" fill="#ecfdf5" />
|
<rect x="54" y="84" width="9" height="14" fill="#86efac" />
|
</g>
|
<g>
|
<rect x="116" y="78" width="4" height="16" fill="#86efac" />
|
<circle cx="118" cy="70" r="12" fill="#86efac" />
|
<rect x="146" y="84" width="3.5" height="12" fill="#6ee7b7" />
|
<circle cx="148" cy="78" r="8" fill="#6ee7b7" />
|
</g>
|
<g stroke="#6ee7b7" stroke-width="3" stroke-linecap="round">
|
<path d="M470 100V64" />
|
<path d="M470 64l-14-10M470 64l16-6M470 64l-2 18" />
|
</g>
|
<g fill="#a7f3d0">
|
<path d="M498 82 L514 68 L530 82 Z" />
|
<rect x="503" y="82" width="22" height="16" fill="#ecfdf5" />
|
</g>
|
</svg>
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
.agri-auth {
|
position: relative;
|
min-height: 100dvh;
|
overflow-y: auto;
|
background-color: #124b39;
|
background-image: url('/agriculture-login-bg.png');
|
background-position: center;
|
background-size: cover;
|
isolation: isolate;
|
}
|
|
.login-background {
|
background-image: none;
|
filter: saturate(1.06) contrast(1.01);
|
}
|
|
.agri-left-panel,
|
.agri-left-panel :deep(*) {
|
background-color: transparent !important;
|
}
|
|
/* 品牌文字:白色 + 文字阴影保证在背景图上的可读性 */
|
.agri-brand-title {
|
color: #fff;
|
text-shadow: 0 2px 10px rgba(2, 44, 34, 0.6);
|
}
|
|
.agri-brand-sub {
|
color: rgba(240, 253, 250, 0.92);
|
text-shadow: 0 1px 6px rgba(2, 44, 34, 0.55);
|
}
|
|
.agri-brand-tag {
|
color: rgba(240, 253, 250, 0.92);
|
text-shadow: 0 1px 6px rgba(2, 44, 34, 0.55);
|
}
|
|
.agri-brand-tag-line { background: rgba(167, 243, 208, 0.8); }
|
|
/* 大屏隐藏左上角小 logo,由品牌区展示 */
|
@media (min-width: 1024px) {
|
.agri-mini-logo {
|
display: none;
|
}
|
}
|
|
/* ---------- 品牌与内容 ---------- */
|
.agri-promo {
|
max-width: 780px;
|
margin-left: clamp(2rem, 6vw, 7rem);
|
padding-top: 3rem;
|
padding-bottom: 4.5rem;
|
}
|
|
/* ---------- 右侧登录卡片 ---------- */
|
.agri-form-wrap {
|
align-self: center;
|
max-height: calc(100dvh - 5rem);
|
margin: 2.5rem clamp(1rem, 4vw, 4.5rem) 2.5rem 1.5rem;
|
width: 40%;
|
flex: 0.82 1 0%;
|
/* 外层统一圆角裁剪,避免底部插画等兄弟装饰把卡片直角化 */
|
overflow: hidden;
|
border-radius: 1.75rem;
|
box-shadow: 0 24px 70px rgba(2, 44, 34, 0.24);
|
}
|
|
.agri-form-panel {
|
overflow: auto;
|
border: 1px solid rgba(255, 255, 255, 0.9);
|
border-radius: 1.75rem;
|
background: rgba(250, 253, 251, 0.96) !important;
|
backdrop-filter: blur(18px);
|
}
|
|
/* 表单内容上移,为底部插画留出空间 */
|
.agri-form-wrap :deep(.side-content) {
|
margin-bottom: 4.5rem;
|
}
|
|
/* 标题下的绿色装饰条 */
|
.agri-form-wrap :deep(h2 + p)::after {
|
content: '';
|
display: block;
|
width: 2.75rem;
|
height: 0.28rem;
|
margin-top: 0.8rem;
|
border-radius: 9999px;
|
background: #16a34a;
|
}
|
|
/* 输入框前缀图标 */
|
.agri-form-wrap :deep(input[autocomplete='username']),
|
.agri-form-wrap :deep(input[autocomplete='current-password']) {
|
padding-left: 2.6rem;
|
background-repeat: no-repeat;
|
background-position: 0.85rem center;
|
background-size: 1.05rem;
|
}
|
|
.agri-form-wrap :deep(input[autocomplete='username']) {
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
|
}
|
|
.agri-form-wrap :deep(input[autocomplete='current-password']) {
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E");
|
}
|
|
/* 输入框整体质感:加高、圆角、柔和边框、聚焦绿色光圈 */
|
.agri-form-wrap :deep(input) {
|
height: 3rem;
|
border-radius: 0.75rem;
|
border-color: rgba(15, 118, 110, 0.16);
|
background-color: #fff;
|
font-size: 0.95rem;
|
transition: border-color 200ms ease, box-shadow 200ms ease;
|
}
|
|
.agri-form-wrap :deep(input:hover) {
|
border-color: rgba(16, 185, 129, 0.45);
|
}
|
|
.agri-form-wrap :deep(input:focus),
|
.agri-form-wrap :deep(input:focus-visible) {
|
border-color: #10b981;
|
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.16);
|
outline: none;
|
}
|
|
.dark .agri-form-wrap :deep(input) {
|
border-color: rgba(255, 255, 255, 0.16);
|
background-color: rgba(255, 255, 255, 0.06);
|
}
|
|
.dark .agri-form-wrap :deep(input:focus),
|
.dark .agri-form-wrap :deep(input:focus-visible) {
|
border-color: #34d399;
|
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.2);
|
}
|
|
@media (max-width: 1023px) {
|
.agri-farm {
|
display: none;
|
}
|
}
|
|
/* ---------- 暗色模式 ---------- */
|
.dark .login-background {
|
background-image:
|
linear-gradient(90deg, rgba(2, 26, 20, 0.9), rgba(2, 26, 20, 0.62)),
|
url('/agriculture-login-bg.png');
|
}
|
|
.dark .agri-form-panel {
|
background: rgba(13, 31, 25, 0.96) !important;
|
}
|
|
.dark .agri-form-wrap > svg {
|
opacity: 0.35;
|
}
|
|
/* ---------- 响应式 ---------- */
|
@media (max-width: 1023px) {
|
.agri-promo { margin-left: 2rem; }
|
.agri-form-wrap { margin-right: 1.25rem; }
|
}
|
|
@media (max-height: 720px) and (min-width: 1024px) {
|
.agri-form-wrap {
|
max-height: calc(100dvh - 2rem);
|
margin-top: 1rem;
|
margin-bottom: 1rem;
|
}
|
|
.agri-promo {
|
padding-top: 1.5rem;
|
padding-bottom: 3rem;
|
}
|
|
.agri-promo .mt-12 {
|
margin-top: 2rem;
|
}
|
}
|
|
@media (max-width: 767px) {
|
.agri-form-wrap {
|
align-self: stretch;
|
margin: 1rem;
|
width: auto;
|
flex: none;
|
}
|
|
.agri-form-panel {
|
border-radius: 1.5rem;
|
}
|
}
|
|
/* ---------- 入场动画(纯 CSS,动画结束必然停留于可见态) ---------- */
|
@media (prefers-reduced-motion: no-preference) {
|
.agri-promo {
|
animation: agri-in-left 0.75s cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
}
|
|
.agri-form-wrap {
|
animation: agri-in-right 0.75s cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
}
|
|
}
|
|
@keyframes agri-in-left {
|
from {
|
opacity: 0;
|
transform: translateX(-28px);
|
}
|
}
|
|
@keyframes agri-in-right {
|
from {
|
opacity: 0;
|
transform: translateX(28px);
|
}
|
}
|
|
@keyframes agri-in-up {
|
from {
|
opacity: 0;
|
transform: translateY(18px);
|
}
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
.agri-auth :deep(*) {
|
animation-duration: 0.01ms !important;
|
animation-iteration-count: 1 !important;
|
transition-duration: 0.01ms !important;
|
}
|
}
|
</style>
|