chore: update common-ui to universal-ui

This commit is contained in:
vben
2024-06-16 23:20:09 +08:00
parent 95252f62f6
commit 0023964eb7
96 changed files with 107 additions and 95 deletions

View File

@@ -0,0 +1,13 @@
<template>
<div class="mb-7 sm:mx-auto sm:w-full sm:max-w-md">
<h2
class="text-foreground mb-3 text-3xl font-bold leading-9 tracking-tight lg:text-4xl"
>
<slot></slot>
</h2>
<p class="text-muted-foreground lg:text-md text-sm">
<slot name="desc"></slot>
</p>
</div>
</template>

View File

@@ -0,0 +1,157 @@
<script setup lang="ts">
import type { LoginCodeEmits } from './typings';
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { VbenButton, VbenInput, VbenPinInput } from '@vben-core/shadcn-ui';
import Title from './auth-title.vue';
interface Props {
/**
* @zh_CN 是否处于加载处理状态
*/
loading?: boolean;
/**
* @zh_CN 登陆路径
*/
loginPath?: string;
}
defineOptions({
name: 'AuthenticationCodeLogin',
});
const props = withDefaults(defineProps<Props>(), {
loading: false,
loginPath: '/auth/login',
});
const emit = defineEmits<{
submit: LoginCodeEmits['submit'];
}>();
const router = useRouter();
const formState = reactive({
code: '',
phoneNumber: '',
requirePhoneNumber: false,
submitted: false,
});
const countdown = ref(0);
const timer = ref<ReturnType<typeof setTimeout>>();
const isValidPhoneNumber = computed(() => {
return /^1[3-9]\d{9}$/.test(formState.phoneNumber);
});
const btnText = computed(() => {
return countdown.value > 0
? $t('authentication.send-text', [countdown.value])
: $t('authentication.send-code');
});
const btnLoading = computed(() => {
return countdown.value > 0;
});
const phoneNumberStatus = computed(() => {
return (formState.submitted || formState.requirePhoneNumber) &&
!isValidPhoneNumber.value
? 'error'
: 'default';
});
const codeStatus = computed(() => {
return formState.submitted && !formState.code ? 'error' : 'default';
});
function handleSubmit() {
formState.submitted = true;
if (phoneNumberStatus.value !== 'default' || codeStatus.value !== 'default') {
return;
}
emit('submit', {
code: formState.code,
phoneNumber: formState.phoneNumber,
});
}
function goLogin() {
router.push(props.loginPath);
}
async function handleSendCode() {
if (btnLoading.value) {
return;
}
if (!isValidPhoneNumber.value) {
formState.requirePhoneNumber = true;
return;
}
countdown.value = 60;
// TODO: 调用发送验证码接口
startCountdown();
}
function startCountdown() {
if (countdown.value > 0) {
timer.value = setTimeout(() => {
countdown.value--;
startCountdown();
}, 1000);
}
}
onBeforeUnmount(() => {
countdown.value = 0;
clearTimeout(timer.value);
});
</script>
<template>
<div>
<Title>
{{ $t('authentication.welcome-back') }} 📲
<template #desc>
<span class="text-muted-foreground">
{{ $t('authentication.code-subtitle') }}
</span>
</template>
</Title>
<VbenInput
v-model="formState.phoneNumber"
:autofocus="true"
:error-tip="$t('authentication.mobile-tip')"
:label="$t('authentication.mobile')"
:placeholder="$t('authentication.mobile')"
:status="phoneNumberStatus"
name="phoneNumber"
type="number"
@keyup.enter="handleSubmit"
/>
<VbenPinInput
v-model="formState.code"
:btn-loading="btnLoading"
:btn-text="btnText"
:code-length="4"
:error-tip="$t('authentication.code-tip')"
:handle-send-code="handleSendCode"
:label="$t('authentication.code')"
:placeholder="$t('authentication.code')"
:status="codeStatus"
name="password"
@keyup.enter="handleSubmit"
/>
<VbenButton :loading="loading" class="mt-2 w-full" @click="handleSubmit">
{{ $t('common.login') }}
</VbenButton>
<VbenButton class="mt-4 w-full" variant="outline" @click="goLogin()">
{{ $t('common.back') }}
</VbenButton>
</div>
</template>

View File

@@ -0,0 +1,86 @@
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { VbenButton, VbenInput } from '@vben-core/shadcn-ui';
import Title from './auth-title.vue';
interface Props {
/**
* @zh_CN 是否处于加载处理状态
*/
loading?: boolean;
/**
* @zh_CN 登陆路径
*/
loginPath?: string;
}
defineOptions({
name: 'AuthenticationForgetPassword',
});
const props = withDefaults(defineProps<Props>(), {
loading: false,
loginPath: '/auth/login',
});
const emit = defineEmits<{
submit: [string];
}>();
const router = useRouter();
const formState = reactive({
email: '',
submitted: false,
});
const emailStatus = computed(() => {
return formState.submitted && !formState.email ? 'error' : 'default';
});
function handleSubmut() {
formState.submitted = true;
if (emailStatus.value !== 'default') {
return;
}
emit('submit', formState.email);
}
function goLogin() {
router.push(props.loginPath);
}
</script>
<template>
<div>
<Title>
{{ $t('authentication.forget-password') }} 🤦🏻
<template #desc>
{{ $t('authentication.forget-password-subtitle') }}
</template>
</Title>
<div class="mb-6">
<VbenInput
v-model="formState.email"
:error-tip="$t('authentication.email-tip')"
:label="$t('authentication.email')"
:status="emailStatus"
autofocus
name="email"
placeholder="example@example.com"
type="text"
/>
</div>
<div>
<VbenButton class="mt-2 w-full" @click="handleSubmut">
{{ $t('authentication.send-reset-link') }}
</VbenButton>
<VbenButton class="mt-4 w-full" variant="outline" @click="goLogin()">
{{ $t('common.back') }}
</VbenButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,8 @@
export { default as AuthenticationCodeLogin } from './code-login.vue';
export { default as AuthenticationForgetPassword } from './forget-password.vue';
export { default as AuthenticationLogin } from './login.vue';
export { default as AuthenticationQrCodeLogin } from './qrcode-login.vue';
export { default as AuthenticationRegister } from './register.vue';
export type { LoginAndRegisterParams, LoginCodeParams } from './typings';
export { default as AuthenticationColorToggle } from './widgets/color-toggle.vue';
export { default as AuthenticationLayoutToggle } from './widgets/layout-toggle.vue';

View File

@@ -0,0 +1,245 @@
<script setup lang="ts">
import type { LoginEmits } from './typings';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import {
VbenButton,
VbenCheckbox,
VbenInput,
VbenInputPassword,
} from '@vben-core/shadcn-ui';
import Title from './auth-title.vue';
import ThirdPartyLogin from './third-party-login.vue';
interface Props {
/**
* @zh_CN 验证码登录路径
*/
codeLoginPath?: string;
/**
* @zh_CN 忘记密码路径
*/
forgetPasswordPath?: string;
/**
* @zh_CN 是否处于加载处理状态
*/
loading?: boolean;
/**
* @zh_CN 密码占位符
*/
passwordPlaceholder?: string;
/**
* @zh_CN 二维码登录路径
*/
qrCodeLoginPath?: string;
/**
* @zh_CN 注册路径
*/
registerPath?: string;
/**
* @zh_CN 是否显示验证码登录
*/
showCodeLogin?: boolean;
/**
* @zh_CN 是否显示忘记密码
*/
showForgetPassword?: boolean;
/**
* @zh_CN 是否显示二维码登录
*/
showQrcodeLogin?: boolean;
/**
* @zh_CN 是否显示注册按钮
*/
showRegister?: boolean;
/**
* @zh_CN 是否显示第三方登录
*/
showThirdPartyLogin?: boolean;
/**
* @zh_CN 用户名占位符
*/
usernamePlaceholder?: string;
}
defineOptions({
name: 'AuthenticationLogin',
});
withDefaults(defineProps<Props>(), {
codeLoginPath: '/auth/code-login',
forgetPasswordPath: '/auth/forget-password',
loading: false,
passwordPlaceholder: '',
qrCodeLoginPath: '/auth/qrcode-login',
registerPath: '/auth/register',
showCodeLogin: true,
showForgetPassword: true,
showQrcodeLogin: true,
showRegister: true,
showThirdPartyLogin: true,
usernamePlaceholder: '',
});
const emit = defineEmits<{
submit: LoginEmits['submit'];
}>();
const router = useRouter();
const REMEMBER_ME_KEY = 'REMEMBER_ME_USERNAME';
const localUsername = localStorage.getItem(REMEMBER_ME_KEY) || '';
const formState = reactive({
password: '',
rememberMe: !!localUsername,
submitted: false,
username: localUsername,
});
const usernameStatus = computed(() => {
return formState.submitted && !formState.username ? 'error' : 'default';
});
const passwordStatus = computed(() => {
return formState.submitted && !formState.password ? 'error' : 'default';
});
function handleSubmit() {
formState.submitted = true;
if (
usernameStatus.value !== 'default' ||
passwordStatus.value !== 'default'
) {
return;
}
localStorage.setItem(
REMEMBER_ME_KEY,
formState.rememberMe ? formState.username : '',
);
emit('submit', {
password: formState.password,
username: formState.username,
});
}
function handleGo(path: string) {
router.push(path);
}
</script>
<template>
<div @keypress.enter.prevent="handleSubmit">
<Title>
{{ $t('authentication.welcome-back') }} 👋🏻
<template #desc>
<span class="text-muted-foreground">
{{ $t('authentication.login-subtitle') }}
</span>
</template>
</Title>
<VbenInput
v-model="formState.username"
:autofocus="false"
:error-tip="$t('authentication.username-tip')"
:label="$t('authentication.username')"
:placeholder="usernamePlaceholder || $t('authentication.username')"
:status="usernameStatus"
name="username"
required
type="text"
/>
<VbenInputPassword
v-model="formState.password"
:error-tip="$t('authentication.password-tip')"
:label="$t('authentication.password')"
:placeholder="passwordPlaceholder || $t('authentication.password')"
:status="passwordStatus"
name="password"
required
type="password"
/>
<div class="mb-6 mt-4 flex justify-between">
<div class="flex-center flex">
<VbenCheckbox v-model:checked="formState.rememberMe" name="rememberMe">
{{ $t('authentication.remember-me') }}
</VbenCheckbox>
</div>
<span
v-if="showForgetPassword"
class="text-primary hover:text-primary/80 cursor-pointer text-sm font-normal"
@click="handleGo(forgetPasswordPath)"
>
{{ $t('authentication.forget-password') }}
</span>
<!-- <VbenButton variant="ghost" @click="handleGo('/auth/forget-password')">
忘记密码?
</VbenButton> -->
</div>
<VbenButton :loading="loading" class="w-full" @click="handleSubmit">
{{ $t('common.login') }}
</VbenButton>
<div class="mb-2 mt-4 flex items-center justify-between">
<VbenButton
v-if="showCodeLogin"
class="w-1/2"
variant="outline"
@click="handleGo(codeLoginPath)"
>
{{ $t('authentication.mobile-login') }}
</VbenButton>
<VbenButton
v-if="showQrcodeLogin"
class="ml-4 w-1/2"
variant="outline"
@click="handleGo(qrCodeLoginPath)"
>
{{ $t('authentication.qrcode-login') }}
</VbenButton>
<!-- <VbenButton
:loading="loading"
variant="outline"
class="w-1/3"
@click="handleGo('/auth/register')"
>
创建账号
</VbenButton> -->
</div>
<!-- 第三方登录 -->
<ThirdPartyLogin v-if="showThirdPartyLogin" />
<div v-if="showRegister" class="text-center text-sm">
{{ $t('authentication.account-tip') }}
<span
class="text-primary hover:text-primary/80 cursor-pointer text-sm font-normal"
@click="handleGo(registerPath)"
>
{{ $t('authentication.create-account') }}
</span>
</div>
</div>
</template>

View File

@@ -0,0 +1,68 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { VbenButton } from '@vben-core/shadcn-ui';
import { useQRCode } from '@vueuse/integrations/useQRCode';
import Title from './auth-title.vue';
interface Props {
/**
* @zh_CN 是否处于加载处理状态
*/
loading?: boolean;
/**
* @zh_CN 登陆路径
*/
loginPath?: string;
}
defineOptions({
name: 'AuthenticationQrCodeLogin',
});
const props = withDefaults(defineProps<Props>(), {
loading: false,
loginPath: '/auth/login',
});
const router = useRouter();
const text = ref('https://vben.vvbin.cn');
const qrcode = useQRCode(text, {
errorCorrectionLevel: 'H',
margin: 4,
});
function goLogin() {
router.push(props.loginPath);
}
</script>
<template>
<div>
<Title>
{{ $t('authentication.welcome-back') }} 📱
<template #desc>
<span class="text-muted-foreground">
{{ $t('authentication.qrcode-subtitle') }}
</span>
</template>
</Title>
<div class="flex-col-center mt-6">
<img :src="qrcode" alt="qrcode" class="w-1/2" />
<p class="text-muted-foreground mt-4 text-sm">
{{ $t('authentication.qrcode-prompt') }}
</p>
</div>
<VbenButton class="mt-4 w-full" variant="outline" @click="goLogin()">
{{ $t('common.back') }}
</VbenButton>
</div>
</template>

View File

@@ -0,0 +1,168 @@
<script setup lang="ts">
import type { RegisterEmits } from './typings';
import { computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import {
VbenButton,
VbenCheckbox,
VbenInput,
VbenInputPassword,
} from '@vben-core/shadcn-ui';
import Title from './auth-title.vue';
interface Props {
/**
* @zh_CN 是否处于加载处理状态
*/
loading?: boolean;
/**
* @zh_CN 登陆路径
*/
loginPath?: string;
}
defineOptions({
name: 'RegisterForm',
});
const props = withDefaults(defineProps<Props>(), {
loading: false,
loginPath: '/auth/login',
});
const emit = defineEmits<{
submit: RegisterEmits['submit'];
}>();
const router = useRouter();
const formState = reactive({
agreePolicy: false,
comfirmPassword: '',
password: '',
submitted: false,
username: '',
});
const usernameStatus = computed(() => {
return formState.submitted && !formState.username ? 'error' : 'default';
});
const passwordStatus = computed(() => {
return formState.submitted && !formState.password ? 'error' : 'default';
});
const comfirmPasswordStatus = computed(() => {
return formState.submitted && formState.password !== formState.comfirmPassword
? 'error'
: 'default';
});
function handleSubmit() {
formState.submitted = true;
if (
usernameStatus.value !== 'default' ||
passwordStatus.value !== 'default'
) {
return;
}
emit('submit', {
password: formState.password,
username: formState.username,
});
}
function goLogin() {
router.push(props.loginPath);
}
</script>
<template>
<div>
<Title>
{{ $t('authentication.create-an-account') }} 🚀
<template #desc> {{ $t('authentication.sign-up-subtitle') }} </template>
</Title>
<VbenInput
v-model="formState.username"
:error-tip="$t('authentication.username-tip')"
:label="$t('authentication.username')"
:placeholder="$t('authentication.username')"
:status="usernameStatus"
name="username"
type="text"
/>
<!-- Use 8 or more characters with a mix of letters, numbers & symbols. -->
<VbenInputPassword
v-model="formState.password"
:error-tip="$t('authentication.password-tip')"
:label="$t('authentication.password')"
:password-strength="true"
:placeholder="$t('authentication.password')"
:status="passwordStatus"
name="password"
required
type="password"
>
<template #strengthText>
{{ $t('authentication.password-strength') }}
</template>
</VbenInputPassword>
<VbenInputPassword
v-model="formState.comfirmPassword"
:error-tip="$t('authentication.comfirm-password-tip')"
:label="$t('authentication.comfirm-password')"
:placeholder="$t('authentication.comfirm-password')"
:status="comfirmPasswordStatus"
name="comfirmPassword"
required
type="password"
/>
<div class="relative mt-4 flex pb-6">
<div class="flex-center">
<VbenCheckbox
v-model:checked="formState.agreePolicy"
name="agreePolicy"
>
{{ $t('authentication.sign-up-agree') }}
<span class="text-primary hover:text-primary/80">{{
$t('authentication.sign-up-privacy-policy')
}}</span>
&
<span class="text-primary hover:text-primary/80">
{{ $t('authentication.sign-up-terms') }}
</span>
</VbenCheckbox>
</div>
<Transition name="slide-up">
<p
v-show="formState.submitted && !formState.agreePolicy"
class="text-destructive absolute bottom-1 left-0 text-xs"
>
{{ $t('authentication.sign-up-agree-tip') }}
</p>
</Transition>
</div>
<div>
<VbenButton :loading="loading" class="w-full" @click="handleSubmit">
{{ $t('authentication.sign-up') }}
</VbenButton>
</div>
<div class="mt-4 text-center text-sm">
{{ $t('authentication.already-account') }}
<span
class="text-primary hover:text-primary/80 cursor-pointer text-sm font-normal"
@click="goLogin()"
>
{{ $t('authentication.go-login') }}
</span>
</div>
</div>
</template>

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben-core/iconify';
import { VbenIconButton } from '@vben-core/shadcn-ui';
defineOptions({
name: 'ThirdPartyLogin',
});
</script>
<template>
<div class="w-full sm:mx-auto md:max-w-md">
<div class="mt-4 flex items-center justify-between">
<span class="border-input w-[35%] border-b dark:border-gray-600"></span>
<span class="text-muted-foreground text-center text-xs uppercase">
{{ $t('authentication.third-party-login') }}
</span>
<span class="border-input w-[35%] border-b dark:border-gray-600"></span>
</div>
<div class="mt-4 flex flex-wrap justify-center">
<VbenIconButton class="mb-3">
<MdiWechat />
</VbenIconButton>
<VbenIconButton class="mb-3">
<MdiQqchat />
</VbenIconButton>
<VbenIconButton class="mb-3">
<MdiGithub />
</VbenIconButton>
<VbenIconButton class="mb-3">
<MdiGoogle />
</VbenIconButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,29 @@
interface LoginAndRegisterParams {
password: string;
username: string;
}
interface LoginCodeParams {
code: string;
phoneNumber: string;
}
interface LoginEmits {
submit: [LoginAndRegisterParams];
}
interface LoginCodeEmits {
submit: [LoginCodeParams];
}
interface RegisterEmits {
submit: [LoginAndRegisterParams];
}
export type {
LoginAndRegisterParams,
LoginCodeEmits,
LoginCodeParams,
LoginEmits,
RegisterEmits,
};

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { IcRoundColorLens } from '@vben-core/iconify';
import {
COLOR_PRIMARY_RESETS,
preferences,
updatePreferences,
} from '@vben-core/preferences';
import { VbenIconButton } from '@vben-core/shadcn-ui';
defineOptions({
name: 'AuthenticationColorToggle',
});
function handleUpdate(value: string) {
updatePreferences({
theme: {
colorPrimary: value,
},
});
}
</script>
<template>
<div class="group relative flex items-center overflow-hidden">
<div
class="ease-ou flex w-0 overflow-hidden transition-all duration-500 group-hover:w-48"
>
<template v-for="color in COLOR_PRIMARY_RESETS" :key="color">
<VbenIconButton
class="flex-center flex-shrink-0"
@click="handleUpdate(color)"
>
<div
:class="[
preferences.theme.colorPrimary === color
? `before:opacity-100`
: '',
]"
:style="{ backgroundColor: color }"
class="relative h-3.5 w-3.5 rounded-[2px] before:absolute before:left-0.5 before:top-0.5 before:h-2.5 before:w-2.5 before:rounded-[2px] before:border before:border-gray-900 before:opacity-0 before:transition-all before:duration-150 before:content-[''] hover:scale-110"
></div>
</VbenIconButton>
</template>
</div>
<VbenIconButton>
<IcRoundColorLens class="text-primary size-5" />
</VbenIconButton>
</div>
</template>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import type { AuthPageLayoutType } from '@vben-core/preferences';
import type { VbenDropdownMenuItem } from '@vben-core/shadcn-ui';
import { computed } from 'vue';
import { $t } from '@vben/locales';
import { MdiDockBottom, MdiDockLeft, MdiDockRight } from '@vben-core/iconify';
import {
preferences,
updatePreferences,
usePreferences,
} from '@vben-core/preferences';
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
defineOptions({
name: 'AuthenticationLayoutToggle',
});
const menus = computed((): VbenDropdownMenuItem[] => [
{
icon: MdiDockLeft,
key: 'panel-left',
text: $t('layout.align-left'),
},
{
icon: MdiDockBottom,
key: 'panel-center',
text: $t('layout.center'),
},
{
icon: MdiDockRight,
key: 'panel-right',
text: $t('layout.align-right'),
},
]);
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreferences();
function handleUpdate(value: string) {
updatePreferences({
app: {
authPageLayout: value as AuthPageLayoutType,
},
});
}
</script>
<template>
<VbenDropdownRadioMenu
:menus="menus"
:model-value="preferences.app.authPageLayout"
@update:model-value="handleUpdate"
>
<VbenIconButton>
<MdiDockRight v-if="authPanelRight" class="size-5" />
<MdiDockLeft v-if="authPanelLeft" class="size-5" />
<MdiDockBottom v-if="authPanelCenter" class="size-5" />
</VbenIconButton>
</VbenDropdownRadioMenu>
</template>

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
interface AssistantProps {
botIcon?: string;
botId?: string;
botTitle?: string;
isMobile?: boolean;
}
const props = withDefaults(defineProps<AssistantProps>(), {
botIcon:
'https://cdn.jsdelivr.net/npm/@vbenjs/static-source@0.1.3/source/avatar-v1-transparent-bg.webp',
botId: '7374674983739621392',
botTitle: 'Vben Admin Assistant',
isMobile: false,
});
let client: any;
const wrapperEl = ref();
const { load, unload } = useScriptTag(
'https://sf-cdn.coze.com/obj/unpkg-va/flow-platform/chat-app-sdk/0.1.0-beta.4/libs/oversea/index.js',
() => {
client = new (window as any).CozeWebSDK.WebChatClient({
componentProps: {
icon: props.botIcon,
layout: props.isMobile ? 'mobile' : 'pc',
// lang: 'zh-CN',
title: props.botTitle,
},
config: {
bot_id: props.botId,
},
el: wrapperEl.value,
});
},
{
manual: true,
},
);
onMounted(() => {
load();
});
onUnmounted(() => {
unload();
client?.destroy();
});
</script>
<template>
<div ref="wrapperEl" class="coze-assistant"></div>
</template>
<style>
.coze-assistant {
position: absolute;
right: 30px;
bottom: 30px;
z-index: 1000;
img {
width: 42px !important;
height: 42px !important;
border-radius: 50%;
}
}
</style>

View File

@@ -0,0 +1 @@
export { default as CozeAssistant } from './assistant.vue';

View File

@@ -0,0 +1,25 @@
interface FallbackProps {
/**
* 描述
*/
description?: string;
/**
* @zh_CN 首页路由地址
* @default /
*/
homePath?: string;
/**
* @zh_CN 默认显示的图片
* @default pageNotFoundSvg
*/
image?: string;
/**
* @zh_CN 内置类型
*/
status?: '403' | '404' | '500' | 'hello' | 'offline';
/**
* @zh_CN 页面提示语
*/
title?: string;
}
export type { FallbackProps };

View File

@@ -0,0 +1,161 @@
<script setup lang="ts">
import type { FallbackProps } from './fallback';
import { computed, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { IcRoundArrowBackIosNew, IcRoundRefresh } from '@vben-core/iconify';
import { VbenButton } from '@vben-core/shadcn-ui';
interface Props extends FallbackProps {}
defineOptions({
name: 'Fallback',
});
const props = withDefaults(defineProps<Props>(), {
description: '',
homePath: '/',
image: '',
showBack: true,
status: 'hello',
title: '',
});
const Icon403 = defineAsyncComponent(() => import('./icons/icon-403.vue'));
const Icon404 = defineAsyncComponent(() => import('./icons/icon-404.vue'));
const Icon500 = defineAsyncComponent(() => import('./icons/icon-500.vue'));
const IconHello = defineAsyncComponent(() => import('./icons/icon-hello.vue'));
const IconOffline = defineAsyncComponent(
() => import('./icons/icon-offline.vue'),
);
const titleText = computed(() => {
if (props.title) {
return props.title;
}
switch (props.status) {
case '403': {
return $t('fallback.forbidden');
}
case '404': {
return $t('fallback.page-not-found');
}
case '500': {
return $t('fallback.internal-error');
}
case 'offline': {
return $t('fallback.offline-error');
}
case 'hello': {
return $t('fallback.coming-soon');
}
default: {
return '';
}
}
});
const descText = computed(() => {
if (props.description) {
return props.description;
}
switch (props.status) {
case '403': {
return $t('fallback.forbidden-desc');
}
case '404': {
return $t('fallback.page-not-found-desc');
}
case '500': {
return $t('fallback.internal-error-desc');
}
case 'offline': {
return $t('fallback.offline-error-desc');
}
default: {
return '';
}
}
});
const fallbackIcon = computed(() => {
switch (props.status) {
case '403': {
return Icon403;
}
case '404': {
return Icon404;
}
case '500': {
return Icon500;
}
case 'offline': {
return IconOffline;
}
case 'hello': {
return IconHello;
}
default: {
return null;
}
}
});
const showBack = computed(() => {
return ['403', '404'].includes(props.status);
});
const showRefresh = computed(() => {
return ['500', 'offline'].includes(props.status);
});
const { push } = useRouter();
// 返回首页
function back() {
push(props.homePath);
}
function refresh() {
location.reload();
}
</script>
<template>
<div class="flex size-full flex-col items-center justify-center duration-300">
<img v-if="image" :src="image" class="md:1/3 w-1/2 lg:w-1/4" />
<component
:is="fallbackIcon"
v-else-if="fallbackIcon"
class="md:1/3 h-1/3 w-1/2 lg:w-1/4"
/>
<div class="flex-col-center">
<slot v-if="$slots.title" name="title"></slot>
<p
v-else-if="titleText"
class="text-foreground mt-12 text-3xl md:text-4xl lg:text-5xl"
>
{{ titleText }}
</p>
<slot v-if="$slots.describe" name="describe"></slot>
<p
v-else-if="descText"
class="text-muted-foreground md:text-md my-6 lg:text-lg"
>
{{ descText }}
</p>
<slot v-if="$slots.action" name="action"></slot>
<VbenButton v-else-if="showBack" size="lg" @click="back">
<IcRoundArrowBackIosNew class="mr-2" />
{{ $t('common.back-to-home') }}
</VbenButton>
<VbenButton v-else-if="showRefresh" size="lg" @click="refresh">
<IcRoundRefresh class="mr-2" />
{{ $t('common.refresh') }}
</VbenButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,151 @@
<template>
<svg
height="659.29778"
viewBox="0 0 586 659.29778"
width="586"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<circle cx="332.47856" cy="254" fill="#f2f2f2" r="254.00001" />
<path
d="M498.46363,113.58835H33.17063c-.99774-.02133-1.78931-.84746-1.76797-1.84521,.02069-.96771,.80026-1.74727,1.76797-1.76796H498.46363c.99774,.02133,1.78931,.84746,1.76794,1.84521-.02069,.96771-.80023,1.74727-1.76794,1.76796Z"
fill="#cacaca"
/>
<rect
fill="#fff"
height="34.98639"
rx="17.49318"
ry="17.49318"
width="163.61147"
x="193.77441"
y="174.47256"
/>
<path
d="M128.17493,244.44534H422.98542c9.66122,0,17.49316,7.83197,17.49316,17.49319h0c0,9.66122-7.83194,17.49319-17.49316,17.49319H128.17493c-9.66122,0-17.49318-7.83197-17.49318-17.49319h0c0-9.66122,7.83196-17.49319,17.49318-17.49319Z"
fill="#fff"
/>
<path
d="M128.17493,314.41812H422.98542c9.66122,0,17.49316,7.83197,17.49316,17.49319h0c0,9.66122-7.83194,17.49319-17.49316,17.49319H128.17493c-9.66122,0-17.49318-7.83197-17.49318-17.49319h0c0-9.66122,7.83196-17.49319,17.49318-17.49319Z"
fill="#fff"
/>
<path
d="M91.64085,657.75932l-.69385-.06793c-23.54068-2.42871-44.82135-15.08929-58.18845-34.61835-3.66138-5.44159-6.62299-11.32251-8.815-17.50409l-.21069-.58966,.62375-.05048c7.44699-.59924,15.09732-1.86292,18.49585-2.46417l-21.91473-7.42511-.1355-.65033c-1.29926-6.10406,1.24612-12.38458,6.4285-15.86176,5.19641-3.64447,12.08731-3.76111,17.40405-.29449,2.38599,1.52399,4.88162,3.03339,7.29489,4.49359,8.29321,5.01636,16.8688,10.20337,23.29828,17.30121,9.74951,10.97778,14.02298,25.76984,11.63,40.25562l4.7829,17.47595Z"
fill="#f2f2f2"
/>
<polygon
fill="#a0616a"
points="171.30016 646.86102 182.10017 646.85999 187.23916 605.198 171.29716 605.19897 171.30016 646.86102"
/>
<path
d="M170.9192,658.12816l33.21436-.00122v-.41998c-.00049-7.13965-5.78833-12.92737-12.92798-12.92773h-.00079l-6.06702-4.60278-11.3197,4.60345-2.89941,.00012,.00055,13.34814Z"
fill="#2f2e41"
/>
<polygon
fill="#a0616a"
points="84.74116 616.94501 93.38016 623.42603 122.49316 593.185 109.74116 583.61902 84.74116 616.94501"
/>
<path
d="M77.67448,625.72966l26.569,19.93188,.25208-.336c4.2843-5.71136,3.12799-13.81433-2.58279-18.09937l-.00064-.00049-2.09079-7.32275-11.81735-3.11102-2.31931-1.73993-8.01019,10.67767Z"
fill="#2f2e41"
/>
<path
d="M120.64463,451.35271s.59625,16.26422,1.3483,29.30737c.12335,2.13916-4.88821,4.46301-4.75842,6.7901,.08609,1.54395,1.02808,3.04486,1.1156,4.65472,.09235,1.69897-1.20822,3.20282-1.1156,4.95984,.09052,1.71667,1.57422,3.6853,1.66373,5.44244,.96317,18.9093,4.45459,41.54633,.9584,47.87439-1.72299,3.11871-23.68533,46.32446-23.68533,46.32446,0,0,12.23666,18.35498,15.73285,12.23663,4.61771-8.08099,40.20615-45.88745,40.20615-53.10712,0-7.21088,8.23346-61.25323,8.23346-61.25323l5.74103,31.98169,2.63239,6.33655-.82715,3.71997,1.70117,5.02045,.09192,4.96838,1.65619,9.22614s-4.98199,71.88159-2.17633,73.88312c2.81439,2.01038,16.44086,5.62018,18.04901,2.01038,1.59955-3.6098,12.0108-75.01947,12.0108-75.01947,0,0,1.6781-32.72424,3.49622-63.14111,.1048-1.76556,1.34607-3.89825,1.4422-5.63763,.11365-2.01898-.67297-4.64111-.56818-6.599,.11365-2.24628,1.11005-3.82831,1.20618-5.97852,.74292-16.6156-3.42761-36.84912-4.7561-38.84192-4.01202-6.01343-7.62177-10.82074-7.62177-10.82074,0,0-54.03558-17.75403-68.47485,.28625l-3.30185,25.37585Z"
fill="#2f2e41"
/>
<path
d="M174.53779,284.10378l-21.4209-4.28418-9.9964,13.56656h0c-18.65262,18.34058-18.93359,34.52753-15.60379,60.47382v36.41553l-2.41,24.41187s-8.53156,17.84521,.26788,22.00006,66.59857,3.80066,72.117,2.14209,.73517-3.69482-.71399-11.4245c-2.72211-14.51929-.90131-7.51562-.71399-12.13849,2.68585-66.31363-3.57013-93.5379-4.20544-100.69376l-10.89398-19.75858-6.42639-10.71042Z"
fill="#3f3d56"
/>
<path
d="M287.43909,337.57097c-2.23248,4.23007-7.47144,5.84943-11.70148,3.61694-.45099-.23804-.88013-.51541-1.28229-.82895l-46.26044,29.37308,.13336-15.9924,44.93842-26.07846c3.20093-3.58887,8.70514-3.90332,12.29401-.70239,3.00305,2.67844,3.7796,7.0657,1.87842,10.61218Z"
fill="#a0616a"
/>
<path
d="M157.62488,302.62425l-5.26666-.55807c-4.86633-.50473-9.64093,1.57941-12.57947,5.491-1.12549,1.48346-1.9339,3.18253-2.37491,4.99164l-.00317,.01447c-1.32108,5.44534,.75095,11.15201,5.25803,14.48117l18.19031,13.41101c12.76544,17.24899,36.75653,28.69272,64.89832,37.98978l43.74274-27.16666-15.47186-18.73843-30.00336,16.0798-44.59833-34.52374-.0257-.02075-16.97424-10.936-4.79169-.5152Z"
fill="#3f3d56"
/>
<circle cx="167.29993" cy="248.60526" fill="#a0616a" r="24.9798" />
<path
d="M167.8769,273.59047c-.20135,.00662-.4032,.01108-.6048,.01657-.0863,.22388-.17938,.44583-.2868,.66357l.8916-.68015Z"
fill="#2f2e41"
/>
<path
d="M174.73243,249.29823c.03918,.24612,.09912,.48846,.17914,.72449-.03302-.24731-.09308-.49026-.17914-.72449Z"
fill="#2f2e41"
/>
<path
d="M192.59852,224.6942c-1.0282,3.19272-1.94586-.85715-5.32825-.12869-4.06885,.87625-8.80377,.57532-12.13586-1.91879-4.96478-3.64273-11.39874-4.62335-17.22333-2.62509-5.70154,2.01706-15.25348,3.43933-16.73907,9.30179-.51642,2.03781-.7215,4.24933-1.97321,5.9382-1.09436,1.47662-2.82166,2.31854-4.26608,3.45499-4.87726,3.83743-1.14954,14.73981,1.15881,20.50046,2.30838,5.76065,7.60355,9.95721,13.42526,12.10678,5.63281,2.07977,11.7464,2.44662,17.75531,2.28317,1.04517-2.7106,.59363-5.84137-.26874-8.65134-.93359-3.04199-2.31592-5.97791-2.70593-9.13599s.46643-6.74527,3.11444-8.50986c2.4339-1.62192,6.39465-.63388,7.32062,1.98843-.54028-3.27841,2.7807-6.4509,6.20508-7.00882,3.67651-.599,7.35291,.72833,11.01886,1.38901s2.36475-14.77301,.64209-18.98425Z"
fill="#2f2e41"
/>
<circle
cx="281.3585"
cy="285.71051"
fill="hsl(var(--color-primary))"
r="51.12006"
transform="translate(-26.58509 542.54478) rotate(-85.26884)"
/>
<path
d="M294.78675,264.41051l-13.42828,13.42828-13.42828-13.42828c-2.17371-2.17374-5.69806-2.17374-7.87177,0s-2.17371,5.69803,0,7.87177l13.42828,13.42828-13.42828,13.42828c-2.17169,2.17575-2.1684,5.70007,.00739,7.87177,2.17285,2.16879,5.69153,2.16879,7.86438-.00003l13.42828-13.42828,13.42828,13.42828c2.17578,2.17169,5.70007,2.1684,7.87177-.00735,2.16882-2.17288,2.16882-5.6915,0-7.86438l-13.42828-13.42828,13.42828-13.42828c2.17371-2.17374,2.17371-5.69803,0-7.87177s-5.69806-2.17374-7.87177,0h0Z"
fill="#fff"
/>
<path
d="M261.21387,242.74385c1.5069,4.53946-.95154,9.44101-5.49097,10.94791-.48401,.16064-.9812,.27823-1.4859,.35141l-10.83051,53.71692-11.44788-11.16785,12.29266-50.48209c-.37366-4.7944,3.21008-8.98395,8.00452-9.3576,4.01166-.31265,7.71509,2.16425,8.95807,5.9913Z"
fill="#a0616a"
/>
<path
d="M146.12519,312.22478l-4.04883,3.41412c-3.73322,3.16214-5.53476,8.05035-4.74649,12.87888,.29129,1.83917,.95773,3.59879,1.95786,5.16949l.00824,.0123c3.01477,4.72311,8.5672,7.17865,14.08978,6.23117l22.27075-3.84171c21.28461,2.72995,46.15155-6.65967,72.34302-20.53055l10.67969-50.37274-24.23297-1.80811-9.16821,32.78271-55.78815,8.28149-.03278,.00415-19.64294,4.67767-3.68896,3.1011Z"
fill="#3f3d56"
/>
<path
d="M272.93684,658.99046l-271.75,.30731c-.65759-.00214-1.18896-.53693-1.18683-1.19452,.00211-.6546,.53223-1.18469,1.18683-1.18683l271.75-.30731c.65759,.00214,1.18896,.53693,1.18683,1.19452-.00208,.6546-.53223,1.18469-1.18683,1.18683Z"
fill="#cacaca"
/>
<g>
<ellipse
cx="56.77685"
cy="82.05834"
fill="#3f3d56"
rx="8.45661"
ry="8.64507"
/>
<ellipse
cx="85.9906"
cy="82.05834"
fill="#3f3d56"
rx="8.45661"
ry="8.64507"
/>
<ellipse
cx="115.20435"
cy="82.05834"
fill="#3f3d56"
rx="8.45661"
ry="8.64507"
/>
<path
d="M148.51577,88.89113c-.25977,0-.51904-.10059-.71484-.30078l-5.70605-5.83301c-.38037-.38867-.38037-1.00977,0-1.39844l5.70605-5.83252c.38721-.39453,1.021-.40088,1.41406-.01562,.39502,.38623,.40186,1.01953,.01562,1.41406l-5.02197,5.1333,5.02197,5.13379c.38623,.39453,.37939,1.02783-.01562,1.41406-.19434,.19043-.44678,.28516-.69922,.28516Z"
fill="#3f3d56"
/>
<path
d="M158.10415,88.89113c-.25244,0-.50488-.09473-.69922-.28516-.39502-.38623-.40186-1.01904-.01562-1.41406l5.02148-5.13379-5.02148-5.1333c-.38623-.39453-.37939-1.02783,.01562-1.41406,.39404-.38672,1.02783-.37939,1.41406,.01562l5.70557,5.83252c.38037,.38867,.38037,1.00977,0,1.39844l-5.70557,5.83301c-.1958,.2002-.45508,.30078-.71484,.30078Z"
fill="#3f3d56"
/>
<path
d="M456.61398,74.41416h-10.60999c-1.21002,0-2.19,.97998-2.19,2.19v10.62c0,1.21002,.97998,2.19,2.19,2.19h10.60999c1.21002,0,2.20001-.97998,2.20001-2.19v-10.62c0-1.21002-.98999-2.19-2.20001-2.19Z"
fill="#3f3d56"
/>
<path
d="M430.61398,74.41416h-10.60999c-1.21002,0-2.19,.97998-2.19,2.19v10.62c0,1.21002,.97998,2.19,2.19,2.19h10.60999c1.21002,0,2.20001-.97998,2.20001-2.19v-10.62c0-1.21002-.98999-2.19-2.20001-2.19Z"
fill="#3f3d56"
/>
<path
d="M481.11398,74.91416h-10.60999c-1.21002,0-2.19,.97998-2.19,2.19v10.62c0,1.21002,.97998,2.19,2.19,2.19h10.60999c1.21002,0,2.20001-.97998,2.20001-2.19v-10.62c0-1.21002-.98999-2.19-2.20001-2.19Z"
fill="#3f3d56"
/>
<path
d="M321.19229,78.95414h-84.81c-1.48004,0-2.67004,1.20001-2.67004,2.67004s1.19,2.66998,2.67004,2.66998h84.81c1.46997,0,2.66998-1.20001,2.66998-2.66998s-1.20001-2.67004-2.66998-2.67004Z"
fill="#3f3d56"
/>
</g>
</svg>
</template>

View File

@@ -0,0 +1,189 @@
<template>
<svg
height="571"
viewBox="0 0 860 571"
width="860"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<path
d="M605.66974,324.95306c-7.66934-12.68446-16.7572-26.22768-30.98954-30.36953-16.482-4.7965-33.4132,4.73193-47.77473,14.13453a1392.15692,1392.15692,0,0,0-123.89338,91.28311l.04331.49238q46.22556-3.1878,92.451-6.37554c22.26532-1.53546,45.29557-3.2827,64.97195-13.8156,7.46652-3.99683,14.74475-9.33579,23.20555-9.70782,10.51175-.46217,19.67733,6.87923,26.8802,14.54931,42.60731,45.371,54.937,114.75409,102.73817,154.61591A1516.99453,1516.99453,0,0,0,605.66974,324.95306Z"
fill="#f2f2f2"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M867.57068,709.78146c-4.71167-5.94958-6.6369-7.343-11.28457-13.34761q-56.7644-73.41638-106.70791-151.79237-33.92354-53.23-64.48275-108.50439-14.54864-26.2781-28.29961-52.96872-10.67044-20.6952-20.8646-41.63793c-1.94358-3.98782-3.8321-7.99393-5.71122-12.00922-4.42788-9.44232-8.77341-18.93047-13.43943-28.24449-5.31686-10.61572-11.789-21.74485-21.55259-28.877a29.40493,29.40493,0,0,0-15.31855-5.89458c-7.948-.51336-15.28184,2.76855-22.17568,6.35295-50.43859,26.301-97.65922,59.27589-140.3696,96.79771A730.77816,730.77816,0,0,0,303.32241,496.24719c-1.008,1.43927-3.39164.06417-2.37419-1.38422q6.00933-8.49818,12.25681-16.81288A734.817,734.817,0,0,1,500.80465,303.06436q18.24824-11.82581,37.18269-22.54245c6.36206-3.60275,12.75188-7.15967,19.25136-10.49653,6.37146-3.27274,13.13683-6.21547,20.41563-6.32547,24.7701-.385,37.59539,27.66695,46.40506,46.54248q4.15283,8.9106,8.40636,17.76626,16.0748,33.62106,33.38729,66.628,10.68453,20.379,21.83683,40.51955,34.7071,62.71816,73.77854,122.897c34.5059,53.1429,68.73651,100.08874,108.04585,149.78472C870.59617,709.21309,868.662,711.17491,867.57068,709.78146Z"
fill="#e4e4e4"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M414.91613,355.804c-1.43911-1.60428-2.86927-3.20856-4.31777-4.81284-11.42244-12.63259-23.6788-25.11847-39.3644-32.36067a57.11025,57.11025,0,0,0-23.92679-5.54622c-8.56213.02753-16.93178,2.27348-24.84306,5.41792-3.74034,1.49427-7.39831,3.1902-11.00078,4.99614-4.11634,2.07182-8.15927,4.28118-12.1834,6.50883q-11.33112,6.27044-22.36816,13.09089-21.9606,13.57221-42.54566,29.21623-10.67111,8.11311-20.90174,16.75788-9.51557,8.03054-18.64618,16.492c-1.30169,1.20091-3.24527-.74255-1.94358-1.94347,1.60428-1.49428,3.22691-2.97938,4.84955-4.44613q6.87547-6.21546,13.9712-12.19257,12.93921-10.91827,26.54851-20.99312,21.16293-15.67614,43.78288-29.22541,11.30361-6.76545,22.91829-12.96259c2.33794-1.24675,4.70318-2.466,7.09572-3.6211a113.11578,113.11578,0,0,1,16.86777-6.86632,60.0063,60.0063,0,0,1,25.476-2.50265,66.32706,66.32706,0,0,1,23.50512,8.1314c15.40091,8.60812,27.34573,21.919,38.97,34.90915C418.03337,355.17141,416.09875,357.12405,414.91613,355.804Z"
fill="#e4e4e4"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M730.47659,486.71092l36.90462-13.498,18.32327-6.70183c5.96758-2.18267,11.92082-4.66747,18.08988-6.23036a28.53871,28.53871,0,0,1,16.37356.20862,37.73753,37.73753,0,0,1,12.771,7.91666,103.63965,103.63965,0,0,1,10.47487,11.18643c3.98932,4.79426,7.91971,9.63877,11.86772,14.46706q24.44136,29.89094,48.56307,60.04134,24.12117,30.14991,47.91981,60.556,23.85681,30.48041,47.38548,61.21573,2.88229,3.76518,5.75966,7.53415c1.0598,1.38809,3.44949.01962,2.37472-1.38808Q983.582,650.9742,959.54931,620.184q-24.09177-30.86383-48.51647-61.46586-24.42421-30.60141-49.17853-60.93743-6.16706-7.55761-12.35445-15.09858c-3.47953-4.24073-6.91983-8.52718-10.73628-12.47427-7.00539-7.24516-15.75772-13.64794-26.23437-13.82166-6.15972-.10214-12.121,1.85248-17.844,3.92287-6.16968,2.232-12.32455,4.50571-18.48633,6.75941l-37.16269,13.59243-9.29067,3.3981c-1.64875.603-.93651,3.2619.73111,2.652Z"
fill="#e4e4e4"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M366.37741,334.52609c-18.75411-9.63866-42.77137-7.75087-60.00508,4.29119a855.84708,855.84708,0,0,1,97.37056,22.72581C390.4603,353.75916,380.07013,341.5635,366.37741,334.52609Z"
fill="#f2f2f2"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M306.18775,338.7841l-3.61042,2.93462c1.22123-1.02713,2.4908-1.99013,3.795-2.90144C306.31073,338.80665,306.24935,338.79473,306.18775,338.7841Z"
fill="#f2f2f2"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M831.54929,486.84576c-3.6328-4.42207-7.56046-9.05222-12.99421-10.84836l-5.07308.20008A575.436,575.436,0,0,0,966.74929,651.418Q899.14929,569.13192,831.54929,486.84576Z"
fill="#f2f2f2"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M516.08388,450.36652A37.4811,37.4811,0,0,0,531.015,471.32518c2.82017,1.92011,6.15681,3.76209,7.12158,7.03463a8.37858,8.37858,0,0,1-.87362,6.1499,24.88351,24.88351,0,0,1-3.86126,5.04137l-.13667.512c-6.99843-4.14731-13.65641-9.3934-17.52227-16.55115s-4.40553-16.53895.34116-23.14544"
fill="#f2f2f2"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M749.08388,653.36652A37.4811,37.4811,0,0,0,764.015,674.32518c2.82017,1.92011,6.15681,3.76209,7.12158,7.03463a8.37858,8.37858,0,0,1-.87362,6.1499,24.88351,24.88351,0,0,1-3.86126,5.04137l-.13667.512c-6.99843-4.14731-13.65641-9.3934-17.52227-16.55115s-4.40553-16.53895.34116-23.14544"
fill="#f2f2f2"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M284.08388,639.36652A37.4811,37.4811,0,0,0,299.015,660.32518c2.82017,1.92011,6.15681,3.76209,7.12158,7.03463a8.37858,8.37858,0,0,1-.87362,6.1499,24.88351,24.88351,0,0,1-3.86126,5.04137l-.13667.512c-6.99843-4.14731-13.65641-9.3934-17.52227-16.55115s-4.40553-16.53895.34116-23.14544"
fill="#f2f2f2"
transform="translate(-169.93432 -164.42601)"
/>
<circle cx="649.24878" cy="51" fill="hsl(var(--color-primary))" r="51" />
<path
d="M911.21851,176.29639c-24.7168-3.34094-52.93512,10.01868-59.34131,34.12353a21.59653,21.59653,0,0,0-41.09351,2.10871l2.82972,2.02667a372.27461,372.27461,0,0,0,160.65881-.72638C957.07935,195.76,935.93537,179.63727,911.21851,176.29639Z"
fill="#f0f0f0"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M805.21851,244.29639c-24.7168-3.34094-52.93512,10.01868-59.34131,34.12353a21.59653,21.59653,0,0,0-41.09351,2.10871l2.82972,2.02667a372.27461,372.27461,0,0,0,160.65881-.72638C851.07935,263.76,829.93537,247.63727,805.21851,244.29639Z"
fill="#f0f0f0"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M1020.94552,257.15423a.98189.98189,0,0,1-.30176-.04688C756.237,173.48919,523.19942,184.42376,374.26388,208.32122c-20.26856,3.251-40.59131,7.00586-60.40381,11.16113-5.05811,1.05957-10.30567,2.19532-15.59668,3.37793-6.31885,1.40723-12.55371,2.85645-18.53223,4.30567q-3.873.917-7.59472,1.84863c-3.75831.92773-7.57178,1.89453-11.65967,2.957-4.56787,1.17774-9.209,2.41309-13.79737,3.67188a.44239.44239,0,0,1-.05127.01465l.00049.001c-5.18261,1.415-10.33789,2.8711-15.32324,4.3252-2.69824.77929-5.30371,1.54785-7.79932,2.30664-.2788.07715-.52587.15136-.77636.22754l-.53614.16308c-.31054.09473-.61718.1875-.92382.27539l-.01953.00586.00048.001-.81152.252c-.96777.293-1.91211.5791-2.84082.86426-24.54492,7.56641-38.03809,12.94922-38.17139,13.00195a1,1,0,1,1-.74414-1.85644c.13428-.05274,13.69336-5.46289,38.32764-13.05762.93213-.28613,1.87891-.57226,2.84961-.86621l.7539-.23438c.02588-.00976.05176-.01757.07813-.02539.30518-.08691.60986-.17968.91943-.27343l.53711-.16309c.26758-.08105.53125-.16113.80127-.23535,2.47852-.75391,5.09278-1.52441,7.79785-2.30664,4.98731-1.45508,10.14746-2.91113,15.334-4.32813.01611-.00586.03271-.00976.04883-.01464v-.001c4.60449-1.2627,9.26269-2.50293,13.84521-3.68457,4.09424-1.06348,7.915-2.03223,11.67969-2.96192q3.73755-.93017,7.60937-1.85253c5.98536-1.45118,12.23291-2.90235,18.563-4.3125,5.29932-1.1836,10.55567-2.32227,15.62207-3.38282,19.84326-4.16211,40.19776-7.92285,60.49707-11.17871C523.09591,182.415,756.46749,171.46282,1021.2463,255.2011a.99974.99974,0,0,1-.30078,1.95313Z"
fill="#ccc"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M432.92309,584.266a6.72948,6.72948,0,0,0-1.7-2.67,6.42983,6.42983,0,0,0-.92-.71c-2.61-1.74-6.51-2.13-8.99,0a5.81012,5.81012,0,0,0-.69.71q-1.11,1.365-2.28,2.67c-1.28,1.46-2.59,2.87-3.96,4.24-.39.38-.78.77-1.18,1.15-.23.23-.46.45-.69.67-.88.84-1.78,1.65-2.69,2.45-.48.43-.96.85-1.45,1.26-.73.61-1.46,1.22-2.2,1.81-.07.05-.14.1-.21.16-.02.01-.03.03-.05.04-.01,0-.02,0-.03.02a.17861.17861,0,0,0-.07.05c-.22.15-.37.25-.48.34.04-.01995.08-.05.12-.07-.18.14-.37.28-.55.42-1.75,1.29-3.54,2.53-5.37,3.69a99.21022,99.21022,0,0,1-14.22,7.55c-.33.13-.67.27-1.01.4a85.96993,85.96993,0,0,1-40.85,6.02q-2.13008-.165-4.26-.45c-1.64-.24-3.27-.53-4.89-.86a97.93186,97.93186,0,0,1-18.02-5.44,118.65185,118.65185,0,0,1-20.66-12.12c-1-.71-2.01-1.42-3.02-2.11,1.15-2.82,2.28-5.64,3.38-8.48.55-1.37,1.08-2.74,1.6-4.12,4.09-10.63,7.93-21.36,11.61-32.13q5.58-16.365,10.53-32.92.51-1.68.99-3.36,2.595-8.745,4.98-17.53c.15-.56994.31-1.12994.45-1.7q.68994-2.52,1.35-5.04c1-3.79-1.26-8.32-5.24-9.23a7.63441,7.63441,0,0,0-9.22,5.24c-.43,1.62-.86,3.23-1.3,4.85q-3.165,11.74494-6.66,23.41-.51,1.68-1.02,3.36-7.71,25.41-16.93,50.31-1.11,3.015-2.25,6.01c-.37.98-.74,1.96-1.12,2.94-.73,1.93-1.48,3.86-2.23,5.79-.43006,1.13-.87006,2.26-1.31,3.38-.29.71-.57,1.42-.85,2.12a41.80941,41.80941,0,0,0-8.81-2.12l-.48-.06a27.397,27.397,0,0,0-7.01.06,23.91419,23.91419,0,0,0-17.24,10.66c-4.77,7.51-4.71,18.25,1.98,24.63,6.89,6.57,17.32,6.52,25.43,2.41a28.35124,28.35124,0,0,0,10.52-9.86,50.56939,50.56939,0,0,0,2.74-4.65c.21.14.42.28.63.43.8.56,1.6,1.13,2.39,1.69a111.73777,111.73777,0,0,0,14.51,8.91,108.35887,108.35887,0,0,0,34.62,10.47c.27.03.53.07.8.1,1.33.17,2.67.3,4.01.41a103.78229,103.78229,0,0,0,55.58-11.36q2.175-1.125,4.31-2.36,3.315-1.92,6.48-4.08c1.15-.78,2.27-1.57,3.38-2.4a101.04244,101.04244,0,0,0,13.51-11.95q2.35491-2.475,4.51-5.11005a8.0612,8.0612,0,0,0,2.2-5.3A7.5644,7.5644,0,0,0,432.92309,584.266Zm-165.59,23.82c.21-.15.42-.31.62-.47C267.89312,607.766,267.60308,607.936,267.33312,608.086Zm3.21-3.23c-.23.26-.44.52-.67.78a23.36609,23.36609,0,0,1-2.25,2.2c-.11.1-.23.2-.35.29a.00976.00976,0,0,0-.01.01,3.80417,3.80417,0,0,0-.42005.22q-.645.39-1.31994.72a17.00459,17.00459,0,0,1-2.71.75,16.79925,16.79925,0,0,1-2.13.02h-.02a14.82252,14.82252,0,0,1-1.45-.4c-.24-.12-.47-.25994-.7-.4-.09-.08-.17005-.16-.22-.21a2.44015,2.44015,0,0,1-.26995-.29.0098.0098,0,0,0-.01-.01c-.11005-.2-.23005-.4-.34-.6a.031.031,0,0,1-.01-.02c-.08-.25-.15-.51-.21-.77a12.51066,12.51066,0,0,1,.01-1.37,13.4675,13.4675,0,0,1,.54-1.88,11.06776,11.06776,0,0,1,.69-1.26c.02-.04.12-.2.23-.38.01-.01.01-.01.01-.02.15-.17.3-.35.46-.51.27-.3.56-.56.85-.83a18.02212,18.02212,0,0,1,1.75-1.01,19.48061,19.48061,0,0,1,2.93-.79,24.98945,24.98945,0,0,1,4.41.04,30.30134,30.30134,0,0,1,4.1,1.01,36.94452,36.94452,0,0,1-2.77,4.54C270.6231,604.746,270.58312,604.806,270.54308,604.856Zm-11.12-3.29a2.18029,2.18029,0,0,1-.31.38995A1.40868,1.40868,0,0,1,259.42309,601.566Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M402.86309,482.136q-.13494,4.71-.27,9.42-.285,10.455-.59,20.92-.315,11.775-.66,23.54-.165,6.07507-.34,12.15-.465,16.365-.92,32.72c-.03,1.13-.07,2.25-.1,3.38q-.225,8.11506-.45,16.23-.255,8.805-.5,17.61-.18,6.59994-.37,13.21-1.34994,47.895-2.7,95.79a7.64844,7.64844,0,0,1-7.5,7.5,7.56114,7.56114,0,0,1-7.5-7.5q.75-26.94,1.52-53.88.675-24.36,1.37-48.72.225-8.025.45-16.06.345-12.09.68-24.18c.03-1.13.07-2.25.1-3.38.02-.99.05-1.97.08-2.96q.66-23.475,1.32-46.96.27-9.24.52-18.49.3-10.545.6-21.08c.09-3.09.17005-6.17.26-9.26a7.64844,7.64844,0,0,1,7.5-7.5A7.56116,7.56116,0,0,1,402.86309,482.136Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M814.29118,484.2172a893.23753,893.23753,0,0,1-28.16112,87.94127c-3.007,7.94641-6.08319,15.877-9.3715,23.71185l.75606-1.7916a54.58274,54.58274,0,0,1-5.58953,10.61184q-.22935.32119-.46685.63642,1.16559-1.49043.4428-.589c-.25405.30065-.5049.60219-.7676.89546a23.66436,23.66436,0,0,1-2.2489,2.20318q-.30139.25767-.61188.5043l.93783-.729c-.10884.25668-.87275.59747-1.11067.74287a18.25362,18.25362,0,0,1-2.40479,1.21853l1.7916-.75606a19.0859,19.0859,0,0,1-4.23122,1.16069l1.9938-.26791a17.02055,17.02055,0,0,1-4.29785.046l1.99379.2679a14.0022,14.0022,0,0,1-3.40493-.917l1.79159.75606a12.01175,12.01175,0,0,1-1.67882-.89614c-.27135-.17688-1.10526-.80852-.01487.02461,1.13336.86595.14562.07434-.08763-.15584-.19427-.19171-.36962-.4-.55974-.595-.88208-.90454.99637,1.55662.39689.49858a18.18179,18.18179,0,0,1-.87827-1.63672l.75606,1.7916a11.92493,11.92493,0,0,1-.728-2.65143l.26791,1.9938a13.65147,13.65147,0,0,1-.00316-3.40491l-.2679,1.9938a15.96371,15.96371,0,0,1,.99486-3.68011l-.75606,1.7916a16.72914,16.72914,0,0,1,1.17794-2.29848,6.72934,6.72934,0,0,1,.72851-1.0714c.04915.01594-1.26865,1.51278-.56937.757.1829-.19767.354-.40592.539-.602.29617-.31382.61354-.60082.92561-.89791,1.04458-.99442-1.46188.966-.25652.17907a19.0489,19.0489,0,0,1,2.74925-1.49923l-1.79159.75606a20.31136,20.31136,0,0,1,4.99523-1.33984l-1.9938.2679a25.62828,25.62828,0,0,1,6.46062.07647l-1.9938-.2679a33.21056,33.21056,0,0,1,7.89178,2.2199l-1.7916-.75606c5.38965,2.31383,10.16308,5.74926,14.928,9.118a111.94962,111.94962,0,0,0,14.50615,8.9065,108.38849,108.38849,0,0,0,34.62226,10.47371,103.93268,103.93268,0,0,0,92.58557-36.75192,8.07773,8.07773,0,0,0,2.1967-5.3033,7.63232,7.63232,0,0,0-2.1967-5.3033c-2.75154-2.52586-7.94926-3.239-10.6066,0a95.63575,95.63575,0,0,1-8.10664,8.72692q-2.01736,1.914-4.14232,3.70983-1.21364,1.02588-2.46086,2.01121c-.3934.31081-1.61863,1.13807.26309-.19744-.43135.30614-.845.64036-1.27058.95478a99.26881,99.26881,0,0,1-20.33215,11.56478l1.79159-.75606a96.8364,96.8364,0,0,1-24.17119,6.62249l1.99379-.2679a97.64308,97.64308,0,0,1-25.75362-.03807l1.99379.2679a99.79982,99.79982,0,0,1-24.857-6.77027l1.7916.75607a116.02515,116.02515,0,0,1-21.7364-12.59112,86.87725,86.87725,0,0,0-11.113-6.99417,42.8238,42.8238,0,0,0-14.43784-4.38851c-9.43884-1.11076-19.0571,2.56562-24.24624,10.72035-4.77557,7.50482-4.71394,18.24362,1.97369,24.62519,6.8877,6.5725,17.31846,6.51693,25.43556,2.40567,7.81741-3.95946,12.51288-12.18539,15.815-19.94186,7.43109-17.45514,14.01023-35.31364,20.1399-53.263q9.09651-26.63712,16.49855-53.81332.91661-3.36581,1.80683-6.73869c1.001-3.78869-1.26094-8.32-5.23829-9.22589a7.63317,7.63317,0,0,0-9.22589,5.23829Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M889.12382,482.13557l-2.69954,95.79311-2.68548,95.29418-1.5185,53.88362a7.56465,7.56465,0,0,0,7.5,7.5,7.64923,7.64923,0,0,0,7.5-7.5l2.69955-95.79311,2.68548-95.29418,1.51849-53.88362a7.56465,7.56465,0,0,0-7.5-7.5,7.64923,7.64923,0,0,0-7.5,7.5Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M629.52566,700.36106h2.32885V594.31942h54.32863v-2.32291H631.85451V547.25214H673.8102q-.92256-1.17339-1.89893-2.31694H631.85451V515.38231c-.7703-.32846-1.54659-.64493-2.32885-.9435V544.9352h-45.652V507.07c-.78227.03583-1.55258.08959-2.3289.15527v37.71h-36.4201V516.68409c-.78227.34636-1.55258.71061-2.31694,1.0928V544.9352h-30.6158v2.31694h30.6158v44.74437h-30.6158v2.32291h30.6158V700.36106h2.31694V594.31942a36.41283,36.41283,0,0,1,36.4201,36.42007v69.62157h2.3289V594.31942h45.652Zm-84.401-108.36455V547.25214h36.4201v44.74437Zm38.749,0V547.25214h.91362a44.74135,44.74135,0,0,1,44.73842,44.74437Z"
opacity="0.2"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M615.30309,668.566a63.05854,63.05854,0,0,1-20.05,33.7c-.74.64-1.48,1.26-2.25,1.87q-2.805.25506-5.57.52c-1.53.14-3.04.29-4.54.43l-.27.03-.19-1.64-.76-6.64a37.623,37.623,0,0,1-3.3-32.44c2.64-7.12,7.42-13.41,12.12-19.65,6.49-8.62,12.8-17.14,13.03-27.65a60.54415,60.54415,0,0,1,7.9,13.33,16.432,16.432,0,0,0-5.12,3.76995c-.41.45-.82,1.08-.54,1.62006.24.46.84.57,1.36.62994,1.25.13,2.51.26,3.76.39,1,.11,2,.21,3,.32a63.99025,63.99025,0,0,1,2.45,12.18A61.18851,61.18851,0,0,1,615.30309,668.566Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M648.50311,642.356c-5.9,4.29-9.35,10.46-12.03,17.26a16.62776,16.62776,0,0,0-7.17,4.58c-.41.45-.82,1.08-.54,1.62006.24.46.84.57,1.36.62994,1.25.13,2.51.26,3.76.39-2.68,8.04-5.14,16.36-9.88,23.15a36.98942,36.98942,0,0,1-12.03,10.91,38.49166,38.49166,0,0,1-4.02,1.99q-7.62.585-14.95,1.25-2.805.25506-5.57.52c-1.53.14-3.04.29-4.54.43q-.015-.825,0-1.65a63.30382,63.30382,0,0,1,15.25-39.86c.45-.52.91-1.03,1.38-1.54a61.7925,61.7925,0,0,1,16.81-12.7A62.65425,62.65425,0,0,1,648.50311,642.356Z"
fill="hsl(var(--color-primary))"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M589.16308,699.526l-1.15,3.4-.58,1.73c-1.53.14-3.04.29-4.54.43l-.27.03c-1.66.17-3.31.34-4.96.51-.43-.5-.86-1.01-1.28-1.53a62.03045,62.03045,0,0,1,8.07-87.11c-1.32,6.91.22,13.53,2.75,20.1-.27.11-.53.22-.78.34a16.432,16.432,0,0,0-5.12,3.76995c-.41.45-.82,1.08-.54,1.62006.24.46.84.57,1.36.62994,1.25.13,2.51.26,3.76.39,1,.11,2,.21,3,.32q.705.075,1.41.15c.07.15.13.29.2.44,2.85,6.18,5.92,12.39,7.65,18.83a43.66591,43.66591,0,0,1,1.02,4.91A37.604,37.604,0,0,1,589.16308,699.526Z"
fill="hsl(var(--color-primary))"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M689.82123,554.48655c-8.60876-16.79219-21.94605-30.92088-37.63219-41.30357a114.2374,114.2374,0,0,0-52.5626-18.37992q-3.69043-.33535-7.399-.39281c-2.92141-.04371-46.866,12.63176-61.58712,22.98214a114.29462,114.29462,0,0,0-35.333,39.527,102.49972,102.49972,0,0,0-12.12557,51.6334,113.56387,113.56387,0,0,0,14.70268,51.47577,110.47507,110.47507,0,0,0,36.44425,38.74592C549.66655,708.561,565.07375,734.51,583.1831,735.426c18.24576.923,39.05418-23.55495,55.6951-30.98707a104.42533,104.42533,0,0,0,41.72554-34.005,110.24964,110.24964,0,0,0,19.599-48.94777c2.57368-18.08313,1.37415-36.73271-4.80123-54.01627a111.85969,111.85969,0,0,0-5.58024-12.9833c-1.77961-3.50519-6.996-4.7959-10.26142-2.69063a7.67979,7.67979,0,0,0-2.69064,10.26142q1.56766,3.08773,2.91536,6.27758l-.75606-1.7916a101.15088,101.15088,0,0,1,6.87641,25.53816l-.26791-1.99379a109.2286,109.2286,0,0,1-.06613,28.68252l.26791-1.9938a109.73379,109.73379,0,0,1-7.55462,27.67419l.75606-1.79159a104.212,104.212,0,0,1-6.67151,13.09835q-1.92308,3.18563-4.08062,6.22159c-.63172.8881-1.28287,1.761-1.939,2.63114-.85625,1.13555,1.16691-1.48321.28228-.36941-.15068.18972-.30049.3801-.45182.5693q-.68121.85165-1.3818,1.68765a93.61337,93.61337,0,0,1-10.17647,10.38359q-1.36615,1.19232-2.77786,2.33115c-.46871.37832-.932.77269-1.42079,1.12472.01861-.0134,1.57956-1.19945.65556-.511-.2905.21644-.57851.43619-.86961.65184q-2.90994,2.1558-5.97433,4.092a103.48509,103.48509,0,0,1-14.75565,7.7131l1.7916-.75606a109.21493,109.21493,0,0,1-27.59663,7.55154l1.9938-.26791a108.15361,108.15361,0,0,1-28.58907.0506l1.99379.2679a99.835,99.835,0,0,1-25.09531-6.78448l1.79159.75607a93.64314,93.64314,0,0,1-13.41605-6.99094q-3.17437-2-6.18358-4.24743c-.2862-.21359-.56992-.43038-.855-.64549-.9155-.69088.65765.50965.67021.51787a19.16864,19.16864,0,0,1-1.535-1.22469q-1.45353-1.18358-2.86136-2.4218a101.98931,101.98931,0,0,1-10.49319-10.70945q-1.21308-1.43379-2.37407-2.91054c-.33524-.4263-.9465-1.29026.40424.5289-.17775-.23939-.36206-.47414-.54159-.71223q-.64657-.85751-1.27568-1.72793-2.203-3.048-4.18787-6.24586a109.29037,109.29037,0,0,1-7.8054-15.10831l.75606,1.7916a106.58753,106.58753,0,0,1-7.34039-26.837l.26791,1.9938a97.86589,97.86589,0,0,1-.04843-25.63587l-.2679,1.9938A94.673,94.673,0,0,1,505.27587,570.55l-.75606,1.7916a101.55725,101.55725,0,0,1,7.19519-13.85624q2.0655-3.32328,4.37767-6.4847.52528-.71832,1.06244-1.42786c.324-.4279,1.215-1.49333-.30537.38842.14906-.18449.29252-.37428.43942-.56041q1.26882-1.60756,2.59959-3.1649A107.40164,107.40164,0,0,1,530.772,536.21508q1.47408-1.29171,2.99464-2.52906.6909-.56218,1.39108-1.11284c.18664-.14673.37574-.29073.56152-.43858-1.99743,1.58953-.555.43261-.10157.09288q3.13393-2.34833,6.43534-4.46134a103.64393,103.64393,0,0,1,15.38655-8.10791l-1.7916.75606c7.76008-3.25839,42.14086-10.9492,48.394-10.10973l-1.99379-.26791A106.22471,106.22471,0,0,1,628.768,517.419l-1.7916-.75606a110.31334,110.31334,0,0,1,12.6002,6.32922q3.04344,1.78405,5.96742,3.76252,1.38351.93658,2.73809,1.915.677.48917,1.34626.98885c.24789.185.49386.37253.74135.558,1.03924.779-1.43148-1.1281-.34209-.26655a110.84261,110.84261,0,0,1,10.36783,9.2532q2.401,2.445,4.63686,5.04515,1.14659,1.33419,2.24643,2.70757c.36436.45495,1.60506,2.101.08448.08457.37165.49285.74744.98239,1.11436,1.47884a97.97718,97.97718,0,0,1,8.39161,13.53807c1.79317,3.49775,6.98675,4.80186,10.26142,2.69064A7.67666,7.67666,0,0,0,689.82123,554.48655Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M602.43116,676.88167a3.77983,3.77983,0,0,1-2.73939-6.55137c.09531-.37882.16368-.65085.259-1.02968q-.05115-.12366-.1029-.24717c-3.47987-8.29769-25.685,14.83336-26.645,22.63179a30.029,30.029,0,0,0,.52714,10.32752A120.39223,120.39223,0,0,1,562.77838,652.01a116.20247,116.20247,0,0,1,.72078-12.96332q.59712-5.293,1.65679-10.51055a121.78667,121.78667,0,0,1,24.1515-51.61646c6.87378.38364,12.898-.66348,13.47967-13.98532.10346-2.36972,1.86113-4.42156,2.24841-6.756-.65621.08607-1.32321.13985-1.97941.18285-.20444.0107-.41958.02149-.624.03228l-.07709.00346a3.745,3.745,0,0,1-3.07566-6.10115q.425-.52305.85054-1.04557c.43036-.53793.87143-1.06507,1.30171-1.60292a1.865,1.865,0,0,0,.13986-.16144c.49494-.61322.98971-1.21564,1.48465-1.82885a10.82911,10.82911,0,0,0-3.55014-3.43169c-4.95941-2.90463-11.80146-.89293-15.38389,3.59313-3.59313,4.486-4.27083,10.77947-3.023,16.3843a43.39764,43.39764,0,0,0,6.003,13.3828c-.269.34429-.54872.67779-.81765,1.02209a122.57366,122.57366,0,0,0-12.79359,20.2681c1.0163-7.93863-11.41159-36.60795-16.21776-42.68052-5.773-7.29409-17.61108-4.11077-18.62815,5.13562q-.01476.13428-.02884.26849,1.07082.60411,2.0964,1.28237a5.12707,5.12707,0,0,1-2.06713,9.33031l-.10452.01613c-9.55573,13.64367,21.07745,49.1547,28.74518,41.18139a125.11045,125.11045,0,0,0-6.73449,31.69282,118.66429,118.66429,0,0,0,.08607,19.15986l-.03231-.22593C558.90163,648.154,529.674,627.51374,521.139,629.233c-4.91675.99041-9.75952.76525-9.01293,5.72484q.01788.11874.03635.2375a34.4418,34.4418,0,0,1,3.862,1.86105q1.07082.60423,2.09639,1.28237a5.12712,5.12712,0,0,1-2.06712,9.33039l-.10464.01606c-.07528.01079-.13987.02157-.21507.03237-4.34967,14.96631,27.90735,39.12,47.5177,31.43461h.01081a125.07484,125.07484,0,0,0,8.402,24.52806H601.679c.10765-.3335.20443-.67779.3013-1.01129a34.102,34.102,0,0,1-8.30521-.49477c2.22693-2.73257,4.45377-5.48664,6.6807-8.21913a1.86122,1.86122,0,0,0,.13986-.16135c1.12956-1.39849,2.26992-2.78627,3.39948-4.18476l.00061-.00173a49.95232,49.95232,0,0,0-1.46367-12.72495Zm-34.37066-67.613.0158-.02133-.0158.04282Zm-6.64832,59.93237-.25822-.58084c.01079-.41957.01079-.83914,0-1.26942,0-.11845-.0215-.23672-.0215-.35508.09678.74228.18285,1.48464.29042,2.22692Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<circle cx="95.24878" cy="439" fill="hsl(var(--color-foreground))" r="11" />
<circle
cx="227.24878"
cy="559"
fill="hsl(var(--color-foreground))"
r="11"
/>
<circle
cx="728.24878"
cy="559"
fill="hsl(var(--color-foreground))"
r="11"
/>
<circle
cx="755.24878"
cy="419"
fill="hsl(var(--color-foreground))"
r="11"
/>
<circle
cx="723.24878"
cy="317"
fill="hsl(var(--color-foreground))"
r="11"
/>
<path
d="M434.1831,583.426a10.949,10.949,0,1,1-.21-2.16A10.9921,10.9921,0,0,1,434.1831,583.426Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<circle
cx="484.24878"
cy="349"
fill="hsl(var(--color-foreground))"
r="11"
/>
<path
d="M545.1831,513.426a10.949,10.949,0,1,1-.21-2.16A10.9921,10.9921,0,0,1,545.1831,513.426Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<path
d="M403.1831,481.426a10.949,10.949,0,1,1-.21-2.16A10.9921,10.9921,0,0,1,403.1831,481.426Z"
fill="hsl(var(--color-foreground))"
transform="translate(-169.93432 -164.42601)"
/>
<circle
cx="599.24878"
cy="443"
fill="hsl(var(--color-foreground))"
r="11"
/>
<circle
cx="426.24878"
cy="338"
fill="hsl(var(--color-foreground))"
r="16"
/>
<path
d="M1028.875,735.26666l-857.75.30733a1.19068,1.19068,0,1,1,0-2.38136l857.75-.30734a1.19069,1.19069,0,0,1,0,2.38137Z"
fill="#cacaca"
transform="translate(-169.93432 -164.42601)"
/>
</svg>
</template>

View File

@@ -0,0 +1,215 @@
<template>
<svg
height="699"
viewBox="0 0 1119 699"
width="1119"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<title>server down</title>
<circle cx="292.60911" cy="213" fill="#f2f2f2" r="213" />
<path
d="M31.39089,151.64237c0,77.49789,48.6181,140.20819,108.70073,140.20819"
fill="#2f2e41"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M140.09162,291.85056c0-78.36865,54.255-141.78356,121.30372-141.78356"
fill="hsl(var(--color-primary))"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M70.77521,158.66768c0,73.61476,31.00285,133.18288,69.31641,133.18288"
fill="hsl(var(--color-primary))"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M140.09162,291.85056c0-100.13772,62.7103-181.16788,140.20819-181.16788"
fill="#2f2e41"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M117.22379,292.83905s15.41555-.47479,20.06141-3.783,23.713-7.2585,24.86553-1.95278,23.16671,26.38821,5.76263,26.5286-40.43935-2.711-45.07627-5.53549S117.22379,292.83905,117.22379,292.83905Z"
fill="#a8a8a8"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M168.224,311.78489c-17.40408.14042-40.43933-2.71094-45.07626-5.53548-3.53126-2.151-4.93843-9.86945-5.40926-13.43043-.32607.014-.51463.02-.51463.02s.97638,12.43276,5.61331,15.2573,27.67217,5.67589,45.07626,5.53547c5.02386-.04052,6.7592-1.82793,6.66391-4.47526C173.87935,310.756,171.96329,311.75474,168.224,311.78489Z"
opacity="0.2"
transform="translate(-31.39089 -100.5)"
/>
<ellipse cx="198.60911" cy="424.5" fill="#3f3d56" rx="187" ry="25.43993" />
<ellipse cx="198.60911" cy="424.5" opacity="0.1" rx="157" ry="21.35866" />
<ellipse cx="836.60911" cy="660.5" fill="#3f3d56" rx="283" ry="38.5" />
<ellipse cx="310.60911" cy="645.5" fill="#3f3d56" rx="170" ry="23.12721" />
<path
d="M494,726.5c90,23,263-30,282-90"
fill="none"
stroke="#2f2e41"
stroke-miterlimit="10"
stroke-width="2"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M341,359.5s130-36,138,80-107,149-17,172"
fill="none"
stroke="#2f2e41"
stroke-miterlimit="10"
stroke-width="2"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M215.40233,637.78332s39.0723-10.82,41.47675,24.04449-32.15951,44.78287-5.10946,51.69566"
fill="none"
stroke="#2f2e41"
stroke-miterlimit="10"
stroke-width="2"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M810.09554,663.73988,802.218,714.03505s-38.78182,20.60284-11.51335,21.20881,155.73324,0,155.73324,0,24.84461,0-14.54318-21.81478l-7.87756-52.719Z"
fill="#2f2e41"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M785.21906,734.69812c6.193-5.51039,16.9989-11.252,16.9989-11.252l7.87756-50.2952,113.9216.10717,7.87756,49.582c9.185,5.08711,14.8749,8.987,18.20362,11.97818,5.05882-1.15422,10.58716-5.44353-18.20362-21.38921l-7.87756-52.719-113.9216,3.02983L802.218,714.03506S769.62985,731.34968,785.21906,734.69812Z"
opacity="0.1"
transform="translate(-31.39089 -100.5)"
/>
<rect
fill="#2f2e41"
height="357.51989"
rx="18.04568"
width="513.25314"
x="578.43291"
y="212.68859"
/>
<rect
fill="#3f3d56"
height="267.83694"
width="478.71308"
x="595.70294"
y="231.77652"
/>
<circle cx="835.05948" cy="223.29299" fill="#f2f2f2" r="3.02983" />
<path
d="M1123.07694,621.32226V652.6628a18.04341,18.04341,0,0,1-18.04568,18.04568H627.86949A18.04341,18.04341,0,0,1,609.8238,652.6628V621.32226Z"
fill="#2f2e41"
transform="translate(-31.39089 -100.5)"
/>
<polygon
fill="#2f2e41"
points="968.978 667.466 968.978 673.526 642.968 673.526 642.968 668.678 643.417 667.466 651.452 645.651 962.312 645.651 968.978 667.466"
/>
<path
d="M1125.828,762.03359c-.59383,2.539-2.83591,5.21743-7.90178,7.75032-18.179,9.08949-55.1429-2.42386-55.1429-2.42386s-28.4804-4.84773-28.4804-17.573a22.72457,22.72457,0,0,1,2.49658-1.48459c7.64294-4.04351,32.98449-14.02122,77.9177.42248a18.73921,18.73921,0,0,1,8.54106,5.59715C1125.07908,756.45353,1126.50669,759.15715,1125.828,762.03359Z"
fill="#2f2e41"
transform="translate(-31.39089 -100.5)"
/>
<path
d="M1125.828,762.03359c-22.251,8.526-42.0843,9.1622-62.43871-4.975-10.26507-7.12617-19.59089-8.88955-26.58979-8.75618,7.64294-4.04351,32.98449-14.02122,77.9177.42248a18.73921,18.73921,0,0,1,8.54106,5.59715C1125.07908,756.45353,1126.50669,759.15715,1125.828,762.03359Z"
opacity="0.1"
transform="translate(-31.39089 -100.5)"
/>
<ellipse
cx="1066.53846"
cy="654.13477"
fill="#f2f2f2"
rx="7.87756"
ry="2.42386"
/>
<circle cx="835.05948" cy="545.66686" fill="#f2f2f2" r="11.51335" />
<polygon
opacity="0.1"
points="968.978 667.466 968.978 673.526 642.968 673.526 642.968 668.678 643.417 667.466 968.978 667.466"
/>
<rect fill="#2f2e41" height="242" width="208" x="108.60911" y="159" />
<rect fill="#3f3d56" height="86" width="250" x="87.60911" y="135" />
<rect fill="#3f3d56" height="86" width="250" x="87.60911" y="237" />
<rect fill="#3f3d56" height="86" width="250" x="87.60911" y="339" />
<rect
fill="#6c63ff"
height="16"
opacity="0.4"
width="16"
x="271.60911"
y="150"
/>
<rect
fill="#6c63ff"
height="16"
opacity="0.8"
width="16"
x="294.60911"
y="150"
/>
<rect fill="#6c63ff" height="16" width="16" x="317.60911" y="150" />
<rect
fill="#6c63ff"
height="16"
opacity="0.4"
width="16"
x="271.60911"
y="251"
/>
<rect
fill="#6c63ff"
height="16"
opacity="0.8"
width="16"
x="294.60911"
y="251"
/>
<rect fill="#6c63ff" height="16" width="16" x="317.60911" y="251" />
<rect
fill="#6c63ff"
height="16"
opacity="0.4"
width="16"
x="271.60911"
y="352"
/>
<rect
fill="#6c63ff"
height="16"
opacity="0.8"
width="16"
x="294.60911"
y="352"
/>
<rect fill="#6c63ff" height="16" width="16" x="317.60911" y="352" />
<circle cx="316.60911" cy="538" fill="#2f2e41" r="79" />
<rect fill="#2f2e41" height="43" width="24" x="280.60911" y="600" />
<rect fill="#2f2e41" height="43" width="24" x="328.60911" y="600" />
<ellipse cx="300.60911" cy="643.5" fill="#2f2e41" rx="20" ry="7.5" />
<ellipse cx="348.60911" cy="642.5" fill="#2f2e41" rx="20" ry="7.5" />
<circle cx="318.60911" cy="518" fill="#fff" r="27" />
<circle cx="318.60911" cy="518" fill="#3f3d56" r="9" />
<path
d="M271.36733,565.03228c-6.37889-28.56758,14.01185-57.43392,45.544-64.47477s62.2651,10.41,68.644,38.9776-14.51861,39.10379-46.05075,46.14464S277.74622,593.59986,271.36733,565.03228Z"
fill="#6c63ff"
transform="translate(-31.39089 -100.5)"
/>
<ellipse
cx="417.21511"
cy="611.34365"
fill="#2f2e41"
rx="39.5"
ry="12.40027"
transform="translate(-238.28665 112.98044) rotate(-23.17116)"
/>
<ellipse
cx="269.21511"
cy="664.34365"
fill="#2f2e41"
rx="39.5"
ry="12.40027"
transform="translate(-271.07969 59.02084) rotate(-23.17116)"
/>
<path
d="M394,661.5c0,7.732-19.90861,23-42,23s-43-14.268-43-22,20.90861-6,43-6S394,653.768,394,661.5Z"
fill="#fff"
transform="translate(-31.39089 -100.5)"
/>
</svg>
</template>

View File

@@ -0,0 +1,262 @@
<template>
<svg
data-name="Layer 1"
height="424.8366"
viewBox="0 0 979.32677 424.8366"
width="979.32677"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<path
d="M993.71816,412.83936H419.142a9.19888,9.19888,0,0,0,0,18.39776H435.417V651.3026a9.19888,9.19888,0,0,0,18.39776,0l.1398-220.06548h461.1557l42.52,220.06548a9.19887,9.19887,0,1,0,18.39775,0l2.67633-220.06548h15.01383a9.19888,9.19888,0,0,0,0-18.39776Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M518.73716,371.85047v38.9547H421.141a19.48915,19.48915,0,1,1-1.35523-38.95474q.67739-.02358,1.35523,0Z"
fill="#f2f2f2"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M521.13449,410.50552a1.49881,1.49881,0,0,1-1.49822,1.49822H419.40273a20.52615,20.52615,0,0,1,0-41.05229H519.63627a1.49827,1.49827,0,1,1,0,2.99653H419.40273a17.52964,17.52964,0,0,0,0,35.05924H519.63627A1.49883,1.49883,0,0,1,521.13449,410.50552Z"
fill="hsl(var(--color-primary))"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M518.73716,380.84H413.85905a.29966.29966,0,0,1-.00552-.59929H518.73716a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M518.73716,388.03169H413.85905a.29966.29966,0,0,1-.00552-.59929H518.73716a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M518.73716,395.22332H413.85905a.29966.29966,0,0,1-.00552-.59929H518.73716a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M518.73716,402.41487H413.85905a.29966.29966,0,0,1-.00552-.59929H518.73716a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M500.33941,330.80932v38.95474H402.74324a19.48915,19.48915,0,0,1-1.35522-38.95474q.67737-.02358,1.35522,0Z"
fill="#f2f2f2"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M502.73673,369.46442a1.49885,1.49885,0,0,1-1.49822,1.49826H401.005a20.52614,20.52614,0,0,1,0-41.05229H501.23851a1.49826,1.49826,0,1,1,0,2.99652H401.005a17.52964,17.52964,0,0,0,0,35.05928H501.23851A1.49884,1.49884,0,0,1,502.73673,369.46442Z"
fill="#3f3d56"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M500.33941,339.79886H395.4613a.29966.29966,0,0,1-.00553-.59929H500.33941a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M500.33941,346.99054H395.4613a.29966.29966,0,0,1-.00553-.59929H500.33941a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M500.33941,354.18217H395.4613a.29966.29966,0,0,1-.00553-.59929H500.33941a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M500.33941,361.37376H395.4613a.29966.29966,0,0,1-.00553-.59929H500.33941a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M613.87355,550.68347V516.71838a5.661,5.661,0,0,0-5.66085-5.66085H479.4284a5.661,5.661,0,0,0-5.66084,5.66085v33.96509Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<rect
fill="#ccc"
height="43.87158"
width="140.10602"
x="363.43092"
y="325.83868"
/>
<path
d="M473.76756,620.02887V653.994a5.661,5.661,0,0,0,5.66084,5.66084H608.2127a5.661,5.661,0,0,0,5.66085-5.66084V620.02887Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<circle cx="432.77633" cy="294.70402" fill="#fff" r="4.24564" />
<circle cx="432.77633" cy="351.3125" fill="#fff" r="4.24564" />
<circle cx="433.00385" cy="406.72228" fill="#fff" r="4.24564" />
<path
d="M597.989,472.33053v38.9547H500.39287a19.48916,19.48916,0,0,1-1.35647-38.9547q.678-.02358,1.35647,0Z"
fill="#f2f2f2"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M600.38637,510.98558a1.49881,1.49881,0,0,1-1.49822,1.49822H498.65461a20.52615,20.52615,0,0,1-.0247-41.05229H598.88815a1.49827,1.49827,0,1,1,0,2.99653H498.65461a17.52963,17.52963,0,0,0,0,35.05923H598.88815A1.49885,1.49885,0,0,1,600.38637,510.98558Z"
fill="#3f3d56"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M597.989,481.32H493.111a.29966.29966,0,0,1-.00553-.59929H597.98913a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M597.989,488.51175H493.111a.29966.29966,0,0,1-.00553-.59929H597.98913a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M597.989,495.70338H493.111a.29966.29966,0,0,1-.00553-.59929H597.98913a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M597.989,502.89493H493.111a.29966.29966,0,0,1-.00553-.59929H597.98913a.29966.29966,0,0,1,0,.59929Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M483.36747,317.81415H438.90162a2.74745,2.74745,0,0,0-1.21689.28306l-11.22288,5.61835a2.0452,2.0452,0,0,0,0,3.76443l11.22288,5.61835a2.74718,2.74718,0,0,0,1.21689.28306h44.46585a2.33381,2.33381,0,0,0,2.4628-2.16532v-11.2367A2.3338,2.3338,0,0,0,483.36747,317.81415Z"
fill="#3f3d56"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M485.83027,319.97947v11.2367a2.33383,2.33383,0,0,1-2.4628,2.16532h-8.8589V317.81415h8.8589A2.33383,2.33383,0,0,1,485.83027,319.97947Z"
fill="hsl(var(--color-primary))"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M216.78083,537.99332a35.33951,35.33951,0,0,0,34.12552-6.01134c11.95262-10.03214,15.70013-26.56,18.74934-41.864q4.50949-22.63308,9.019-45.26617l-18.88217,13.00153c-13.57891,9.34993-27.46375,18.99939-36.86572,32.54233S209.42082,522.42587,216.975,537.08"
fill="#e6e6e6"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M218.39489,592.79741c-1.91113-13.92071-3.87625-28.0202-2.53572-42.09016,1.19057-12.4956,5.00277-24.70032,12.764-34.70734a57.73582,57.73582,0,0,1,14.81307-13.42309c1.48131-.935,2.84468,1.41257,1.36983,2.34348a54.88844,54.88844,0,0,0-21.71125,26.19626c-4.72684,12.02273-5.48591,25.12848-4.67135,37.90006.4926,7.72345,1.53656,15.39627,2.58859,23.05926a1.40615,1.40615,0,0,1-.94781,1.66928,1.3653,1.3653,0,0,1-1.6693-.94781Z"
fill="#f2f2f2"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M236.80246,568.16434a26.01425,26.01425,0,0,0,22.6665,11.69871c11.47417-.54466,21.04-8.55293,29.651-16.15584l25.46969-22.48783-16.85671-.80672c-12.12234-.58011-24.55745-1.12124-36.10356,2.617s-22.19457,12.73508-24.30583,24.68624"
fill="#e6e6e6"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M212.99392,600.79976c9.19853-16.27621,19.86805-34.36538,38.93262-40.14695A43.445,43.445,0,0,1,268.3022,558.962c1.73863.14991,1.30448,2.82994-.431,2.6803a40.36111,40.36111,0,0,0-26.133,6.91386c-7.36852,5.01554-13.10573,11.98848-17.96161,19.383-2.97439,4.52936-5.63867,9.25082-8.30346,13.966-.85161,1.50687-3.34078.41915-2.47922-1.10534Z"
fill="#f2f2f2"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M198.25523,617.93168a19.69836,19.69836,0,0,1,12.0709-16.49847v-9.40956h15.782v9.70608a19.68812,19.68812,0,0,1,11.41362,16.202l3.711,43.13835H194.54417Z"
fill="#f2f2f2"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M734.973,411.955l-4.69488-1.97685-3.22067-23.53551h-42.889l-3.491,23.43936-4.20031,2.10013a.99744.99744,0,0,0,.44611,1.88955h57.66283A.99739.99739,0,0,0,734.973,411.955Z"
fill="#e6e6e6"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M811.1898,389.574H600.50692a4.174,4.174,0,0,1-4.16467-4.174V355.69092H815.35446V385.4A4.17408,4.17408,0,0,1,811.1898,389.574Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M815.57469,369.73213H596.15V242.61337a5.0375,5.0375,0,0,1,5.03186-5.03167h209.361a5.03755,5.03755,0,0,1,5.03191,5.03167Z"
fill="#3f3d56"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M802.46932,360.50584h-193.214a3.88344,3.88344,0,0,1-3.87919-3.87908V250.68707a3.88365,3.88365,0,0,1,3.87919-3.87932h193.214a3.88366,3.88366,0,0,1,3.8792,3.87932V356.62676A3.88345,3.88345,0,0,1,802.46932,360.50584Z"
fill="#fff"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M751.57964,397.88662a11.6159,11.6159,0,0,1,17.666,2.27241l26.13446-4.64642,6.69716,15.19317-36.99908,6.04328a11.67883,11.67883,0,0,1-13.49855-18.86244Z"
fill="#ffb6b6"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M775.77611,417.286l27.24571-.33963,3.44882-.04668,55.43253-.69843s15.05312-14.3609,28.16068-29.1465l-1.83719-13.28833A54.29159,54.29159,0,0,0,870.023,340.1519C851.24988,352.696,840.363,377.52559,840.363,377.52559l-34.37018,8.22071-3.43848.82227-21.35608,5.10326Z"
fill="hsl(var(--color-primary))"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M915.25011,498.96167H864.39249c0,2.17915-55.59414,3.94772-55.59414,3.94772a20.30858,20.30858,0,0,0-3.33166,3.15818,19.59694,19.59694,0,0,0-4.58,12.63271v3.15818a19.74588,19.74588,0,0,0,19.73861,19.73861h94.62478a19.75579,19.75579,0,0,0,19.73862-19.73861v-3.15818A19.76607,19.76607,0,0,0,915.25011,498.96167Z"
fill="#e4e4e4"
transform="translate(-110.33661 -237.5817)"
/>
<rect
fill="#e4e4e4"
height="118.48951"
width="20.52816"
x="747.4019"
y="303.23122"
/>
<path
d="M799.31222,658.58132c0,2.218,31.10721.858,69.47992.858s69.47991,1.36012,69.47991-.858-31.1072-19.807-69.47991-19.807S799.31222,656.36323,799.31222,658.58132Z"
fill="#e4e4e4"
transform="translate(-110.33661 -237.5817)"
/>
<polygon
fill="#ffb6b6"
points="675.186 407.461 659.908 407.46 652.64 348.531 675.188 348.532 675.186 407.461"
/>
<path
d="M789.41863,659.852l-49.2623-.00183v-.62309a19.17528,19.17528,0,0,1,19.17426-19.17395h.00122l30.08773.00122Z"
fill="#2f2e41"
transform="translate(-110.33661 -237.5817)"
/>
<polygon
fill="#ffb6b6"
points="630.031 407.461 614.753 407.46 607.485 348.531 630.033 348.532 630.031 407.461"
/>
<path
d="M744.2636,659.852l-49.2623-.00183v-.62309a19.1753,19.1753,0,0,1,19.17426-19.17395h.00122l30.08773.00122Z"
fill="#2f2e41"
transform="translate(-110.33661 -237.5817)"
/>
<circle cx="766.88656" cy="41.63615" fill="#ffb6b6" r="26.56401" />
<path
d="M920.21655,461.22417s8.91308,47.1307-24.99958,53.13247-82.86639,10.21993-82.86639,10.21993L790.36706,627.14324l-29.53443-2.63675s3.928-123.46737,13.5876-133.127,70.71212-38.58282,70.71212-38.58282Z"
fill="#2f2e41"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M853.98286,441.47135,839.151,456.35062s-107.0941,17.25-111.22553,41.9852c-6.23747,37.34427-13.60493,118.552-13.60493,118.552l32.1988-2.41491,12.62647-92.31123,51.5182-11.71874L869.27729,478.5Z"
fill="#2f2e41"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M902.78526,263.36115c-2.6223-4.94751-5.95413-14.80785-11.24679-16.63657a42.07731,42.07731,0,0,0-9.05841-1.92972l-8.99618,3.46009,4.89616-3.808q-1.42988-.08519-2.85817-.13928l-6.0699,2.33453,3.10542-2.41532c-5.65883-.05808-11.5.53031-15.88468,3.9752-3.73817,2.93677-7.44169,14.06185-8.04057,18.77753a35.9171,35.9171,0,0,0,.6603,13.53055l1.53716,1.46166a18.85936,18.85936,0,0,0,1.206-3.83883,18.18056,18.18056,0,0,1,8.70263-11.80641l.08368-.0472c2.5782-1.451,5.7065-1.3841,8.66308-1.27769l14.04158.50527c3.37829.12158,7.01608.33533,9.64978,2.45443a15.888,15.888,0,0,1,3.85826,5.58929c1.30868,2.6414,3.8661,12.60418,3.8661,12.60418s1.44689-1.88062,2.1404-.48092a48.39766,48.39766,0,0,0,2.01437-11.23347A22.00877,22.00877,0,0,0,902.78526,263.36115Z"
fill="#2f2e41"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M995.69426,290.88349A11.61582,11.61582,0,0,0,985.181,305.26136l-21.3614,15.75722,6.40951,15.31674,29.8539-22.67594a11.67883,11.67883,0,0,0-4.38876-22.77589Z"
fill="#ffb6b6"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M992.25627,323.052l-53.551,59.4744s-25.60913-8.19816-45.41466-17.08624l-8.8977-27.32787a54.34329,54.34329,0,0,1-2.60112-19.66442c27.45606-7.306,59.391,19.87863,59.391,19.87863l40.08517-31.39877Z"
fill="hsl(var(--color-primary))"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M867.301,465.6169c-9.554-3.30029-19.43312-6.71277-30.08912-7.99385l-.45773-.05533.12632-.443c11.03073-38.7308,8.27761-63.50657,2.87195-100.72306a37.59072,37.59072,0,0,1,21.5483-39.50121l.06542-.02958,30.43436-1.93391.06935-.00423,22.13437,6.50989a15.18313,15.18313,0,0,1,10.86724,14.83111c-.23987,12.23937.26868,25.9043.80711,40.37114,1.20787,32.45569,2.45686,66.01647-4.63045,87.79166l-.03718.11412-.09462.07416a36.09883,36.09883,0,0,1-23.08086,8.10758C887.90057,472.73235,877.76186,469.23034,867.301,465.6169Z"
fill="hsl(var(--color-primary))"
transform="translate(-110.33661 -237.5817)"
/>
<path
d="M1088.24817,662.4183H111.75183a1.41521,1.41521,0,1,1,0-2.83042h976.49634a1.41521,1.41521,0,1,1,0,2.83042Z"
fill="#ccc"
transform="translate(-110.33661 -237.5817)"
/>
</svg>
</template>

View File

@@ -0,0 +1,112 @@
<template>
<svg
height="458.68642"
viewBox="0 0 656 458.68642"
width="656"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<rect fill="#3f3d56" height="2" width="656" y="434.34322" />
<g>
<path
d="M471.97092,210.81397c-6.0733-36.41747-37.72842-64.16942-75.86423-64.16942H240.14931c-38.12099,0-69.76869,27.72972-75.86421,64.12497-.70358,4.16241-1.06653,8.44331-1.06653,12.80573v135.88599c0,4.36237,.36295,8.63589,1.06653,12.79831,4.85126,28.99625,25.92996,52.49686,53.58563,60.84393,7.05095,2.13306,14.53143,3.28104,22.27859,3.28104h155.9574c7.74716,0,15.22763-1.14798,22.27859-3.28104,27.66309-8.35449,48.74921-31.86993,53.58563-60.88837,.6962-4.14758,1.05911-8.40628,1.05911-12.75388V223.57525c0-4.34758-.36292-8.61369-1.05911-12.76128h-.00003Zm-62.66592,222.28954c-4.2883,.76285-8.69516,1.16281-13.19827,1.16281H240.14931c-4.50313,0-8.90997-.39999-13.19829-1.16281-35.01768-6.22885-61.60677-36.83228-61.60677-73.64224v-45.10526c0-127.45004,103.31242-165.58582,230.76244-165.58582,41.31314,0,74.80505,33.49194,74.80505,74.80505v135.88599c-100.29059,13.42047-26.58911,67.41339-61.60678,73.64224l.00003,.00003Z"
fill="#3f3d56"
/>
<polygon
fill="hsl(var(--color-primary))"
points="349.16196 249.18644 355.16196 288.18642 443.16196 276.18642 434.66196 230.6195 349.16196 249.18644"
/>
<rect
fill="#2f2e41"
height="37.66125"
width="36.38461"
x="381.84177"
y="30.34218"
/>
<polygon
fill="#ffb6b6"
points="385.16196 70.18643 394.16196 43.18643 411.70447 43.18643 412.62653 70.18643 385.16196 70.18643"
/>
<polygon
isolation="isolate"
opacity=".1"
points="385.16196 70.18643 394.16196 43.18643 411.70447 43.18643 412.62653 70.18643 385.16196 70.18643"
/>
<path
d="M394.66196,310.68642l-1,104-1,8v11.48425l15,1.51575,1-23s16-45,12-80-2-25-2-25l-24,3Z"
fill="#ffb6b6"
/>
<path
d="M404.18408,318.85363l-36.90134,97.23831-1.97873,7.81567-4.1777,10.69742-14.52368-4.04477,7.43539-21.78796s1.46619-47.7373,17.92432-78.88422,10.9574-22.5596,10.9574-22.5596l21.26434,11.52512v.00003Z"
fill="#ffb6b6"
/>
<path
d="M385.16196,67.18643l-27,12,17.23959,89.01208-2.72385,127.75565-18,38s-3.01575,21.73227,27.98425,7.73227,66-18,66-18l-8.5-58.5-7.5-153.5,1-34-22-14s-26.5,3.5-26.5,3.50001Z"
fill="#2f2e41"
/>
<path
d="M370.1243,335.34322l-29.96231-50.15677,34.23959-116.98792-16.23959-89.01208,28.49045-12.19685s14.74915,14.36248,14.74915,26.20894-31.27728,242.1447-31.27728,242.1447v-.00003Z"
fill="#e6e6e6"
/>
<path
d="M435.1243,325.34322l-27.19693-233.62811c-.34341-2.94999,.16013-5.93678,1.45178-8.6111l7.78284-16.11441,30.5,8.69685-12.26041,95.51208,32.76041,93.98792-33.03769,60.15677Z"
fill="#e6e6e6"
/>
<path
d="M410.66196,433.68642s-19-11-21-5-3,11-3,11c0,0-5,19,10,19s14-8.64172,14-8.64172v-16.35828Z"
fill="#2f2e41"
/>
<path
d="M344.53574,427.60598s21.69977-3.33459,21.3801,2.9819c-.3197,6.31647-1.20709,11.33768-1.20709,11.33768,0,0-2.25433,19.51712-16.22662,14.06046s-9.89713-13.14252-9.89713-13.14252l5.95078-15.23749-.00003-.00003Z"
fill="#2f2e41"
/>
<circle cx="404.10297" cy="33.02146" fill="#ffb6b6" r="24.85993" />
<path
d="M423.96469,10.86766c-1.15707-6.12936-7.44913-10.27514-13.66504-10.79501s-12.30453,1.82726-17.90228,4.57921c-3.79456,1.86548-7.53061,3.96811-10.60425,6.87182s-5.46063,6.69692-6.01202,10.88913c-.19507,1.48324-.1698,3.03289-.77692,4.40016-.75845,1.708-2.38654,2.86795-3.36917,4.4576-1.76227,2.85096-.95267,6.99858,1.75238,8.97753-3.40024,1.44912-6.89398,2.96069-9.48602,5.59563s-4.08878,6.70308-2.66644,10.11462c.50323,1.20699,1.33481,2.26349,1.76489,3.49843,.81668,2.34499,.03943,5.00909-1.40924,7.02585s-3.49316,3.51228-5.50174,4.97226c5.16196,1.01177,10.43097,1.80015,15.66992,1.32811s10.49707-2.30805,14.29086-5.95176c3.79379-3.64371,5.88083-9.26437,4.51974-14.34539-1.04269-3.89231-3.95898-7.30301-3.95712-11.33256,.00143-3.09747,1.7431-5.89158,3.4249-8.49271,3.67291-5.68066,7.34579-11.36132,11.01868-17.04197,.66068-1.02183,1.35739-2.07924,2.4014-2.70425,1.77606-1.06326,4.0798-.59568,5.95227,.28683,1.87244,.88252,3.58304,2.14867,5.57941,2.69585,4.07452,1.11677,8.80106-1.44789,10.08575-5.47261"
fill="#2f2e41"
/>
<path
d="M409.27951,61.42523c-2.07159,2.0061-5.05701,2.65225-7.82379,3.46516s-5.70978,2.09141-6.95499,4.69243c-1.22101,2.55043-.33459,5.78793,1.68692,7.76505s4.95816,2.80999,7.78555,2.77077c2.82736-.03922,5.58282-.86796,8.24176-1.8301,7.27054-2.63087,14.15665-6.32148,20.37314-10.919-4.02679-1.11411-6.66107-5.81614-5.50836-9.83205,.93768-3.26677,3.80499-5.54528,5.75616-8.32809,3.35959-4.79151,3.91925-11.10753,2.80676-16.85277-1.11246-5.74524-3.73163-11.07097-6.32358-16.3176-.81934-1.65853-1.65805-3.34513-2.93619-4.68245-1.27814-1.33731-3.08783-2.29539-4.92776-2.10379-3.05334,.31795-5.00302,3.66989-5.02377,6.7397s1.32593,5.95491,2.34732,8.84988c1.05231,2.98259,1.78381,6.14409,1.50146,9.29425-.2366,2.63989-1.19669,5.21132-2.74811,7.36029-1.19809,1.65954-2.72479,3.05223-4.0275,4.63097-1.00714,1.22055-1.90009,2.60309-2.16486,4.16321-.48181,2.83914,1.18356,5.71186,.72714,8.55519-.48248,3.0056-3.6452,5.3067-6.65341,4.84085"
fill="#2f2e41"
/>
<g>
<circle
cx="333.2486"
cy="323.64455"
fill="hsl(var(--color-primary))"
r="85"
/>
<g>
<path
d="M384.17838,316.82296h-10.56668c-1.64377-9.68713-6.7168-18.46011-14.2923-24.71729-17.43427-14.39993-43.24109-11.94022-57.64099,5.49411-.04913,.05563-.09644,.11282-.14169,.17151-1.15063,1.49146-.87427,3.63333,.61716,4.784,1.49118,1.1507,3.63306,.87448,4.78394-.61697,6.25537-7.5788,15.72369-12.40167,26.31064-12.40167,16.20853,.00195,30.17899,11.40631,33.42572,27.28629h-9.31805c-.3988,.00012-.78458,.13992-1.09082,.39502-.72375,.60281-.82175,1.6781-.21915,2.40186l13.41125,16.09894c.06577,.07889,.13855,.1517,.21759,.21747,.72324,.60327,1.79871,.50583,2.40186-.21747l13.41125-16.09894c.25504-.30624,.3949-.69223,.39514-1.09082,.00027-.94186-.763-1.70566-1.70486-1.70605v.00003Z"
fill="#fff"
/>
<path
d="M364.34329,344.7337c-1.49146-1.15063-3.63333-.87433-4.78394,.6171-4.96201,6.00781-11.83066,10.13629-19.46436,11.69922-18.46167,3.77988-36.49231-8.12213-40.27225-26.58392h9.3183c.94186-.0004,1.70514-.76419,1.70486-1.70605-.00027-.39853-.14011-.78452-.39514-1.09082l-13.41125-16.09888c-.60312-.72336-1.67862-.8208-2.40186-.21753-.07904,.06577-.15182,.13855-.21759,.21753l-13.41125,16.09888c-.6026,.72375-.50461,1.7991,.21915,2.40186,.30624,.25516,.69205,.3949,1.09082,.39502h10.56641c1.64404,9.68723,6.7168,18.46011,14.29254,24.71729,17.43427,14.39999,43.24109,11.94022,57.64099-5.49405,.04913-.05569,.09619-.11295,.14142-.17163,1.15088-1.49146,.87454-3.63327-.61691-4.784h.00006Z"
fill="#fff"
/>
</g>
</g>
<path
id="uuid-da16df1e-5659-4232-96f6-61e8c639a9ec-574"
d="M356.98148,237.19363c-1.02939,7.36621-5.66458,12.80598-10.35239,12.15012-4.68781-.65588-7.65225-7.15837-6.62149-14.52707,.37137-2.94914,1.4436-5.76646,3.12701-8.21626l4.75577-31.15587,14.57297,2.54338-6.23553,30.44414c.94736,2.81844,1.20581,5.82278,.75369,8.76157h-.00003Z"
fill="#ffb6b6"
/>
<path
d="M369.66196,77.68643s-15-5-17,13-4,39.99999-4,39.99999c0,0-9,21-5,32s11,3.3307,4,12.66534-6.02478,40.04724-6.02478,40.04724l22.52478-1.13387s12.5-82.57875,12.5-84.57875-7-52-7-52v.00004Z"
fill="#e6e6e6"
/>
<g>
<path
id="uuid-6bf35aa9-e432-4b51-af77-8f4eb19e6e42-575"
d="M467.16132,233.84998c.27881,7.43257-3.33017,13.60114-8.06033,13.7778s-8.78937-5.70491-9.06732-13.14017c-.15176-2.96857,.40961-5.93028,1.63712-8.63741l-.78369-31.507,14.79315-.05261-.798,31.0659c1.42709,2.60854,2.20859,5.52095,2.27905,8.49347l.00003,.00002Z"
fill="#ffb6b6"
/>
<path
d="M444.06961,77.34876s15.08694-4.73121,16.76505,13.30165,3.28473,51.06508,3.28473,51.06508c0,0,8.62338,21.15744,4.42749,32.08421s-11.05774,3.13365-4.22565,12.59187c6.83212,9.45822,4.37997,36.13126,4.37997,36.13126l-22.50095-1.53612s-10.09427-78.77167-10.05853-80.77133,7.92792-62.86664,7.92792-62.86664l-.00003,.00002Z"
fill="#e6e6e6"
/>
</g>
</g>
</svg>
</template>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,2 @@
export type * from './fallback';
export { default as Fallback } from './fallback.vue';

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import { preferences } from '@vben-core/preferences';
import { Toaster } from '@vben-core/shadcn-ui';
import { CozeAssistant } from '../coze-assistant';
defineOptions({ name: 'GlobalProvider' });
</script>
<template>
<Toaster />
<CozeAssistant
v-if="preferences.app.aiAssistant"
:is-mobile="preferences.app.isMobile"
/>
<slot></slot>
</template>

View File

@@ -0,0 +1 @@
export { default as GlobalProvider } from './global-provider.vue';

View File

@@ -0,0 +1,124 @@
<script setup lang="ts">
import type { MenuRecordRaw } from '@vben/types';
import { ref } from 'vue';
import { $t } from '@vben/locales';
import {
IcRoundArrowDownward,
IcRoundArrowUpward,
IcRoundSearch,
IcRoundSubdirectoryArrowLeft,
MdiKeyboardEsc,
} from '@vben-core/iconify';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@vben-core/shadcn-ui';
import { isWindowsOs } from '@vben-core/toolkit';
import { useMagicKeys, useToggle, whenever } from '@vueuse/core';
import SearchPanel from './search-panel.vue';
defineOptions({
name: 'GlobalSearch',
});
const props = withDefaults(
defineProps<{ enableShortcutKey?: boolean; menus: MenuRecordRaw[] }>(),
{
enableShortcutKey: true,
menus: () => [],
},
);
const [open, toggleOpen] = useToggle();
const keyword = ref('');
function handleClose() {
open.value = false;
keyword.value = '';
}
if (props.enableShortcutKey) {
const keys = useMagicKeys();
const cmd = isWindowsOs() ? keys['ctrl+k'] : keys['cmd+k'];
whenever(cmd, () => {
if (props.enableShortcutKey) {
open.value = true;
}
});
}
</script>
<template>
<div>
<Dialog :open="open">
<DialogTrigger as-child>
<div
class="md:bg-accent group flex h-8 cursor-pointer items-center gap-3 rounded-2xl border-none bg-none px-2 py-0.5 outline-none"
@click="toggleOpen()"
>
<IcRoundSearch
class="text-muted-foreground group-hover:text-foreground size-4 group-hover:opacity-100"
/>
<span
class="text-muted-foreground group-hover:text-foreground hidden text-sm duration-300 md:block"
>
{{ $t('search.search') }}
</span>
<span
v-if="enableShortcutKey"
class="bg-background border-foreground/50 text-muted-foreground group-hover:text-foreground relative hidden rounded-sm rounded-r-xl px-1.5 py-1 text-xs leading-none group-hover:opacity-100 md:block"
>
{{ isWindowsOs() ? 'Ctrl' : '⌘' }}
<kbd>K</kbd>
</span>
<span v-else></span>
</div>
</DialogTrigger>
<DialogContent
class="top-0 h-full w-full -translate-y-0 border-none p-0 shadow-xl sm:top-[10%] sm:h-[unset] sm:w-[600px] sm:rounded-2xl"
@close="handleClose"
>
<DialogHeader>
<DialogTitle
class="border-border flex h-12 items-center gap-5 border-b px-5 font-normal"
>
<IcRoundSearch class="mt-1 size-4" />
<input
v-model="keyword"
:placeholder="$t('search.search-navigate')"
class="ring-none placeholder:text-muted-foreground w-[80%] rounded-md border border-none bg-transparent p-2 text-sm outline-none ring-0 ring-offset-transparent focus-visible:ring-transparent"
/>
</DialogTitle>
<DialogDescription />
</DialogHeader>
<SearchPanel :keyword="keyword" :menus="menus" @close="handleClose" />
<DialogFooter
class="text-muted-foreground border-border hidden flex-row rounded-b-2xl border-t px-4 py-2 text-xs sm:flex sm:justify-start sm:gap-x-4"
>
<div class="flex items-center">
<IcRoundSubdirectoryArrowLeft class="mr-1" />
{{ $t('search.select') }}
</div>
<div class="flex items-center">
<IcRoundArrowUpward class="mr-2" />
<IcRoundArrowDownward class="mr-2" />
{{ $t('search.navigate') }}
</div>
<div class="flex items-center">
<MdiKeyboardEsc class="mr-1" />
{{ $t('search.close') }}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as GlobalSearch } from './global-search.vue';

View File

@@ -0,0 +1,279 @@
<script setup lang="ts">
import type { MenuRecordRaw } from '@vben/types';
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { IcRoundClose, IcRoundSearchOff } from '@vben-core/iconify';
import { VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
import { mapTree, traverseTreeValues } from '@vben-core/toolkit';
import { onKeyStroke, useLocalStorage, useThrottleFn } from '@vueuse/core';
defineOptions({
name: 'SearchPanel',
});
const props = withDefaults(
defineProps<{ keyword: string; menus: MenuRecordRaw[] }>(),
{
keyword: '',
menus: () => [],
},
);
const emit = defineEmits<{ close: [] }>();
const router = useRouter();
const searchHistory = useLocalStorage<MenuRecordRaw[]>(
`__search-history-${import.meta.env.PROD ? 'prod' : 'dev'}__`,
[],
);
const activeIndex = ref(-1);
const searchItems = shallowRef<MenuRecordRaw[]>([]);
const searchResults = ref<MenuRecordRaw[]>([]);
const handleSearch = useThrottleFn(search, 200);
// 搜索函数,用于根据搜索关键词查找匹配的菜单项
function search(searchKey: string) {
// 去除搜索关键词的前后空格
searchKey = searchKey.trim();
// 如果搜索关键词为空,清空搜索结果并返回
if (!searchKey) {
searchResults.value = [];
return;
}
// 使用搜索关键词创建正则表达式
const reg = createSearchReg(searchKey);
// 初始化结果数组
const results: MenuRecordRaw[] = [];
// 遍历搜索项
traverseTreeValues(searchItems.value, (item) => {
// 如果菜单项的名称匹配正则表达式,将其添加到结果数组中
if (reg.test(item.name?.toLowerCase())) {
results.push(item);
}
});
// 更新搜索结果
searchResults.value = results;
// 如果有搜索结果,设置索引为 0
if (results.length > 0) {
activeIndex.value = 0;
}
// 赋值索引为 0
activeIndex.value = 0;
}
// When the keyboard up and down keys move to an invisible place
// the scroll bar needs to scroll automatically
function scrollIntoView() {
const element = document.querySelector(
`[data-search-item="${activeIndex.value}"`,
);
if (element) {
element.scrollIntoView({ block: 'nearest' });
}
}
// enter keyboard event
async function handleEnter() {
if (searchResults.value.length === 0) {
return;
}
const result = searchResults.value;
const index = activeIndex.value;
if (result.length === 0 || index < 0) {
return;
}
const to = result[index];
searchHistory.value.push(to);
handleClose();
await nextTick();
router.push(to.path);
}
// Arrow key up
function handleUp() {
if (searchResults.value.length === 0) {
return;
}
activeIndex.value--;
if (activeIndex.value < 0) {
activeIndex.value = searchResults.value.length - 1;
}
scrollIntoView();
}
// Arrow key down
function handleDown() {
if (searchResults.value.length === 0) {
return;
}
activeIndex.value++;
if (activeIndex.value > searchResults.value.length - 1) {
activeIndex.value = 0;
}
scrollIntoView();
}
// close search modal
function handleClose() {
searchResults.value = [];
emit('close');
}
// Activate when the mouse moves to a certain line
function handleMouseenter(e: MouseEvent) {
const index = (e.target as HTMLElement)?.dataset.index;
activeIndex.value = Number(index);
}
function removeItem(index: number) {
if (props.keyword) {
searchResults.value.splice(index, 1);
} else {
searchHistory.value.splice(index, 1);
}
activeIndex.value = activeIndex.value - 1 >= 0 ? activeIndex.value - 1 : 0;
scrollIntoView();
}
// 存储所有需要转义的特殊字符
const code = new Set([
'$',
'(',
')',
'*',
'+',
'.',
'[',
']',
'?',
'\\',
'^',
'{',
'}',
'|',
]);
// 转换函数,用于转义特殊字符
function transform(c: string) {
// 如果字符在特殊字符列表中,返回转义后的字符
// 如果不在,返回字符本身
return code.has(c) ? `\\${c}` : c;
}
// 创建搜索正则表达式
function createSearchReg(key: string) {
// 将输入的字符串拆分为单个字符
// 对每个字符进行转义
// 然后用'.*'连接所有字符,创建正则表达式
const keys = [...key].map((item) => transform(item)).join('.*');
// 返回创建的正则表达式
return new RegExp(`.*${keys}.*`);
}
watch(
() => props.keyword,
(val) => {
if (val) {
handleSearch(val);
} else {
searchResults.value = [...searchHistory.value];
}
},
);
onMounted(() => {
searchItems.value = mapTree(props.menus, (item) => {
return {
...item,
name: $t(item?.name),
};
});
if (searchHistory.value.length > 0) {
searchResults.value = searchHistory.value;
}
// enter search
onKeyStroke('Enter', handleEnter);
// Monitor keyboard arrow keys
onKeyStroke('ArrowUp', handleUp);
onKeyStroke('ArrowDown', handleDown);
// esc close
onKeyStroke('Escape', handleClose);
});
</script>
<template>
<VbenScrollbar>
<div class="!flex h-full justify-center px-4 sm:max-h-[450px]">
<!-- 无搜索结果 -->
<div
v-if="keyword && searchResults.length === 0"
class="text-muted-foreground text-center"
>
<IcRoundSearchOff class="size-12" />
<p class="my-10 text-xs">
{{ $t('search.no-results') }}
<span class="text-foreground text-sm font-medium">
"{{ keyword }}"
</span>
</p>
</div>
<!-- 历史搜索记录 & 没有搜索结果 -->
<div
v-if="!keyword && searchResults.length === 0"
class="text-muted-foreground text-center"
>
<p class="my-10 text-xs">
{{ $t('search.no-recent') }}
</p>
</div>
<ul v-show="searchResults.length > 0" class="w-full">
<li
v-if="searchHistory.length > 0 && !keyword"
class="text-muted-foreground mb-2 text-xs"
>
{{ $t('search.recent') }}
</li>
<li
v-for="(item, index) in searchResults"
:key="item.path"
:class="
activeIndex === index
? 'active bg-primary text-primary-foreground text-muted-foreground'
: ''
"
:data-index="index"
:data-search-item="index"
class="bg-accent flex-center group mb-3 w-full cursor-pointer rounded-lg px-4 py-4"
@mouseenter="handleMouseenter"
>
<VbenIcon
:icon="item.icon"
class="mr-2 size-5 flex-shrink-0"
fallback
/>
<span class="flex-1">{{ item.name }}</span>
<div
class="flex-center dark:hover:bg-accent hover:text-primary-foreground rounded-full p-1 hover:scale-110"
@click.stop="removeItem(index)"
>
<IcRoundClose />
</div>
</li>
</ul>
</div>
</VbenScrollbar>
</template>

View File

@@ -0,0 +1,10 @@
export * from './authentication';
export * from './coze-assistant';
export * from './fallback';
export * from './global-provider';
export * from './global-search';
export * from './language-toggle';
export * from './notification';
export * from './preferences';
export * from './theme-toggle';
export * from './user-dropdown';

View File

@@ -0,0 +1 @@
export { default as LanguageToggle } from './language-toggle.vue';

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import type { SupportedLanguagesType } from '@vben/types';
import { loadLocaleMessages } from '@vben/locales';
import { IcBaselineLanguage } from '@vben-core/iconify';
import {
SUPPORT_LANGUAGES,
preferences,
updatePreferences,
} from '@vben-core/preferences';
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
defineOptions({
name: 'LanguageToggle',
});
const menus = SUPPORT_LANGUAGES;
async function handleUpdate(value: string) {
const locale = value as SupportedLanguagesType;
updatePreferences({
app: {
locale,
},
});
// 更改预览
await loadLocaleMessages(locale);
}
</script>
<template>
<div>
<VbenDropdownRadioMenu
:menus="menus"
:model-value="preferences.app.locale"
@update:model-value="handleUpdate"
>
<VbenIconButton>
<IcBaselineLanguage class="size-5" />
</VbenIconButton>
</VbenDropdownRadioMenu>
</div>
</template>

View File

@@ -0,0 +1,3 @@
export type * from './interface';
export { default as Notification } from './notification.vue';

View File

@@ -0,0 +1,9 @@
interface NotificationItem {
avatar: string;
date: string;
isRead?: boolean;
message: string;
title: string;
}
export type { NotificationItem };

View File

@@ -0,0 +1,183 @@
<script lang="ts" setup>
import type { NotificationItem } from './interface';
import { $t } from '@vben/locales';
import {
IcRoundMarkEmailRead,
IcRoundNotificationsNone,
} from '@vben-core/iconify';
import {
VbenButton,
VbenIconButton,
VbenPopover,
VbenScrollbar,
} from '@vben-core/shadcn-ui';
import { useToggle } from '@vueuse/core';
interface Props {
/**
* 显示圆点
*/
dot?: boolean;
/**
* 消息列表
*/
notifications?: NotificationItem[];
}
defineOptions({ name: 'NotificationPopup' });
withDefaults(defineProps<Props>(), {
dot: false,
notifications: () => [],
});
const emit = defineEmits<{
clear: [];
makeAll: [];
read: [NotificationItem];
viewAll: [];
}>();
const [open, toggle] = useToggle();
function close() {
open.value = false;
}
function handleViewAll() {
emit('viewAll');
close();
}
function handleMakeAll() {
emit('makeAll');
}
function handleClear() {
emit('clear');
}
function handleClick(item: NotificationItem) {
emit('read', item);
}
</script>
<template>
<VbenPopover
v-model:open="open"
content-class="relative right-2 w-[360px] p-0"
>
<template #trigger>
<div class="flex-center mr-2 h-full" @click.stop="toggle()">
<VbenIconButton class="bell-button relative">
<span
v-if="dot"
class="bg-primary absolute right-0.5 top-0.5 h-2 w-2 rounded"
></span>
<IcRoundNotificationsNone class="size-5" />
</VbenIconButton>
</div>
</template>
<div class="relative">
<div class="flex items-center justify-between p-4 py-3">
<div class="text-foreground">{{ $t('widgets.notifications') }}</div>
<VbenIconButton
:tooltip="$t('widgets.make-all-as-read')"
@click="handleMakeAll"
>
<IcRoundMarkEmailRead />
</VbenIconButton>
</div>
<VbenScrollbar v-if="notifications.length > 0">
<ul class="!flex max-h-[360px] w-full flex-col">
<template v-for="item in notifications" :key="item.title">
<li
class="hover:bg-accent border-border relative flex w-full cursor-pointer items-start gap-5 border-t px-3 py-3"
@click="handleClick(item)"
>
<span
v-if="!item.isRead"
class="bg-primary absolute right-2 top-2 h-2 w-2 rounded"
></span>
<span
class="relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full"
>
<img
:src="item.avatar"
class="aspect-square h-full w-full object-cover"
role="img"
/>
</span>
<div class="flex flex-col gap-1 leading-none">
<p class="font-semibold">{{ item.title }}</p>
<p class="text-muted-foreground my-1 line-clamp-2 text-xs">
{{ item.message }}
</p>
<p class="text-muted-foreground line-clamp-2 text-xs">
{{ item.date }}
</p>
</div>
</li>
</template>
</ul>
</VbenScrollbar>
<template v-else>
<div class="flex-center text-muted-foreground min-h-[150px] w-full">
{{ $t('common.not-data') }}
</div>
</template>
<div
class="border-border flex items-center justify-between border-t px-4 py-3"
>
<VbenButton size="sm" variant="ghost" @click="handleClear">
{{ $t('widgets.clear-notifications') }}
</VbenButton>
<VbenButton size="sm" @click="handleViewAll">
{{ $t('widgets.view-all') }}
</VbenButton>
</div>
</div>
</VbenPopover>
</template>
<style scoped>
:deep(.bell-button) {
&:hover {
svg {
animation: bell-ring 1s both;
}
}
}
@keyframes bell-ring {
0%,
100% {
transform-origin: top;
}
15% {
transform: rotateZ(10deg);
}
30% {
transform: rotateZ(-10deg);
}
45% {
transform: rotateZ(5deg);
}
60% {
transform: rotateZ(-5deg);
}
75% {
transform: rotateZ(2deg);
}
}
</style>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
interface Props {
title?: string;
}
defineOptions({
name: 'PreferenceBlock',
});
withDefaults(defineProps<Props>(), {
title: '',
});
</script>
<template>
<div class="flex flex-col py-4">
<h3 class="mb-3 font-semibold leading-none tracking-tight">
{{ title }}
</h3>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceAnimation',
});
const transitionProgress = defineModel<boolean>('transitionProgress', {
// 默认值
default: false,
});
const transitionName = defineModel<string>('transitionName');
const transitionEnable = defineModel<boolean>('transitionEnable');
const transitionPreset = ['fade', 'fade-slide', 'fade-up', 'fade-down'];
function handleClick(value: string) {
transitionName.value = value;
}
</script>
<template>
<SwitchItem v-model="transitionProgress">
{{ $t('preferences.page-progress') }}
</SwitchItem>
<SwitchItem v-model="transitionEnable">
{{ $t('preferences.page-transition') }}
</SwitchItem>
<div
v-if="transitionEnable"
class="mb-2 mt-3 flex justify-between gap-3 px-2"
>
<div
v-for="item in transitionPreset"
:key="item"
:class="{
'outline-box-active': transitionName === item,
}"
class="outline-box p-2"
@click="handleClick(item)"
>
<div :class="`${item}-slow`" class="bg-accent h-10 w-12 rounded-md"></div>
</div>
</div>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import type { SelectListItem } from '@vben/types';
import { $t } from '@vben/locales';
import { SUPPORT_LANGUAGES } from '@vben-core/preferences';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceGeneralConfig',
});
const appLocale = defineModel<string>('appLocale');
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
const appAiAssistant = defineModel<boolean>('appAiAssistant');
const localeItems: SelectListItem[] = SUPPORT_LANGUAGES.map((item) => ({
label: item.text,
value: item.key,
}));
</script>
<template>
<SelectItem v-model="appLocale" :items="localeItems">
{{ $t('preferences.language') }}
</SelectItem>
<SwitchItem v-model="appDynamicTitle">
{{ $t('preferences.dynamic-title') }}
</SwitchItem>
<SwitchItem v-model="appAiAssistant">
{{ $t('preferences.ai-assistant') }}
</SwitchItem>
</template>

View File

@@ -0,0 +1,16 @@
export { default as Block } from './block.vue';
export { default as Animation } from './general/animation.vue';
export { default as General } from './general/general.vue';
export { default as Breadcrumb } from './layout/breadcrumb.vue';
export { default as Content } from './layout/content.vue';
export { default as Footer } from './layout/footer.vue';
export { default as Header } from './layout/header.vue';
export { default as Layout } from './layout/layout.vue';
export { default as Navigation } from './layout/navigation.vue';
export { default as Sidebar } from './layout/sidebar.vue';
export { default as Tabbar } from './layout/tabbar.vue';
export { default as GlobalShortcutKeys } from './shortcut-keys/global.vue';
export { default as SwitchItem } from './switch-item.vue';
export { default as ThemeColor } from './theme/color.vue';
export { default as ColorMode } from './theme/color-mode.vue';
export { default as Theme } from './theme/theme.vue';

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import type { SelectListItem } from '@vben/types';
import { computed } from 'vue';
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
import ToggleItem from '../toggle-item.vue';
defineOptions({
name: 'PreferenceBreadcrumbConfig',
});
const props = defineProps<{ disabled?: boolean }>();
const breadcrumbEnable = defineModel<boolean>('breadcrumbEnable');
const breadcrumbShowIcon = defineModel<boolean>('breadcrumbShowIcon');
const breadcrumbStyleType = defineModel<string>('breadcrumbStyleType');
const breadcrumbShowHome = defineModel<boolean>('breadcrumbShowHome');
const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
const typeItems: SelectListItem[] = [
{ label: $t('preferences.normal'), value: 'normal' },
{ label: $t('preferences.breadcrumb-background'), value: 'background' },
];
const disableItem = computed(() => {
return !breadcrumbEnable.value || props.disabled;
});
</script>
<template>
<SwitchItem v-model="breadcrumbEnable" :disabled="disabled">
{{ $t('preferences.breadcrumb-enable') }}
</SwitchItem>
<SwitchItem v-model="breadcrumbHideOnlyOne" :disabled="disableItem">
{{ $t('preferences.breadcrumb-hide-only-one') }}
</SwitchItem>
<SwitchItem v-model="breadcrumbShowHome" :disabled="disableItem">
{{ $t('preferences.breadcrumb-home') }}
</SwitchItem>
<SwitchItem v-model="breadcrumbShowIcon" :disabled="disableItem">
{{ $t('preferences.breadcrumb-icon') }}
</SwitchItem>
<ToggleItem
v-model="breadcrumbStyleType"
:disabled="disableItem"
:items="typeItems"
>
{{ $t('preferences.breadcrumb-style') }}
</ToggleItem>
</template>

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import { type Component, computed } from 'vue';
import { $t } from '@vben/locales';
import { ContentCompact, ContentWide } from '../../icons';
defineOptions({
name: 'PreferenceLayoutContent',
});
const modelValue = defineModel<string>({ default: 'wide' });
const components: Record<string, Component> = {
compact: ContentCompact,
wide: ContentWide,
};
const PRESET = computed(() => [
{
name: $t('preferences.wide'),
type: 'wide',
},
{
name: '定宽',
type: 'compact',
},
]);
function activeClass(theme: string): string[] {
return theme === modelValue.value ? ['outline-box-active'] : [];
}
</script>
<template>
<div class="flex w-full gap-5">
<template v-for="theme in PRESET" :key="theme.name">
<div
class="flex w-[100px] cursor-pointer flex-col"
@click="modelValue = theme.type"
>
<div :class="activeClass(theme.type)" class="outline-box flex-center">
<component :is="components[theme.type]" />
</div>
<div class="text-muted-foreground mt-2 text-center text-xs">
{{ theme.name }}
</div>
</div>
</template>
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceBreadcrumbConfig',
});
const footerEnable = defineModel<boolean>('footerEnable');
const footerFixed = defineModel<boolean>('footerFixed');
</script>
<template>
<SwitchItem v-model="footerEnable">
{{ $t('preferences.footer-visible') }}
</SwitchItem>
<SwitchItem v-model="footerFixed" :disabled="!footerEnable">
{{ $t('preferences.footer-fixed') }}
</SwitchItem>
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { LayoutHeaderModeType, SelectListItem } from '@vben/types';
import { $t } from '@vben/locales';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceBreadcrumbConfig',
});
defineProps<{ disabled: boolean }>();
const headerEnable = defineModel<boolean>('headerEnable');
const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
const localeItems: SelectListItem[] = [
{
label: $t('preferences.header-mode-static'),
value: 'static',
},
{
label: $t('preferences.header-mode-fixed'),
value: 'fixed',
},
{
label: $t('preferences.header-mode-auto'),
value: 'auto',
},
{
label: $t('preferences.header-mode-auto-scroll'),
value: 'auto-scroll',
},
];
</script>
<template>
<SwitchItem v-model="headerEnable" :disabled="disabled">
{{ $t('preferences.header-visible') }}
</SwitchItem>
<SelectItem
v-model="headerMode"
:disabled="!headerEnable"
:items="localeItems"
>
{{ $t('preferences.mode') }}
</SelectItem>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceInterfaceControl',
});
const tabsVisible = defineModel<boolean>('tabsVisible');
const logoVisible = defineModel<boolean>('logoVisible');
</script>
<template>
<SwitchItem v-model="tabsVisible">
{{ $t('preferences.tabs-visible') }}
</SwitchItem>
<SwitchItem v-model="logoVisible">
{{ $t('preferences.logo-visible') }}
</SwitchItem>
</template>

View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
import type { LayoutType } from '@vben/types';
import { type Component, computed } from 'vue';
import { $t } from '@vben/locales';
import { MdiQuestionMarkCircleOutline } from '@vben-core/iconify';
import { VbenTooltip } from '@vben-core/shadcn-ui';
import {
FullContent,
HeaderNav,
MixedNav,
SidebarMixedNav,
SidebarNav,
} from '../../icons';
interface PresetItem {
name: string;
tip: string;
type: LayoutType;
}
defineOptions({
name: 'PreferenceLayout',
});
const modelValue = defineModel<LayoutType>({ default: 'sidebar-nav' });
const components: Record<LayoutType, Component> = {
'full-content': FullContent,
'header-nav': HeaderNav,
'mixed-nav': MixedNav,
'sidebar-mixed-nav': SidebarMixedNav,
'sidebar-nav': SidebarNav,
};
const PRESET = computed((): PresetItem[] => [
{
name: $t('preferences.vertical'),
tip: $t('preferences.vertical-tip'),
type: 'sidebar-nav',
},
{
name: $t('preferences.two-column'),
tip: $t('preferences.two-column-tip'),
type: 'sidebar-mixed-nav',
},
{
name: $t('preferences.horizontal'),
tip: $t('preferences.vertical-tip'),
type: 'header-nav',
},
{
name: $t('preferences.mixed-menu'),
tip: $t('preferences.mixed-menu-tip'),
type: 'mixed-nav',
},
{
name: $t('preferences.full-content'),
tip: $t('preferences.full-content-tip'),
type: 'full-content',
},
]);
function activeClass(theme: string): string[] {
return theme === modelValue.value ? ['outline-box-active'] : [];
}
</script>
<template>
<div class="flex w-full flex-wrap gap-5">
<template v-for="theme in PRESET" :key="theme.name">
<div
class="flex w-[100px] cursor-pointer flex-col"
@click="modelValue = theme.type"
>
<div :class="activeClass(theme.type)" class="outline-box flex-center">
<component :is="components[theme.type]" />
</div>
<div
class="text-muted-foreground flex-center hover:text-foreground mt-2 text-center text-xs"
>
{{ theme.name }}
<VbenTooltip v-if="theme.tip" side="bottom">
<template #trigger>
<MdiQuestionMarkCircleOutline class="ml-1 cursor-help" />
</template>
{{ theme.tip }}
</VbenTooltip>
</div>
</div>
</template>
</div>
</template>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import type { SelectListItem } from '@vben/types';
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
import ToggleItem from '../toggle-item.vue';
defineOptions({
name: 'PreferenceNavigationConfig',
});
defineProps<{ disabled?: boolean; disabledNavigationSplit?: boolean }>();
const navigationStyleType = defineModel<string>('navigationStyleType');
const navigationSplit = defineModel<boolean>('navigationSplit');
const navigationAccordion = defineModel<boolean>('navigationAccordion');
const stylesItems: SelectListItem[] = [
{ label: $t('preferences.rounded'), value: 'rounded' },
{ label: $t('preferences.plain'), value: 'plain' },
];
</script>
<template>
<ToggleItem
v-model="navigationStyleType"
:disabled="disabled"
:items="stylesItems"
>
{{ $t('preferences.navigation-style') }}
</ToggleItem>
<SwitchItem
v-model="navigationSplit"
:disabled="disabledNavigationSplit || disabled"
>
{{ $t('preferences.navigation-split') }}
<template #tip>
{{ $t('preferences.navigation-split-tip') }}
</template>
</SwitchItem>
<SwitchItem v-model="navigationAccordion" :disabled="disabled">
{{ $t('preferences.navigation-accordion') }}
</SwitchItem>
</template>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceBreadcrumbConfig',
});
defineProps<{ disabled: boolean }>();
const sidebarEnable = defineModel<boolean>('sidebarEnable');
const sidebarCollapsedShowTitle = defineModel<boolean>(
'sidebarCollapsedShowTitle',
);
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
</script>
<template>
<SwitchItem v-model="sidebarEnable" :disabled="disabled">
{{ $t('preferences.side-visible') }}
</SwitchItem>
<SwitchItem v-model="sidebarCollapsed" :disabled="!sidebarEnable || disabled">
{{ $t('preferences.collapse') }}
</SwitchItem>
<SwitchItem
v-model="sidebarCollapsedShowTitle"
:disabled="!sidebarEnable || disabled"
>
{{ $t('preferences.collapse-show-title') }}
</SwitchItem>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceTabsConfig',
});
defineProps<{ disabled?: boolean }>();
const tabbarEnable = defineModel<boolean>('tabbarEnable');
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
</script>
<template>
<SwitchItem v-model="tabbarEnable" :disabled="disabled">
{{ $t('preferences.tabs-visible') }}
</SwitchItem>
<SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable">
{{ $t('preferences.tabs-icon') }}
</SwitchItem>
<!-- <SwitchItem v-model="sideCollapseShowTitle" :disabled="!tabsVisible">
{{ $t('preferences.collapse-show-title') }}
</SwitchItem> -->
</template>

View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
import type { SelectListItem } from '@vben/types';
import { useSlots } from 'vue';
import { MdiQuestionMarkCircleOutline } from '@vben-core/iconify';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
VbenTooltip,
} from '@vben-core/shadcn-ui';
defineOptions({
name: 'PreferenceSelectItem',
});
withDefaults(
defineProps<{
disabled?: boolean;
items?: SelectListItem[];
placeholder?: string;
}>(),
{
disabled: false,
placeholder: '',
items: () => [],
},
);
const selectValue = defineModel<string>();
const slots = useSlots();
</script>
<template>
<div
:class="{
'hover:bg-accent': !slots.tip,
'pointer-events-none opacity-50': disabled,
}"
class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"
>
<span class="flex items-center text-sm">
<slot></slot>
<VbenTooltip v-if="slots.tip" side="bottom">
<template #trigger>
<MdiQuestionMarkCircleOutline class="ml-1 cursor-help" />
</template>
<slot name="tip"></slot>
</VbenTooltip>
</span>
<Select v-model="selectValue">
<SelectTrigger class="h-7 w-[140px]">
<SelectValue :placeholder="placeholder" />
</SelectTrigger>
<SelectContent>
<template v-for="item in items" :key="item.value">
<SelectItem :value="item.value"> {{ item.label }} </SelectItem>
</template>
</SelectContent>
</Select>
</div>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@vben/locales';
import { isWindowsOs } from '@vben-core/toolkit';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceGeneralConfig',
});
const shortcutKeysEnable = defineModel<boolean>('shortcutKeysEnable');
const shortcutKeysGlobalSearch = defineModel<boolean>(
'shortcutKeysGlobalSearch',
);
const shortcutKeysLogout = defineModel<boolean>('shortcutKeysLogout');
const shortcutKeysPreferences = defineModel<boolean>('shortcutKeysPreferences');
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
</script>
<template>
<SwitchItem v-model="shortcutKeysEnable">
{{ $t('preferences.shortcut-keys.title') }}
</SwitchItem>
<SwitchItem v-if="shortcutKeysEnable" v-model="shortcutKeysGlobalSearch">
{{ $t('preferences.shortcut-keys.search') }}
<template #shortcut>
{{ isWindowsOs() ? 'Ctrl' : '⌘' }}
<kbd> K </kbd>
</template>
</SwitchItem>
<SwitchItem v-if="shortcutKeysEnable" v-model="shortcutKeysLogout">
{{ $t('preferences.shortcut-keys.logout') }}
<template #shortcut> {{ altView }} Q </template>
</SwitchItem>
<SwitchItem v-if="shortcutKeysEnable" v-model="shortcutKeysPreferences">
{{ $t('preferences.shortcut-keys.preferences') }}
<template #shortcut> {{ altView }} , </template>
</SwitchItem>
</template>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { useSlots } from 'vue';
import { MdiQuestionMarkCircleOutline } from '@vben-core/iconify';
import { Switch, VbenTooltip } from '@vben-core/shadcn-ui';
defineOptions({
name: 'PreferenceSwitchItem',
});
withDefaults(defineProps<{ disabled?: boolean }>(), {
disabled: false,
});
const checked = defineModel<boolean>();
const slots = useSlots();
function handleClick() {
checked.value = !checked.value;
}
</script>
<template>
<div
:class="{
'pointer-events-none opacity-50': disabled,
}"
class="hover:bg-accent my-1 flex w-full items-center justify-between rounded-md px-2 py-2"
@click="handleClick"
>
<span class="flex items-center text-sm">
<slot></slot>
<VbenTooltip v-if="slots.tip" side="bottom">
<template #trigger>
<MdiQuestionMarkCircleOutline class="ml-1 cursor-help" />
</template>
<slot name="tip"></slot>
</VbenTooltip>
</span>
<span v-if="$slots.shortcut" class="ml-auto mr-2 text-xs opacity-60">
<slot name="shortcut"></slot>
</span>
<Switch v-model:checked="checked" @click.stop />
</div>
</template>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceColorMode',
});
const appColorWeakMode = defineModel<boolean>('appColorWeakMode', {
default: false,
});
const appColorGrayMode = defineModel<boolean>('appColorGrayMode', {
default: false,
});
</script>
<template>
<SwitchItem v-model="appColorWeakMode">
{{ $t('preferences.weak-mode') }}
</SwitchItem>
<SwitchItem v-model="appColorGrayMode">
{{ $t('preferences.gray-mode') }}
</SwitchItem>
</template>

View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import type { CSSProperties } from 'vue';
import { computed, ref, watch, watchEffect } from 'vue';
import { MdiEditBoxOutline } from '@vben-core/iconify';
import { TinyColor, convertToHsl } from '@vben-core/toolkit';
defineOptions({
name: 'PreferenceColor',
});
const props = withDefaults(defineProps<{ colorPrimaryPresets: string[] }>(), {
colorPrimaryPresets: () => [],
});
const colorInput = ref();
const currentColor = ref(props.colorPrimaryPresets?.[0]);
const modelValue = defineModel<string>();
const activeColor = computed((): CSSProperties => {
return {
outlineColor: currentColor.value,
outlineWidth: '2px',
};
});
function isActive(color: string): string[] {
return color === currentColor.value ? ['outline-box-active'] : [];
}
const inputStyle = computed((): CSSProperties => {
return props.colorPrimaryPresets.includes(currentColor.value)
? {}
: activeColor.value;
});
const inputValue = computed(() => {
return new TinyColor(modelValue.value).toHexString();
});
function selectColor() {
colorInput.value.click();
}
function handleInputChange(e: Event) {
const target = e.target as HTMLInputElement;
modelValue.value = convertToHsl(target.value);
}
// 监听颜色变化,转成系统可识别的 hsl 格式
watch(currentColor, (val) => {
modelValue.value = convertToHsl(val);
});
watchEffect(() => {
if (modelValue.value) {
currentColor.value = modelValue.value;
}
});
</script>
<template>
<div class="flex w-full flex-wrap justify-between">
<template v-for="color in colorPrimaryPresets" :key="color">
<div
:class="isActive(color)"
class="outline-box p-2"
@click="currentColor = color"
>
<div
:style="{ backgroundColor: color }"
class="h-6 w-6 rounded-md"
></div>
</div>
</template>
<div :style="inputStyle" class="outline-box p-2" @click="selectColor">
<div class="flex-center bg-accent relative h-6 w-6 rounded-md">
<MdiEditBoxOutline class="absolute z-10" />
<input
ref="colorInput"
:value="inputValue"
class="absolute inset-0 opacity-0"
type="color"
@input="handleInputChange"
/>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,81 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import {
IcRoundMotionPhotosAuto,
IcRoundWbSunny,
MdiMoonAndStars,
} from '@vben-core/iconify';
import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceTheme',
});
const modelValue = defineModel<string>({ default: 'auto' });
const appSemiDarkMenu = defineModel<boolean>('appSemiDarkMenu', {
default: true,
});
const THEME_PRESET = [
{
icon: IcRoundWbSunny,
name: 'light',
},
{
icon: MdiMoonAndStars,
name: 'dark',
},
{
icon: IcRoundMotionPhotosAuto,
name: 'auto',
},
];
function activeClass(theme: string): string[] {
return theme === modelValue.value ? ['outline-box-active'] : [];
}
function nameView(name: string) {
switch (name) {
case 'light': {
return $t('preferences.light');
}
case 'dark': {
return $t('preferences.dark');
}
case 'auto': {
return $t('preferences.follow-system');
}
}
}
</script>
<template>
<div class="flex w-full flex-wrap justify-between">
<template v-for="theme in THEME_PRESET" :key="theme.name">
<div
class="flex cursor-pointer flex-col"
@click="modelValue = theme.name"
>
<div
:class="activeClass(theme.name)"
class="outline-box flex-center py-4"
>
<component :is="theme.icon" class="mx-9 size-5" />
</div>
<div class="text-muted-foreground mt-2 text-center text-xs">
{{ nameView(theme.name) }}
</div>
</div>
</template>
<SwitchItem
v-model="appSemiDarkMenu"
:disabled="modelValue !== 'light'"
class="mt-6"
>
{{ $t('preferences.dark-menu') }}
</SwitchItem>
</div>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import type { SelectListItem } from '@vben/types';
import { ToggleGroup, ToggleGroupItem } from '@vben-core/shadcn-ui';
defineOptions({
name: 'PreferenceToggleItem',
});
withDefaults(defineProps<{ disabled?: boolean; items: SelectListItem[] }>(), {
disabled: false,
items: () => [],
});
const modelValue = defineModel<string>();
</script>
<template>
<div
:class="{
'pointer-events-none opacity-50': disabled,
}"
class="hover:bg-accent flex w-full items-center justify-between rounded-md px-2 py-2"
disabled
>
<span class="text-sm"><slot></slot></span>
<ToggleGroup
v-model="modelValue"
class="gap-2"
size="sm"
type="single"
variant="outline"
>
<template v-for="item in items" :key="item.value">
<ToggleGroupItem
:value="item.value"
class="data-[state=on]:bg-primary data-[state=on]:text-primary-foreground h-7 rounded-sm"
>
{{ item.label }}
</ToggleGroupItem>
</template>
</ToggleGroup>
</div>
</template>

View File

@@ -0,0 +1,119 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
id="svg_1"
fill="currentColor"
fill-opacity="0.02"
height="66"
rx="4"
stroke="null"
width="104"
x="0.13514"
y="0.13514"
/>
<rect
id="svg_8"
fill="hsl(var(--color-primary))"
height="9.07027"
stroke="null"
width="104.07934"
x="-0.07419"
y="-0.05773"
/>
<rect
id="svg_3"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="15.58168"
y="3.20832"
/>
<path
id="svg_12"
d="m98.19822,2.872c0,-0.54338 0.45662,-1 1,-1l1.925,0c0.54338,0 1,0.45662 1,1l0,2.4c0,0.54338 -0.45662,1 -1,1l-1.925,0c-0.54338,0 -1,-0.45662 -1,-1l0,-2.4z"
fill="#ffffff"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="21.51892"
rx="2"
stroke="null"
width="41.98275"
x="45.37589"
y="13.53192"
/>
<path
id="svg_14"
d="m16.4123,15.53192c0,-1.08676 0.74096,-2 1.62271,-2l21.74653,0c0.88175,0 1.62271,0.91324 1.62271,2l0,17.24865c0,1.08676 -0.74096,2 -1.62271,2l-21.74653,0c-0.88175,0 -1.62271,-0.91324 -1.62271,-2l0,-17.24865z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="21.65405"
rx="2"
stroke="null"
width="71.10636"
x="16.54743"
y="39.34689"
/>
<rect
id="svg_21"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="28.14924"
y="3.07319"
/>
<rect
id="svg_22"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="41.25735"
y="3.20832"
/>
<rect
id="svg_23"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="54.23033"
y="3.07319"
/>
<rect
id="svg_4"
fill="#ffffff"
height="7.13843"
rx="2"
stroke="null"
width="7.78397"
x="1.5327"
y="0.881"
/>
</g>
</svg>
</template>

View File

@@ -0,0 +1,50 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
id="svg_1"
d="m0.13514,4.13514c0,-2.17352 1.82648,-4 4,-4l96,0c2.17352,0 4,1.82648 4,4l0,58c0,2.17352 -1.82648,4 -4,4l-96,0c-2.17352,0 -4,-1.82648 -4,-4l0,-58z"
fill="currentColor"
fill-opacity="0.02"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="26.57155"
rx="2"
stroke="null"
width="53.18333"
x="45.79979"
y="3.77232"
/>
<path
id="svg_14"
d="m4.28142,5.96169c0,-1.37748 1.06465,-2.53502 2.33158,-2.53502l31.2463,0c1.26693,0 2.33158,1.15754 2.33158,2.53502l0,21.86282c0,1.37748 -1.06465,2.53502 -2.33158,2.53502l-31.2463,0c-1.26693,0 -2.33158,-1.15754 -2.33158,-2.53502l0,-21.86282z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="25.02247"
rx="2"
stroke="null"
width="94.39371"
x="4.56735"
y="34.92584"
/>
</g>
</svg>
</template>

View File

@@ -0,0 +1,119 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
id="svg_1"
fill="currentColor"
fill-opacity="0.02"
height="66"
rx="4"
stroke="null"
width="104"
x="0.13514"
y="0.13514"
/>
<rect
id="svg_8"
fill="hsl(var(--color-primary))"
height="9.07027"
stroke="null"
width="104.07934"
x="-0.07419"
y="-0.05773"
/>
<rect
id="svg_3"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="15.58168"
y="3.20832"
/>
<path
id="svg_12"
d="m98.19822,2.872c0,-0.54338 0.45662,-1 1,-1l1.925,0c0.54338,0 1,0.45662 1,1l0,2.4c0,0.54338 -0.45662,1 -1,1l-1.925,0c-0.54338,0 -1,-0.45662 -1,-1l0,-2.4z"
fill="#ffffff"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="21.51892"
rx="2"
stroke="null"
width="53.60438"
x="43.484"
y="13.66705"
/>
<path
id="svg_14"
d="m3.43932,15.53192c0,-1.08676 1.03344,-2 2.26323,-2l30.33036,0c1.22979,0 2.26323,0.91324 2.26323,2l0,17.24865c0,1.08676 -1.03344,2 -2.26323,2l-30.33036,0c-1.22979,0 -2.26323,-0.91324 -2.26323,-2l0,-17.24865z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="21.65405"
rx="2"
stroke="null"
width="95.02528"
x="3.30419"
y="39.34689"
/>
<rect
id="svg_21"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="28.14924"
y="3.07319"
/>
<rect
id="svg_22"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="41.25735"
y="3.20832"
/>
<rect
id="svg_23"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="54.23033"
y="3.07319"
/>
<rect
id="svg_4"
fill="#ffffff"
height="7.13843"
rx="2"
stroke="null"
width="7.78397"
x="1.5327"
y="0.881"
/>
</g>
</svg>
</template>

View File

@@ -0,0 +1,10 @@
import HeaderNav from './header-nav.vue';
export { default as ContentCompact } from './content-compact.vue';
export { default as FullContent } from './full-content.vue';
export { default as MixedNav } from './mixed-nav.vue';
export { default as SidebarMixedNav } from './sidebar-mixed-nav.vue';
export { default as SidebarNav } from './sidebar-nav.vue';
const ContentWide = HeaderNav;
export { ContentWide, HeaderNav };

View File

@@ -0,0 +1,161 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
id="svg_1"
fill="currentColor"
fill-opacity="0.02"
height="66"
rx="4"
stroke="null"
width="104"
x="0.13514"
y="0.13514"
/>
<rect
id="svg_8"
fill="hsl(var(--color-primary))"
height="9.07027"
stroke="null"
width="104.07934"
x="-0.07419"
y="-0.05773"
/>
<rect
id="svg_3"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="15.58168"
y="3.20832"
/>
<path
id="svg_12"
d="m98.19822,2.872c0,-0.54338 0.45662,-1 1,-1l1.925,0c0.54338,0 1,0.45662 1,1l0,2.4c0,0.54338 -0.45662,1 -1,1l-1.925,0c-0.54338,0 -1,-0.45662 -1,-1l0,-2.4z"
fill="#ffffff"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="21.51892"
rx="2"
stroke="null"
width="44.13071"
x="53.37873"
y="13.45652"
/>
<path
id="svg_14"
d="m19.4393,15.74245c0,-1.08676 0.79001,-2 1.73013,-2l23.18605,0c0.94011,0 1.73013,0.91324 1.73013,2l0,17.24865c0,1.08676 -0.79001,2 -1.73013,2l-23.18605,0c-0.94011,0 -1.73013,-0.91324 -1.73013,-2l0,-17.24865z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="21.65405"
rx="2"
stroke="null"
width="78.39372"
x="19.93575"
y="39.34689"
/>
<rect
id="svg_21"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="28.14924"
y="3.07319"
/>
<rect
id="svg_22"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="41.25735"
y="3.20832"
/>
<rect
id="svg_23"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="7.52486"
x="54.23033"
y="3.07319"
/>
<rect
id="svg_4"
fill="#ffffff"
height="7.13843"
rx="2"
stroke="null"
width="7.78397"
x="1.5327"
y="0.881"
/>
<rect
id="svg_5"
fill="currentColor"
fill-opacity="0.08"
height="56.81191"
stroke="null"
width="15.44642"
x="-0.06423"
y="9.03113"
/>
<path
id="svg_2"
d="m2.38669,15.38074c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
<path
id="svg_6"
d="m2.38669,28.43336c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
<path
id="svg_7"
d="m2.17616,41.27545c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
<path
id="svg_9"
d="m2.17616,54.32806c0,-0.20384 0.27195,-0.37513 0.59557,-0.37513l7.98149,0c0.32362,0 0.59557,0.17129 0.59557,0.37513l0,3.23525c0,0.20384 -0.27195,0.37513 -0.59557,0.37513l-7.98149,0c-0.32362,0 -0.59557,-0.17129 -0.59557,-0.37513l0,-3.23525z"
fill="currentColor"
fill-opacity="0.08"
opacity="undefined"
stroke="null"
/>
</g>
</svg>
</template>

View File

@@ -0,0 +1,13 @@
<template>
<svg
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.9 12.66a1 1 0 0 1 0-1.32l1.28-1.44a1 1 0 0 0 .12-1.17l-2-3.46a1 1 0 0 0-1.07-.48l-1.88.38a1 1 0 0 1-1.15-.66l-.61-1.83a1 1 0 0 0-.95-.68h-4a1 1 0 0 0-1 .68l-.56 1.83a1 1 0 0 1-1.15.66L5 4.79a1 1 0 0 0-1 .48L2 8.73a1 1 0 0 0 .1 1.17l1.27 1.44a1 1 0 0 1 0 1.32L2.1 14.1a1 1 0 0 0-.1 1.17l2 3.46a1 1 0 0 0 1.07.48l1.88-.38a1 1 0 0 1 1.15.66l.61 1.83a1 1 0 0 0 1 .68h4a1 1 0 0 0 .95-.68l.61-1.83a1 1 0 0 1 1.15-.66l1.88.38a1 1 0 0 0 1.07-.48l2-3.46a1 1 0 0 0-.12-1.17ZM18.41 14l.8.9l-1.28 2.22l-1.18-.24a3 3 0 0 0-3.45 2L12.92 20h-2.56L10 18.86a3 3 0 0 0-3.45-2l-1.18.24l-1.3-2.21l.8-.9a3 3 0 0 0 0-4l-.8-.9l1.28-2.2l1.18.24a3 3 0 0 0 3.45-2L10.36 4h2.56l.38 1.14a3 3 0 0 0 3.45 2l1.18-.24l1.28 2.22l-.8.9a3 3 0 0 0 0 3.98m-6.77-6a4 4 0 1 0 4 4a4 4 0 0 0-4-4m0 6a2 2 0 1 1 2-2a2 2 0 0 1-2 2"
fill="white"
/>
</svg>
</template>

View File

@@ -0,0 +1,173 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
id="svg_1"
fill="currentColor"
fill-opacity="0.02"
height="66"
rx="4"
stroke="null"
width="104"
x="0.13514"
y="0.13514"
/>
<path
id="svg_2"
d="m-3.37838,3.7543a1.93401,4.02457 0 0 1 1.93401,-4.02457l11.3488,0l0,66.40541l-11.3488,0a1.93401,4.02457 0 0 1 -1.93401,-4.02457l0,-58.35627z"
fill="hsl(var(--color-primary))"
stroke="null"
/>
<rect
id="svg_3"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.64059"
y="15.46086"
/>
<rect
id="svg_4"
fill="#ffffff"
height="7.67897"
rx="2"
stroke="null"
width="8.18938"
x="0.58676"
y="1.42154"
/>
<rect
id="svg_8"
fill="currentColor"
fill-opacity="0.08"
height="9.07027"
rx="2"
stroke="null"
width="75.91967"
x="25.38277"
y="1.42876"
/>
<rect
id="svg_9"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="27.91529"
y="3.69284"
/>
<rect
id="svg_10"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="80.75054"
y="3.62876"
/>
<rect
id="svg_11"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="87.78868"
y="3.69981"
/>
<rect
id="svg_12"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="94.6847"
y="3.62876"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="21.51892"
rx="2"
stroke="null"
width="42.9287"
x="58.75427"
y="14.613"
/>
<rect
id="svg_14"
fill="currentColor"
fill-opacity="0.08"
height="20.97838"
rx="2"
stroke="null"
width="28.36894"
x="26.14342"
y="14.613"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="21.65405"
rx="2"
stroke="null"
width="75.09493"
x="26.34264"
y="39.68822"
/>
<rect
id="svg_5"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.79832"
y="28.39462"
/>
<rect
id="svg_6"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.64059"
y="41.80156"
/>
<rect
id="svg_7"
fill="#e5e5e5"
height="2.789"
rx="1.395"
stroke="null"
width="5.47439"
x="1.64059"
y="55.36623"
/>
<rect
id="svg_16"
fill="currentColor"
fill-opacity="0.08"
height="65.72065"
stroke="null"
width="12.49265"
x="9.85477"
y="-0.02618"
/>
</g>
</svg>
</template>

View File

@@ -0,0 +1,153 @@
<template>
<svg
class="custom-radio-image"
fill="none"
height="66"
width="104"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<rect
id="svg_1"
fill="currentColor"
fill-opacity="0.02"
height="66"
rx="4"
stroke="null"
width="104"
/>
<path
id="svg_2"
d="m-3.37838,3.61916a4.4919,4.02457 0 0 1 4.4919,-4.02457l26.35848,0l0,66.40541l-26.35848,0a4.4919,4.02457 0 0 1 -4.4919,-4.02457l0,-58.35627z"
fill="hsl(var(--color-primary))"
stroke="null"
/>
<rect
id="svg_3"
fill="#e5e5e5"
height="2.789"
rx="1.395"
width="17.66"
x="4.906"
y="23.884"
/>
<rect
id="svg_4"
fill="#ffffff"
height="9.706"
rx="2"
width="9.811"
x="8.83"
y="5.881"
/>
<path
id="svg_5"
d="m4.906,35.833c0,-0.75801 0.63699,-1.395 1.395,-1.395l14.87,0c0.75801,0 1.395,0.63699 1.395,1.395l0,-0.001c0,0.75801 -0.63699,1.395 -1.395,1.395l-14.87,0c-0.75801,0 -1.395,-0.63699 -1.395,-1.395l0,0.001z"
fill="#ffffff"
opacity="undefined"
/>
<rect
id="svg_6"
fill="#ffffff"
height="2.789"
rx="1.395"
width="17.66"
x="4.906"
y="44.992"
/>
<rect
id="svg_7"
fill="#ffffff"
height="2.789"
rx="1.395"
width="17.66"
x="4.906"
y="55.546"
/>
<rect
id="svg_8"
fill="currentColor"
fill-opacity="0.08"
height="9.07027"
rx="2"
stroke="null"
width="73.53879"
x="28.97986"
y="1.42876"
/>
<rect
id="svg_9"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="32.039"
y="3.89903"
/>
<rect
id="svg_10"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="80.75054"
y="3.62876"
/>
<rect
id="svg_11"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="87.58249"
y="3.49362"
/>
<rect
id="svg_12"
fill="#b2b2b2"
height="4.4"
rx="1"
stroke="null"
width="3.925"
x="94.6847"
y="3.62876"
/>
<rect
id="svg_13"
fill="currentColor"
fill-opacity="0.08"
height="21.51892"
rx="2"
stroke="null"
width="45.63141"
x="56.05157"
y="14.613"
/>
<rect
id="svg_14"
fill="currentColor"
fill-opacity="0.08"
height="20.97838"
rx="2"
stroke="null"
width="22.82978"
x="29.38527"
y="14.613"
/>
<rect
id="svg_15"
fill="currentColor"
fill-opacity="0.08"
height="21.65405"
rx="2"
stroke="null"
width="72.45771"
x="28.97986"
y="39.48203"
/>
</g>
</svg>
</template>

View File

@@ -0,0 +1 @@
export { default as PreferencesWidget } from './preferences-widget.vue';

View File

@@ -0,0 +1,152 @@
<script lang="ts" setup>
import { loadLocaleMessages } from '@vben/locales';
import {
COLOR_PRIMARY_RESETS,
preferences,
updatePreferences,
} from '@vben-core/preferences';
import Preferences from './preferences.vue';
</script>
<template>
<Preferences
:app-ai-assistant="preferences.app.aiAssistant"
:app-color-gray-mode="preferences.app.colorGrayMode"
:app-color-weak-mode="preferences.app.colorWeakMode"
:app-content-compact="preferences.app.contentCompact"
:app-dynamic-title="preferences.app.dynamicTitle"
:app-layout="preferences.app.layout"
:app-locale="preferences.app.locale"
:app-semi-dark-menu="preferences.app.semiDarkMenu"
:app-theme-mode="preferences.app.themeMode"
:breadcrumb-enable="preferences.breadcrumb.enable"
:breadcrumb-hide-only-one="preferences.breadcrumb.hideOnlyOne"
:breadcrumb-home="preferences.breadcrumb.showHome"
:breadcrumb-icon="preferences.breadcrumb.showIcon"
:breadcrumb-style-type="preferences.breadcrumb.styleType"
:color-primary-presets="COLOR_PRIMARY_RESETS"
:footer-enable="preferences.footer.enable"
:footer-fixed="preferences.footer.fixed"
:header-enable="preferences.header.enable"
:header-mode="preferences.header.mode"
:navigation-accordion="preferences.navigation.accordion"
:navigation-split="preferences.navigation.split"
:navigation-style-type="preferences.navigation.styleType"
:shortcut-keys-enable="preferences.shortcutKeys.enable"
:shortcut-keys-global-logout="preferences.shortcutKeys.globalLogout"
:shortcut-keys-global-preferences="
preferences.shortcutKeys.globalPreferences
"
:shortcut-keys-global-search="preferences.shortcutKeys.globalSearch"
:sidebar-collapsed="preferences.sidebar.collapsed"
:sidebar-collapsed-show-title="preferences.sidebar.collapsedShowTitle"
:sidebar-enable="preferences.sidebar.enable"
:tabbar-enable="preferences.tabbar.enable"
:tabbar-show-icon="preferences.tabbar.showIcon"
:theme-color-primary="preferences.theme.colorPrimary"
:transition-enable="preferences.transition.enable"
:transition-name="preferences.transition.name"
:transition-progress="preferences.transition.progress"
@update:app-ai-assistant="
(val) => updatePreferences({ app: { aiAssistant: val } })
"
@update:app-color-gray-mode="
(val) => updatePreferences({ app: { colorGrayMode: val } })
"
@update:app-color-weak-mode="
(val) => updatePreferences({ app: { colorWeakMode: val } })
"
@update:app-content-compact="
(val) => updatePreferences({ app: { contentCompact: val } })
"
@update:app-dynamic-title="
(val) => updatePreferences({ app: { dynamicTitle: val } })
"
@update:app-layout="(val) => updatePreferences({ app: { layout: val } })"
@update:app-locale="
(val) => {
updatePreferences({ app: { locale: val } });
loadLocaleMessages(val);
}
"
@update:app-semi-dark-menu="
(val) => updatePreferences({ app: { semiDarkMenu: val } })
"
@update:app-theme-mode="
(val) => updatePreferences({ app: { themeMode: val } })
"
@update:breadcrumb-enable="
(val) => updatePreferences({ breadcrumb: { enable: val } })
"
@update:breadcrumb-hide-only-one="
(val) => updatePreferences({ breadcrumb: { hideOnlyOne: val } })
"
@update:breadcrumb-show-home="
(val) => updatePreferences({ breadcrumb: { showHome: val } })
"
@update:breadcrumb-show-icon="
(val) => updatePreferences({ breadcrumb: { showIcon: val } })
"
@update:breadcrumb-style-type="
(val) => updatePreferences({ breadcrumb: { styleType: val } })
"
@update:footer-enable="
(val) => updatePreferences({ footer: { enable: val } })
"
@update:footer-fixed="
(val) => updatePreferences({ footer: { fixed: val } })
"
@update:header-enable="
(val) => updatePreferences({ header: { enable: val } })
"
@update:header-mode="(val) => updatePreferences({ header: { mode: val } })"
@update:navigation-accordion="
(val) => updatePreferences({ navigation: { accordion: val } })
"
@update:navigation-split="
(val) => updatePreferences({ navigation: { split: val } })
"
@update:navigation-style-type="
(val) => updatePreferences({ navigation: { styleType: val } })
"
@update:shortcut-keys-enable="
(val) => updatePreferences({ shortcutKeys: { enable: val } })
"
@update:shortcut-keys-global-logout="
(val) => updatePreferences({ shortcutKeys: { globalLogout: val } })
"
@update:shortcut-keys-global-preferences="
(val) => updatePreferences({ shortcutKeys: { globalPreferences: val } })
"
@update:shortcut-keys-global-search="
(val) => updatePreferences({ shortcutKeys: { globalSearch: val } })
"
@update:sidebar-collapsed="
(val) => updatePreferences({ sidebar: { collapsed: val } })
"
@update:sidebar-collapsed-show-title="
(val) => updatePreferences({ sidebar: { collapsedShowTitle: val } })
"
@update:sidebar-enable="
(val) => updatePreferences({ sidebar: { enable: val } })
"
@update:tabbar-enable="
(val) => updatePreferences({ tabbar: { enable: val } })
"
@update:tabbar-show-icon="
(val) => updatePreferences({ tabbar: { showIcon: val } })
"
@update:theme-color-primary="
(val) => updatePreferences({ theme: { colorPrimary: val } })
"
@update:transition-enable="
(val) => updatePreferences({ transition: { enable: val } })
"
@update:transition-name="
(val) => updatePreferences({ transition: { name: val } })
"
@update:transition-progress="
(val) => updatePreferences({ transition: { progress: val } })
"
/>
</template>

View File

@@ -0,0 +1,328 @@
<script setup lang="ts">
import type {
ContentCompactType,
LayoutHeaderModeType,
LayoutType,
SupportedLanguagesType,
ThemeModeType,
} from '@vben/types';
import type {
BreadcrumbStyleType,
NavigationStyleType,
} from '@vben-core/preferences';
import type { SegmentedItem } from '@vben-core/shadcn-ui';
import { computed } from 'vue';
import { $t } from '@vben/locales';
import { IcRoundFolderCopy, IcRoundRestartAlt } from '@vben-core/iconify';
import {
preferences,
resetPreferences,
usePreferences,
} from '@vben-core/preferences';
import {
VbenButton,
VbenIconButton,
VbenSegmented,
VbenSheet,
toast,
} from '@vben-core/shadcn-ui';
import { useClipboard } from '@vueuse/core';
import {
Animation,
Block,
Breadcrumb,
ColorMode,
Content,
Footer,
General,
GlobalShortcutKeys,
Header,
Layout,
Navigation,
Sidebar,
Tabbar,
Theme,
ThemeColor,
} from './blocks';
import Trigger from './trigger.vue';
import { useOpenPreferences } from './use-open-preferences';
withDefaults(defineProps<{ colorPrimaryPresets: string[] }>(), {
colorPrimaryPresets: () => [],
});
const appThemeMode = defineModel<ThemeModeType>('appThemeMode');
const appLocale = defineModel<SupportedLanguagesType>('appLocale');
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
const appAiAssistant = defineModel<boolean>('appAiAssistant');
const appLayout = defineModel<LayoutType>('appLayout');
const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
const appSemiDarkMenu = defineModel<boolean>('appSemiDarkMenu');
const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
const transitionProgress = defineModel<boolean>('transitionProgress');
const transitionName = defineModel<string>('transitionName');
const transitionEnable = defineModel<boolean>('transitionEnable');
const themeColorPrimary = defineModel<string>('themeColorPrimary');
const sidebarEnable = defineModel<boolean>('sidebarEnable');
const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
const sidebarCollapsedShowTitle = defineModel<boolean>(
'sidebarCollapsedShowTitle',
);
const headerEnable = defineModel<boolean>('headerEnable');
const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
const breadcrumbEnable = defineModel<boolean>('breadcrumbEnable');
const breadcrumbShowIcon = defineModel<boolean>('breadcrumbShowIcon');
const breadcrumbShowHome = defineModel<boolean>('breadcrumbShowHome');
const breadcrumbStyleType = defineModel<BreadcrumbStyleType>(
'breadcrumbStyleType',
);
const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
const tabbarEnable = defineModel<boolean>('tabbarEnable');
const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
const navigationStyleType = defineModel<NavigationStyleType>(
'navigationStyleType',
);
const navigationSplit = defineModel<boolean>('navigationSplit');
const navigationAccordion = defineModel<boolean>('navigationAccordion');
// const logoVisible = defineModel<boolean>('logoVisible');
const footerEnable = defineModel<boolean>('footerEnable');
const footerFixed = defineModel<boolean>('footerFixed');
const shortcutKeysEnable = defineModel<boolean>('shortcutKeysEnable');
const shortcutKeysGlobalSearch = defineModel<boolean>(
'shortcutKeysGlobalSearch',
);
const shortcutKeysGlobalLogout = defineModel<boolean>(
'shortcutKeysGlobalLogout',
);
const shortcutKeysGlobalPreferences = defineModel<boolean>(
'shortcutKeysGlobalPreferences',
);
const {
diffPreference,
isFullContent,
isHeaderNav,
isMixedNav,
isSideMixedNav,
isSideMode,
isSideNav,
} = usePreferences();
const { copy } = useClipboard();
const tabs = computed((): SegmentedItem[] => {
return [
{
label: $t('preferences.general'),
value: 'general',
},
{
label: $t('preferences.appearance'),
value: 'appearance',
},
{
label: $t('preferences.layout'),
value: 'layout',
},
{
label: $t('preferences.shortcut-keys.title'),
value: 'shortcutKey',
},
];
});
const showBreadcrumbConfig = computed(() => {
return (
!isFullContent.value &&
!isMixedNav.value &&
!isHeaderNav.value &&
preferences.header.enable
);
});
const { openPreferences } = useOpenPreferences();
async function handleCopy() {
await copy(JSON.stringify(diffPreference.value, null, 2));
toast($t('preferences.copy-success'));
}
function handleReset() {
if (!diffPreference.value) {
return;
}
resetPreferences();
toast($t('preferences.reset-success'));
}
</script>
<template>
<div class="z-100 fixed right-0 top-1/2">
<VbenSheet
v-model:open="openPreferences"
:description="$t('preferences.preferences-subtitle')"
:title="$t('preferences.preferences')"
>
<template #trigger>
<Trigger />
</template>
<template #extra>
<VbenIconButton
:disabled="!diffPreference"
:tooltip="$t('preferences.reset-tip')"
class="relative"
>
<span
v-if="diffPreference"
class="bg-primary absolute right-0.5 top-0.5 h-2 w-2 rounded"
></span>
<IcRoundRestartAlt class="size-5" @click="handleReset" />
</VbenIconButton>
</template>
<div class="p-4 pt-4">
<VbenSegmented :tabs="tabs" default-value="general">
<template #appearance>
<Block :title="$t('preferences.theme')">
<Theme
v-model="appThemeMode"
v-model:app-semi-dark-menu="appSemiDarkMenu"
/>
</Block>
<Block :title="$t('preferences.theme-color')">
<ThemeColor
v-model="themeColorPrimary"
:color-primary-presets="colorPrimaryPresets"
/>
</Block>
<Block :title="$t('preferences.other')">
<ColorMode
v-model:app-color-gray-mode="appColorGrayMode"
v-model:app-color-weak-mode="appColorWeakMode"
/>
</Block>
</template>
<template #layout>
<Block :title="$t('preferences.layout')">
<Layout v-model="appLayout" />
</Block>
<Block :title="$t('preferences.content')">
<Content v-model="appContentCompact" />
</Block>
<Block :title="$t('preferences.sidebar')">
<Sidebar
v-model:sidebar-collapsed="sidebarCollapsed"
v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle"
v-model:sidebar-enable="sidebarEnable"
:disabled="!isSideMode"
/>
</Block>
<Block :title="$t('preferences.header')">
<Header
v-model:headerEnable="headerEnable"
v-model:headerMode="headerMode"
:disabled="isFullContent"
/>
</Block>
<Block :title="$t('preferences.navigation-menu')">
<Navigation
v-model:navigation-accordion="navigationAccordion"
v-model:navigation-split="navigationSplit"
v-model:navigation-style-type="navigationStyleType"
:disabled="isFullContent"
:disabled-navigation-split="!isMixedNav"
/>
</Block>
<Block :title="$t('preferences.breadcrumb')">
<Breadcrumb
v-model:breadcrumb-enable="breadcrumbEnable"
v-model:breadcrumb-hide-only-one="breadcrumbHideOnlyOne"
v-model:breadcrumb-show-home="breadcrumbShowHome"
v-model:breadcrumb-show-icon="breadcrumbShowIcon"
v-model:breadcrumb-style-type="breadcrumbStyleType"
:disabled="
!showBreadcrumbConfig || !(isSideNav || isSideMixedNav)
"
/>
</Block>
<Block :title="$t('preferences.tabs')">
<Tabbar
v-model:tabbar-enable="tabbarEnable"
v-model:tabbar-show-icon="tabbarShowIcon"
/>
</Block>
<Block :title="$t('preferences.footer')">
<Footer
v-model:footer-enable="footerEnable"
v-model:footer-fixed="footerFixed"
/>
</Block>
</template>
<template #general>
<Block :title="$t('preferences.general')">
<General
v-model:app-ai-assistant="appAiAssistant"
v-model:app-dynamic-title="appDynamicTitle"
v-model:app-locale="appLocale"
/>
</Block>
<Block :title="$t('preferences.animation')">
<Animation
v-model:transition-enable="transitionEnable"
v-model:transition-name="transitionName"
v-model:transition-progress="transitionProgress"
/>
</Block>
</template>
<template #shortcutKey>
<Block :title="$t('preferences.shortcut-keys.global')">
<GlobalShortcutKeys
v-model:shortcut-keys-enable="shortcutKeysEnable"
v-model:shortcut-keys-global-search="shortcutKeysGlobalSearch"
v-model:shortcut-keys-logout="shortcutKeysGlobalLogout"
v-model:shortcut-keys-preferences="
shortcutKeysGlobalPreferences
"
/>
</Block>
</template>
</VbenSegmented>
</div>
<template #footer>
<VbenButton
:disabled="!diffPreference"
class="mx-6 w-full"
size="sm"
variant="default"
@click="handleCopy"
>
<IcRoundFolderCopy class="mr-2 size-3" />
{{ $t('preferences.copy') }}
</VbenButton>
</template>
</VbenSheet>
</div>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import { $t } from '@vben/locales';
import { VbenButton } from '@vben-core/shadcn-ui';
import IconSetting from './icons/setting.vue';
defineOptions({
name: 'PreferenceTrigger',
});
</script>
<template>
<VbenButton
:title="$t('preferences.preferences')"
class="bg-primary flex-col-center h-12 w-12 cursor-pointer rounded-l-lg rounded-r-none border-none"
>
<IconSetting class="duration-3000 animate-spin text-2xl" />
</VbenButton>
</template>

View File

@@ -0,0 +1,16 @@
import { ref } from 'vue';
const openPreferences = ref(false);
function useOpenPreferences() {
function handleOpenPreference() {
openPreferences.value = true;
}
return {
handleOpenPreference,
openPreferences,
};
}
export { useOpenPreferences };

View File

@@ -0,0 +1 @@
export { default as ThemeToggle } from './theme-toggle.vue';

View File

@@ -0,0 +1,196 @@
<script lang="ts" setup>
import { computed, nextTick } from 'vue';
import { VbenButton } from '@vben-core/shadcn-ui';
import { useNamespace } from '@vben-core/toolkit';
interface Props {
/**
* 类型
*/
type?: 'icon' | 'normal';
}
defineOptions({
name: 'ThemeToggleButton',
});
const props = withDefaults(defineProps<Props>(), {
type: 'normal',
});
const isDark = defineModel<boolean>();
const { b, e, is } = useNamespace('theme-toggle');
const theme = computed(() => {
return isDark.value ? 'light' : 'dark';
});
const bindProps = computed(() => {
const type = props.type;
return type === 'normal'
? {
variant: 'heavy' as const,
}
: {
class: 'rounded-full',
size: 'icon' as const,
style: { padding: '6px' },
variant: 'icon' as const,
};
});
function toggleTheme(event: MouseEvent) {
const isAppearanceTransition =
// @ts-expect-error
document.startViewTransition &&
!window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!isAppearanceTransition || !event) {
isDark.value = !isDark.value;
return;
}
const x = event.clientX;
const y = event.clientY;
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
);
// @ts-expect-error: Transition API
const transition = document.startViewTransition(async () => {
isDark.value = !isDark.value;
await nextTick();
});
transition.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
];
document.documentElement.animate(
{
clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
},
{
duration: 450,
easing: 'ease-in',
pseudoElement: isDark.value
? '::view-transition-old(root)'
: '::view-transition-new(root)',
},
);
});
}
</script>
<template>
<VbenButton
:aria-label="theme"
:class="[b(), is(theme)]"
aria-live="polite"
class="cursor-pointer border-none bg-none"
v-bind="bindProps"
@click.stop="toggleTheme"
>
<svg aria-hidden="true" height="24" viewBox="0 0 24 24" width="24">
<mask
id="theme-toggle-moon"
:class="e('moon')"
fill="hsl(var(--color-foreground)/80%)"
stroke="none"
>
<rect fill="white" height="100%" width="100%" x="0" y="0" />
<circle cx="40" cy="8" fill="black" r="11" />
</mask>
<circle
id="sun"
:class="e('sun')"
cx="12"
cy="12"
mask="url(#theme-toggle-moon)"
r="11"
/>
<g :class="e('sun-beams')">
<line x1="12" x2="12" y1="1" y2="3" />
<line x1="12" x2="12" y1="21" y2="23" />
<line x1="4.22" x2="5.64" y1="4.22" y2="5.64" />
<line x1="18.36" x2="19.78" y1="18.36" y2="19.78" />
<line x1="1" x2="3" y1="12" y2="12" />
<line x1="21" x2="23" y1="12" y2="12" />
<line x1="4.22" x2="5.64" y1="19.78" y2="18.36" />
<line x1="18.36" x2="19.78" y1="5.64" y2="4.22" />
</g>
</svg>
</VbenButton>
</template>
<style lang="scss" scoped>
@import '@vben-core/design/global';
@include b('theme-toggle') {
@include e('moon') {
& > circle {
transition: transform 0.5s cubic-bezier(0, 0, 0.3, 1);
}
}
@include e('sun') {
fill: hsl(var(--color-foreground) / 80%);
stroke: none;
transition: transform 1.6s cubic-bezier(0.25, 0, 0.2, 1);
transform-origin: center center;
&:hover > svg > & {
fill: hsl(var(--color-foreground));
}
}
@include e('sun-beams') {
stroke: hsl(var(--color-foreground) / 80%);
stroke-width: 2px;
transition:
transform 1.6s cubic-bezier(0.5, 1.5, 0.75, 1.25),
opacity 0.6s cubic-bezier(0.25, 0, 0.3, 1);
transform-origin: center center;
&:hover > svg > & {
stroke: hsl(var(--color-foreground));
}
}
@include is('light') {
@include b('theme-toggle') {
@include e('sun') {
transform: scale(0.5);
}
@include e('sun-beams') {
transform: rotateZ(0.25turn);
}
}
}
@include is('dark') {
@include b('theme-toggle') {
@include e('moon') {
& > circle {
transform: translateX(-20px);
}
}
@include e('sun-beams') {
opacity: 0;
}
}
}
&:hover > svg {
@include b('theme-toggle') {
&__moon,
&__sun {
fill: hsl(var(--color-foreground));
}
}
}
}
</style>

View File

@@ -0,0 +1,86 @@
<script lang="ts" setup>
import { $t } from '@vben/locales';
import {
IcRoundMotionPhotosAuto,
IcRoundWbSunny,
MdiMoonAndStars,
} from '@vben-core/iconify';
import {
type ThemeModeType,
preferences,
updatePreferences,
usePreferences,
} from '@vben-core/preferences';
import {
ToggleGroup,
ToggleGroupItem,
VbenTooltip,
} from '@vben-core/shadcn-ui';
import ThemeButton from './theme-button.vue';
defineOptions({
name: 'ThemeToggle',
});
withDefaults(defineProps<{ shouldOnHover?: boolean }>(), {
shouldOnHover: false,
});
function handleChange(isDark: boolean) {
updatePreferences({
app: { themeMode: isDark ? 'dark' : 'light' },
});
}
const { isDark } = usePreferences();
const PRESETS = [
{
icon: IcRoundWbSunny,
name: 'light',
title: $t('preferences.light'),
},
{
icon: MdiMoonAndStars,
name: 'dark',
title: $t('preferences.dark'),
},
{
icon: IcRoundMotionPhotosAuto,
name: 'auto',
title: $t('preferences.follow-system'),
},
];
</script>
<template>
<div>
<VbenTooltip :disabled="!shouldOnHover" side="bottom">
<template #trigger>
<ThemeButton
:model-value="isDark"
type="icon"
@update:model-value="handleChange"
/>
</template>
<ToggleGroup
:model-value="preferences.app.themeMode"
class="gap-2"
type="single"
variant="outline"
@update:model-value="
(val) =>
updatePreferences({ app: { themeMode: val as ThemeModeType } })
"
>
<ToggleGroupItem
v-for="item in PRESETS"
:key="item.name"
:value="item.name"
>
<component :is="item.icon" class="size-5" />
</ToggleGroupItem>
</ToggleGroup>
</VbenTooltip>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as UserDropdown } from './user-dropdown.vue';

View File

@@ -0,0 +1,196 @@
<script setup lang="ts">
import type { AnyFunction } from '@vben/types';
import type { Component } from 'vue';
import { computed, ref } from 'vue';
import { $t } from '@vben/locales';
import { IcRoundLogout, IcRoundSettingsSuggest } from '@vben-core/iconify';
import { preferences, usePreferences } from '@vben-core/preferences';
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
VbenAlertDialog,
VbenAvatar,
VbenIcon,
} from '@vben-core/shadcn-ui';
import { isWindowsOs } from '@vben-core/toolkit';
import { useMagicKeys, whenever } from '@vueuse/core';
import { useOpenPreferences } from '../preferences/use-open-preferences';
interface Props {
/**
* 头像
*/
avatar?: string;
/**
* @zh_CN 描述
*/
description?: string;
/**
* 是否启用快捷键
*/
enableShortcutKey?: boolean;
/**
* 菜单数组
*/
menus?: Array<{ handler: AnyFunction; icon?: Component; text: string }>;
/**
* 标签文本
*/
tagText?: string;
/**
* 文本
*/
text?: string;
}
defineOptions({
name: 'UserDropdown',
});
const props = withDefaults(defineProps<Props>(), {
avatar: '',
description: '',
enableShortcutKey: true,
menus: () => [],
showShortcutKey: true,
tagText: '',
text: '',
});
const emit = defineEmits<{ logout: [] }>();
const openPopover = ref(false);
const openDialog = ref(false);
const { globalLogoutShortcutKey, globalPreferencesShortcutKey } =
usePreferences();
const { handleOpenPreference } = useOpenPreferences();
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
const enableLogoutShortcutKey = computed(() => {
return props.enableShortcutKey && globalLogoutShortcutKey.value;
});
const enableShortcutKey = computed(() => {
return props.enableShortcutKey && preferences.shortcutKeys.enable;
});
const enablePreferencesShortcutKey = computed(() => {
return props.enableShortcutKey && globalPreferencesShortcutKey.value;
});
function handleLogout() {
// emit
openDialog.value = true;
openPopover.value = false;
}
function handleSubmitLogout() {
emit('logout');
openDialog.value = false;
}
if (enableShortcutKey.value) {
const keys = useMagicKeys();
whenever(keys['Alt+KeyQ'], () => {
if (enableLogoutShortcutKey.value) {
handleLogout();
}
});
whenever(keys['Alt+Comma'], () => {
if (enablePreferencesShortcutKey.value) {
handleOpenPreference();
}
});
}
</script>
<template>
<VbenAlertDialog
v-model:open="openDialog"
:cancel-text="$t('common.cancel')"
:content="$t('widgets.logout-tip')"
:submit-text="$t('common.confirm')"
:title="$t('common.prompt')"
@submit="handleSubmitLogout"
/>
<DropdownMenu>
<DropdownMenuTrigger>
<div class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full p-1.5">
<div class="hover:text-accent-foreground flex-center">
<VbenAvatar :alt="text" :src="avatar" class="size-8" dot />
<!-- <div v-if="text" class="ml-2 text-sm">{{ text }}</div> -->
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent class="mr-2 min-w-[240px] p-0 pb-1">
<DropdownMenuLabel class="flex items-center p-3">
<VbenAvatar
:alt="text"
:src="avatar"
class="size-12"
dot
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
/>
<div class="ml-2 w-full">
<div
class="text-foreground mb-1 flex items-center text-sm font-medium"
>
{{ text }}
<Badge class="ml-2 text-green-400">
{{ tagText }}
</Badge>
</div>
<div class="text-muted-foreground text-xs font-normal">
{{ description }}
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
v-for="menu in menus"
:key="menu.text"
class="lineh mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="menu.handler"
>
<VbenIcon :icon="menu.icon" class="mr-2 size-5" />
{{ menu.text }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleOpenPreference"
>
<IcRoundSettingsSuggest class="mr-2 size-5" />
{{ $t('preferences.preferences') }}
<DropdownMenuShortcut v-if="enablePreferencesShortcutKey">
{{ altView }} ,
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleLogout"
>
<IcRoundLogout class="mr-2 size-5" />
{{ $t('common.logout') }}
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
{{ altView }} Q
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>