chore: init project
This commit is contained in:
3
packages/business/README.md
Normal file
3
packages/business/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# business
|
||||
|
||||
与业务耦合的 UI 组件库 & SDK,同时也会耦合组件库及大仓依赖,如果你的包或者组件不知道放哪里,可以考虑放到这里。
|
||||
62
packages/business/common-ui/package.json
Normal file
62
packages/business/common-ui/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@vben/common-ui",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/business/common-ui"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vbenjs/vue-vben-admin/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/design": "workspace:*",
|
||||
"@vben-core/iconify": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/preference": "workspace:*",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@vueuse/integrations": "^10.9.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vben-core/typings": "workspace:*"
|
||||
}
|
||||
}
|
||||
1
packages/business/common-ui/postcss.config.mjs
Normal file
1
packages/business/common-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
||||
@@ -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>
|
||||
156
packages/business/common-ui/src/authentication/code-login.vue
Normal file
156
packages/business/common-ui/src/authentication/code-login.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
import { VbenButton, VbenInput, VbenPinInput } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import Title from './auth-title.vue';
|
||||
|
||||
import type { LoginCodeEmits } from './typings';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* @zh_CN 是否处于加载处理状态
|
||||
*/
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationCodeLogin',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
});
|
||||
|
||||
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 handleGo(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
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"
|
||||
:status="phoneNumberStatus"
|
||||
:error-tip="$t('authentication.mobile-tip')"
|
||||
:label="$t('authentication.mobile')"
|
||||
name="phoneNumber"
|
||||
type="number"
|
||||
:placeholder="$t('authentication.mobile')"
|
||||
:autofocus="true"
|
||||
@keyup.enter="handleSubmit"
|
||||
/>
|
||||
<VbenPinInput
|
||||
v-model="formState.code"
|
||||
:handle-send-code="handleSendCode"
|
||||
:status="codeStatus"
|
||||
:code-length="4"
|
||||
:error-tip="$t('authentication.code-tip')"
|
||||
:label="$t('authentication.code')"
|
||||
name="password"
|
||||
:placeholder="$t('authentication.code')"
|
||||
:btn-text="btnText"
|
||||
:btn-loading="btnLoading"
|
||||
@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="handleGo('/auth/login')"
|
||||
>
|
||||
{{ $t('common.back') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { IcRoundColorLens } from '@vben-core/iconify';
|
||||
import { VbenIconButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
import {
|
||||
preference,
|
||||
staticPreference,
|
||||
updatePreference,
|
||||
} from '@vben/preference';
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationColorToggle',
|
||||
});
|
||||
|
||||
function handleUpdate(value: string) {
|
||||
updatePreference({
|
||||
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 staticPreference.colorPrimaryPresets"
|
||||
:key="color"
|
||||
>
|
||||
<VbenIconButton
|
||||
class="flex-center flex-shrink-0"
|
||||
@click="handleUpdate(color)"
|
||||
>
|
||||
<div
|
||||
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"
|
||||
:class="[
|
||||
preference.colorPrimary === color ? `before:opacity-100` : '',
|
||||
]"
|
||||
:style="{ backgroundColor: color }"
|
||||
></div>
|
||||
</VbenIconButton>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<VbenIconButton>
|
||||
<IcRoundColorLens class="text-primary size-5" />
|
||||
</VbenIconButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { VbenButton, VbenInput } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import Title from './auth-title.vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* @zh_CN 是否处于加载处理状态
|
||||
*/
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationForgetPassword',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
});
|
||||
|
||||
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 handleGo(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
</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"
|
||||
:status="emailStatus"
|
||||
:error-tip="$t('authentication.email-tip')"
|
||||
:label="$t('authentication.email')"
|
||||
name="email"
|
||||
autofocus
|
||||
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="handleGo('/auth/login')"
|
||||
>
|
||||
{{ $t('common.back') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
8
packages/business/common-ui/src/authentication/index.ts
Normal file
8
packages/business/common-ui/src/authentication/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { default as AuthenticationCodeLogin } from './code-login.vue';
|
||||
export { default as AuthenticationColorToggle } from './color-toggle.vue';
|
||||
export { default as AuthenticationForgetPassword } from './forget-password.vue';
|
||||
export { default as AuthenticationLayoutToggle } from './layout-toggle.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';
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenDropdownMenuItem } from '@vben-core/shadcn-ui';
|
||||
import type { AuthPageLayout } from '@vben-core/typings';
|
||||
|
||||
import { MdiDockBottom, MdiDockLeft, MdiDockRight } from '@vben-core/iconify';
|
||||
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { preference, updatePreference, usePreference } from '@vben/preference';
|
||||
import { computed } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationLayoutToggle',
|
||||
// inheritAttrs: false,
|
||||
});
|
||||
|
||||
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'),
|
||||
},
|
||||
]);
|
||||
|
||||
function handleUpdate(value: string) {
|
||||
updatePreference({
|
||||
authPageLayout: value as AuthPageLayout,
|
||||
});
|
||||
}
|
||||
|
||||
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreference();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VbenDropdownRadioMenu
|
||||
:menus="menus"
|
||||
:model-value="preference.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>
|
||||
175
packages/business/common-ui/src/authentication/login.vue
Normal file
175
packages/business/common-ui/src/authentication/login.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
VbenButton,
|
||||
VbenCheckbox,
|
||||
VbenInput,
|
||||
VbenInputPassword,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import Title from './auth-title.vue';
|
||||
import ThirdPartyLogin from './third-party-login.vue';
|
||||
|
||||
import type { LoginEmits } from './typings';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* @zh_CN 是否处于加载处理状态
|
||||
*/
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationLogin',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
});
|
||||
|
||||
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>
|
||||
<Title>
|
||||
{{ $t('authentication.welcome-back') }} 👋🏻
|
||||
<template #desc>
|
||||
<span class="text-muted-foreground">
|
||||
{{ $t('authentication.login-subtitle') }}
|
||||
</span>
|
||||
</template>
|
||||
</Title>
|
||||
|
||||
<VbenInput
|
||||
v-model="formState.username"
|
||||
:status="usernameStatus"
|
||||
:error-tip="$t('authentication.username-tip')"
|
||||
:label="$t('authentication.username')"
|
||||
name="username"
|
||||
:placeholder="$t('authentication.username')"
|
||||
type="text"
|
||||
:autofocus="false"
|
||||
@keyup.enter="handleSubmit"
|
||||
/>
|
||||
<VbenInputPassword
|
||||
v-model="formState.password"
|
||||
:status="passwordStatus"
|
||||
:error-tip="$t('authentication.password-tip')"
|
||||
:label="$t('authentication.password')"
|
||||
name="password"
|
||||
:placeholder="$t('authentication.password')"
|
||||
required
|
||||
type="password"
|
||||
@keyup.enter="handleSubmit"
|
||||
/>
|
||||
|
||||
<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
|
||||
class="text-primary hover:text-primary/80 cursor-pointer text-sm font-normal"
|
||||
@click="handleGo('/auth/forget-password')"
|
||||
>
|
||||
{{ $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
|
||||
variant="outline"
|
||||
class="w-1/2"
|
||||
@click="handleGo('/auth/code-login')"
|
||||
>
|
||||
{{ $t('authentication.mobile-login') }}
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
variant="outline"
|
||||
class="ml-4 w-1/2"
|
||||
@click="handleGo('/auth/qrcode-login')"
|
||||
>
|
||||
{{ $t('authentication.qrcode-login') }}
|
||||
</VbenButton>
|
||||
<!-- <VbenButton
|
||||
:loading="loading"
|
||||
variant="outline"
|
||||
class="w-1/3"
|
||||
@click="handleGo('/auth/register')"
|
||||
>
|
||||
创建账号
|
||||
</VbenButton> -->
|
||||
</div>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<ThirdPartyLogin />
|
||||
|
||||
<div 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('/auth/register')"
|
||||
>
|
||||
{{ $t('authentication.create-account') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { VbenButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { useQRCode } from '@vueuse/integrations/useQRCode';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import Title from './auth-title.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationQrCodeLogin',
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const text = ref('https://vben.vvbin.cn');
|
||||
|
||||
const qrcode = useQRCode(text, {
|
||||
errorCorrectionLevel: 'H',
|
||||
margin: 4,
|
||||
});
|
||||
|
||||
function handleGo(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Title>
|
||||
{{ $t('authentication.welcome-back') }} 📱
|
||||
<template #desc>
|
||||
<span class="text-muted-foreground">
|
||||
{{ $t('authentication.qrcode-subtitle') }}
|
||||
</span>
|
||||
</template>
|
||||
</Title>
|
||||
|
||||
<div class="mt-6 flex flex-col items-center justify-center">
|
||||
<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="handleGo('/auth/login')"
|
||||
>
|
||||
{{ $t('common.back') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
163
packages/business/common-ui/src/authentication/register.vue
Normal file
163
packages/business/common-ui/src/authentication/register.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
VbenButton,
|
||||
VbenCheckbox,
|
||||
VbenInput,
|
||||
VbenInputPassword,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import Title from './auth-title.vue';
|
||||
|
||||
import type { RegisterEmits } from './typings';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* @zh_CN 是否处于加载处理状态
|
||||
*/
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'RegisterForm',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
});
|
||||
|
||||
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 handleGo(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Title>
|
||||
{{ $t('authentication.create-an-account') }} 🚀
|
||||
<template #desc> {{ $t('authentication.sign-up-subtitle') }} </template>
|
||||
</Title>
|
||||
<VbenInput
|
||||
v-model="formState.username"
|
||||
:status="usernameStatus"
|
||||
:error-tip="$t('authentication.username-tip')"
|
||||
:label="$t('authentication.username')"
|
||||
name="username"
|
||||
:placeholder="$t('authentication.username')"
|
||||
type="text"
|
||||
/>
|
||||
<!-- Use 8 or more characters with a mix of letters, numbers & symbols. -->
|
||||
<VbenInputPassword
|
||||
v-model="formState.password"
|
||||
:status="passwordStatus"
|
||||
:error-tip="$t('authentication.password-tip')"
|
||||
:label="$t('authentication.password')"
|
||||
name="password"
|
||||
:placeholder="$t('authentication.password')"
|
||||
required
|
||||
type="password"
|
||||
:password-strength="true"
|
||||
>
|
||||
<template #strengthText>
|
||||
{{ $t('authentication.password-strength') }}
|
||||
</template>
|
||||
</VbenInputPassword>
|
||||
|
||||
<VbenInputPassword
|
||||
v-model="formState.comfirmPassword"
|
||||
:status="comfirmPasswordStatus"
|
||||
:error-tip="$t('authentication.comfirm-password-tip')"
|
||||
:label="$t('authentication.comfirm-password')"
|
||||
name="comfirmPassword"
|
||||
:placeholder="$t('authentication.comfirm-password')"
|
||||
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="handleGo('/auth/login')"
|
||||
>
|
||||
{{ $t('authentication.go-login') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben-core/iconify';
|
||||
import { VbenIconButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
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>
|
||||
29
packages/business/common-ui/src/authentication/typings.ts
Normal file
29
packages/business/common-ui/src/authentication/typings.ts
Normal 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,
|
||||
};
|
||||
190
packages/business/common-ui/src/fallback/fallback-icon.vue
Normal file
190
packages/business/common-ui/src/fallback/fallback-icon.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<svg
|
||||
data-name="Layer 1"
|
||||
height="571.14799"
|
||||
viewBox="0 0 860.13137 571.14799"
|
||||
width="860.13137"
|
||||
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>
|
||||
79
packages/business/common-ui/src/fallback/fallback.vue
Normal file
79
packages/business/common-ui/src/fallback/fallback.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { IcRoundArrowBackIosNew } from '@vben-core/iconify';
|
||||
import { VbenButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import FeedbackIcon from './fallback-icon.vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* @zh_CN 首页路由地址
|
||||
* @default /
|
||||
*/
|
||||
homePath?: string;
|
||||
/**
|
||||
* @zh_CN 默认显示的图片
|
||||
* @default pageNotFoundSvg
|
||||
*/
|
||||
image?: string;
|
||||
|
||||
/**
|
||||
* @zh_CN 页面提示语
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'Fallback',
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
description: '',
|
||||
homePath: '/',
|
||||
image: '',
|
||||
title: '',
|
||||
});
|
||||
|
||||
const titleText = computed(() => {
|
||||
return props.title || $t('fallback.page-not-found');
|
||||
});
|
||||
|
||||
const descText = computed(() => {
|
||||
return props.description || $t('fallback.page-not-found-desc');
|
||||
});
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
// 返回首页
|
||||
function back() {
|
||||
push(props.homePath);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="-enter-x flex h-screen w-full flex-col items-center justify-center"
|
||||
>
|
||||
<img v-if="image" :src="image" class="md:1/3 w-1/2 lg:w-1/4" />
|
||||
<FeedbackIcon v-else class="md:1/3 h-1/3 w-1/2 lg:w-1/4" />
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<p class="text-foreground mt-12 text-3xl md:text-4xl lg:text-5xl">
|
||||
{{ titleText }}
|
||||
</p>
|
||||
<p class="text-muted-foreground my-8 md:text-lg lg:text-xl">
|
||||
{{ descText }}
|
||||
</p>
|
||||
<VbenButton size="lg" @click="back">
|
||||
<IcRoundArrowBackIosNew class="mr-2" />
|
||||
{{ $t('common.back-to-home') }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
1
packages/business/common-ui/src/fallback/index.ts
Normal file
1
packages/business/common-ui/src/fallback/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Fallback } from './fallback.vue';
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Toaster } from '@vben-core/shadcn-ui';
|
||||
|
||||
defineOptions({ name: 'GlobalProvider' });
|
||||
</script>
|
||||
<template>
|
||||
<Toaster />
|
||||
<slot></slot>
|
||||
</template>
|
||||
1
packages/business/common-ui/src/global-provider/index.ts
Normal file
1
packages/business/common-ui/src/global-provider/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as GlobalProvider } from './global-provider.vue';
|
||||
115
packages/business/common-ui/src/global-search/global-search.vue
Normal file
115
packages/business/common-ui/src/global-search/global-search.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
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 { $t } from '@vben/locales';
|
||||
import { useMagicKeys, useToggle, whenever } from '@vueuse/core';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import SearchPanel from './search-panel.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'GlobalSearch',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<{ menus: MenuRecordRaw[] }>(), {
|
||||
menus: () => [],
|
||||
});
|
||||
|
||||
const [open, toggleOpen] = useToggle();
|
||||
const keyword = ref('');
|
||||
|
||||
function handleClose() {
|
||||
open.value = false;
|
||||
keyword.value = '';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const keys = useMagicKeys();
|
||||
const cmd = isWindowsOs() ? keys['ctrl+k'] : keys['cmd+k'];
|
||||
whenever(cmd, () => {
|
||||
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
|
||||
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>
|
||||
</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 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>
|
||||
1
packages/business/common-ui/src/global-search/index.ts
Normal file
1
packages/business/common-ui/src/global-search/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as GlobalSearch } from './global-search.vue';
|
||||
278
packages/business/common-ui/src/global-search/search-panel.vue
Normal file
278
packages/business/common-ui/src/global-search/search-panel.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { IcRoundClose, IcRoundSearchOff } from '@vben-core/iconify';
|
||||
import { ScrollArea, VbenIcon } from '@vben-core/shadcn-ui';
|
||||
import { mapTree, traverseTreeValues } from '@vben-core/toolkit';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { onKeyStroke, useLocalStorage, useThrottleFn } from '@vueuse/core';
|
||||
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
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>
|
||||
<ScrollArea>
|
||||
<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"
|
||||
: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"
|
||||
:class="
|
||||
activeIndex === index
|
||||
? 'active bg-primary text-primary-foreground text-muted-foreground'
|
||||
: ''
|
||||
"
|
||||
@mouseenter="handleMouseenter"
|
||||
>
|
||||
<VbenIcon
|
||||
fallback
|
||||
:icon="item.icon"
|
||||
class="mr-2 size-5 flex-shrink-0"
|
||||
/>
|
||||
|
||||
<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>
|
||||
</ScrollArea>
|
||||
</template>
|
||||
10
packages/business/common-ui/src/index.ts
Normal file
10
packages/business/common-ui/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from './authentication';
|
||||
export * from './fallback';
|
||||
export * from './global-provider';
|
||||
export * from './global-search';
|
||||
export * from './language-toggle';
|
||||
export * from './notification';
|
||||
export * from './preference';
|
||||
export * from './spinner';
|
||||
export * from './theme-toggle';
|
||||
export * from './user-dropdown';
|
||||
1
packages/business/common-ui/src/language-toggle/index.ts
Normal file
1
packages/business/common-ui/src/language-toggle/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LanguageToggle } from './language-toggle.vue';
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { SupportLocale } from '@vben-core/typings';
|
||||
|
||||
import { IcBaselineLanguage } from '@vben-core/iconify';
|
||||
import { VbenDropdownRadioMenu, VbenIconButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { loadLocaleMessages } from '@vben/locales';
|
||||
import {
|
||||
preference,
|
||||
staticPreference,
|
||||
updatePreference,
|
||||
} from '@vben/preference';
|
||||
|
||||
defineOptions({
|
||||
name: 'LanguageToggle',
|
||||
});
|
||||
|
||||
const menus = staticPreference.supportLanguages;
|
||||
|
||||
async function handleUpdate(value: string) {
|
||||
const locale = value as SupportLocale;
|
||||
updatePreference({
|
||||
locale,
|
||||
});
|
||||
// 更改预览
|
||||
await loadLocaleMessages(locale);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VbenDropdownRadioMenu
|
||||
:menus="menus"
|
||||
:model-value="preference.locale"
|
||||
@update:model-value="handleUpdate"
|
||||
>
|
||||
<VbenIconButton>
|
||||
<IcBaselineLanguage class="size-5" />
|
||||
</VbenIconButton>
|
||||
</VbenDropdownRadioMenu>
|
||||
</div>
|
||||
</template>
|
||||
3
packages/business/common-ui/src/notification/index.ts
Normal file
3
packages/business/common-ui/src/notification/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type * from './interface';
|
||||
|
||||
export { default as Notification } from './notification.vue';
|
||||
@@ -0,0 +1,9 @@
|
||||
interface NotificationItem {
|
||||
avatar: string;
|
||||
date: string;
|
||||
isRead?: boolean;
|
||||
message: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type { NotificationItem };
|
||||
181
packages/business/common-ui/src/notification/notification.vue
Normal file
181
packages/business/common-ui/src/notification/notification.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
IcRoundMarkEmailRead,
|
||||
IcRoundNotificationsNone,
|
||||
} from '@vben-core/iconify';
|
||||
import {
|
||||
ScrollArea,
|
||||
VbenButton,
|
||||
VbenIconButton,
|
||||
VbenPopover,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
import type { NotificationItem } from './interface';
|
||||
|
||||
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 border-b p-4 py-3">
|
||||
<div class="text-foreground">{{ $t('widgets.notifications') }}</div>
|
||||
<VbenIconButton
|
||||
:tooltip="$t('widgets.make-all-notify')"
|
||||
@click="handleMakeAll"
|
||||
>
|
||||
<IcRoundMarkEmailRead />
|
||||
</VbenIconButton>
|
||||
</div>
|
||||
<ScrollArea 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 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>
|
||||
</ScrollArea>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex-center text-muted-foreground min-h-[150px] w-full">
|
||||
{{ $t('common.not-data') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="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>
|
||||
22
packages/business/common-ui/src/preference/blocks/block.vue
Normal file
22
packages/business/common-ui/src/preference/blocks/block.vue
Normal 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>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceAnimation',
|
||||
});
|
||||
const pageProgress = defineModel<boolean>('pageProgress', {
|
||||
// 默认值
|
||||
default: false,
|
||||
});
|
||||
|
||||
const pageTransition = defineModel<string>('pageTransition');
|
||||
const pageTransitionEnable = defineModel<boolean>('pageTransitionEnable');
|
||||
|
||||
const transitionPreset = ['fade', 'fade-slide', 'fade-up', 'fade-down'];
|
||||
|
||||
function handleClick(value: string) {
|
||||
pageTransition.value = value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchItem v-model="pageProgress">
|
||||
{{ $t('preference.page-progress') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="pageTransitionEnable">
|
||||
{{ $t('preference.page-transition') }}
|
||||
</SwitchItem>
|
||||
<div
|
||||
v-if="pageTransitionEnable"
|
||||
class="mb-2 mt-3 flex justify-between gap-3 px-2"
|
||||
>
|
||||
<div
|
||||
v-for="item in transitionPreset"
|
||||
:key="item"
|
||||
class="outline-box p-2"
|
||||
:class="{
|
||||
'outline-box-active': pageTransition === item,
|
||||
}"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<div class="bg-accent h-10 w-12 rounded-md" :class="`${item}-slow`"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectListItem } from '@vben-core/typings';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { staticPreference } from '@vben/preference';
|
||||
|
||||
import SelectItem from '../select-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceGeneralConfig',
|
||||
});
|
||||
|
||||
const locale = defineModel<string>('locale');
|
||||
|
||||
const localeItems: SelectListItem[] = staticPreference.supportLanguages.map(
|
||||
(item) => ({
|
||||
label: item.text,
|
||||
value: item.key,
|
||||
}),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItem v-model="locale" :items="localeItems">
|
||||
{{ $t('preference.language') }}
|
||||
</SelectItem>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectListItem } from '@vben-core/typings';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import ToggleItem from '../toggle-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceNavigationConfig',
|
||||
});
|
||||
|
||||
defineProps<{ disabled: boolean }>();
|
||||
|
||||
const navigationStyle = defineModel<string>('navigationStyle');
|
||||
|
||||
const stylesItems: SelectListItem[] = [
|
||||
{ label: $t('preference.normal'), value: 'normal' },
|
||||
{ label: $t('preference.rounded'), value: 'rounded' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ToggleItem v-model="navigationStyle" :items="stylesItems" disabled>
|
||||
{{ $t('preference.navigation-style') }}
|
||||
</ToggleItem>
|
||||
</template>
|
||||
15
packages/business/common-ui/src/preference/blocks/index.ts
Normal file
15
packages/business/common-ui/src/preference/blocks/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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 Navigation } from './general/navigation.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 Sidebar } from './layout/sidebar.vue';
|
||||
export { default as Tabs } from './layout/tabs.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';
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectListItem } from '@vben-core/typings';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
import ToggleItem from '../toggle-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceBreadcrumbConfig',
|
||||
});
|
||||
|
||||
defineProps<{ disabled: boolean }>();
|
||||
|
||||
const breadcrumbVisible = defineModel<boolean>('breadcrumbVisible');
|
||||
const breadcrumbIcon = defineModel<boolean>('breadcrumbIcon');
|
||||
const breadcrumbStyle = defineModel<string>('breadcrumbStyle');
|
||||
const breadcrumbHome = defineModel<boolean>('breadcrumbHome');
|
||||
const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
|
||||
|
||||
const typeItems: SelectListItem[] = [
|
||||
{ label: $t('preference.normal'), value: 'normal' },
|
||||
{ label: $t('preference.breadcrumb-background'), value: 'background' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchItem v-model="breadcrumbVisible" :disabled="disabled">
|
||||
{{ $t('preference.breadcrumb-enable') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="breadcrumbHideOnlyOne" :disabled="!breadcrumbVisible">
|
||||
{{ $t('preference.breadcrumb-hide-only-one') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="breadcrumbHome" :disabled="!breadcrumbVisible">
|
||||
{{ $t('preference.breadcrumb-home') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="breadcrumbIcon" :disabled="!breadcrumbVisible">
|
||||
{{ $t('preference.breadcrumb-icon') }}
|
||||
</SwitchItem>
|
||||
<ToggleItem
|
||||
v-model="breadcrumbStyle"
|
||||
:items="typeItems"
|
||||
:disabled="!breadcrumbVisible"
|
||||
>
|
||||
{{ $t('preference.breadcrumb-style') }}
|
||||
</ToggleItem>
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@vben/locales';
|
||||
import { type Component, computed } from 'vue';
|
||||
|
||||
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('preference.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>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceBreadcrumbConfig',
|
||||
});
|
||||
|
||||
const footerVisible = defineModel<boolean>('footerVisible');
|
||||
const footerFixed = defineModel<boolean>('footerFixed');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchItem v-model="footerVisible">
|
||||
{{ $t('preference.footer-visible') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="footerFixed" :disabled="!footerVisible">
|
||||
{{ $t('preference.footer-fixed') }}
|
||||
</SwitchItem>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import type { LayoutHeaderMode, SelectListItem } from '@vben-core/typings';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SelectItem from '../select-item.vue';
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceBreadcrumbConfig',
|
||||
});
|
||||
|
||||
defineProps<{ disabled: boolean }>();
|
||||
|
||||
const headerVisible = defineModel<boolean>('headerVisible');
|
||||
const headerMode = defineModel<LayoutHeaderMode>('headerMode');
|
||||
|
||||
const localeItems: SelectListItem[] = [
|
||||
{
|
||||
label: $t('preference.header-mode-static'),
|
||||
value: 'static',
|
||||
},
|
||||
{
|
||||
label: $t('preference.header-mode-fixed'),
|
||||
value: 'fixed',
|
||||
},
|
||||
{
|
||||
label: $t('preference.header-mode-auto'),
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
label: $t('preference.header-mode-auto-scroll'),
|
||||
value: 'auto-scroll',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchItem v-model="headerVisible" :disabled="disabled">
|
||||
{{ $t('preference.header-visible') }}
|
||||
</SwitchItem>
|
||||
<SelectItem
|
||||
v-model="headerMode"
|
||||
:items="localeItems"
|
||||
:disabled="!headerVisible"
|
||||
>
|
||||
{{ $t('preference.mode') }}
|
||||
</SelectItem>
|
||||
</template>
|
||||
@@ -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('preference.tabs-visible') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="logoVisible">
|
||||
{{ $t('preference.logo-visible') }}
|
||||
</SwitchItem>
|
||||
</template>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import type { LayoutType } from '@vben-core/typings';
|
||||
|
||||
import { MdiQuestionMarkCircleOutline } from '@vben-core/iconify';
|
||||
import { VbenTooltip } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { type Component, computed } from 'vue';
|
||||
|
||||
import {
|
||||
FullContent,
|
||||
HeaderNav,
|
||||
MixedNav,
|
||||
SideMixedNav,
|
||||
SideNav,
|
||||
} from '../../icons';
|
||||
|
||||
interface PresetItem {
|
||||
name: string;
|
||||
tip: string;
|
||||
type: LayoutType;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceLayout',
|
||||
});
|
||||
|
||||
const modelValue = defineModel<LayoutType>({ default: 'side-nav' });
|
||||
|
||||
const components: Record<LayoutType, Component> = {
|
||||
'full-content': FullContent,
|
||||
'header-nav': HeaderNav,
|
||||
'mixed-nav': MixedNav,
|
||||
'side-mixed-nav': SideMixedNav,
|
||||
'side-nav': SideNav,
|
||||
};
|
||||
|
||||
const PRESET = computed((): PresetItem[] => [
|
||||
{
|
||||
name: $t('preference.vertical'),
|
||||
tip: $t('preference.vertical-tip'),
|
||||
type: 'side-nav',
|
||||
},
|
||||
{
|
||||
name: $t('preference.two-column'),
|
||||
tip: $t('preference.two-column-tip'),
|
||||
type: 'side-mixed-nav',
|
||||
},
|
||||
{
|
||||
name: $t('preference.horizontal'),
|
||||
tip: $t('preference.vertical-tip'),
|
||||
type: 'header-nav',
|
||||
},
|
||||
{
|
||||
name: $t('preference.mixed-menu'),
|
||||
tip: $t('preference.mixed-menu-tip'),
|
||||
type: 'mixed-nav',
|
||||
},
|
||||
{
|
||||
name: $t('preference.full-content'),
|
||||
tip: $t('preference.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>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceBreadcrumbConfig',
|
||||
});
|
||||
|
||||
defineProps<{ disabled: boolean }>();
|
||||
|
||||
const sideVisible = defineModel<boolean>('sideVisible');
|
||||
const sideCollapseShowTitle = defineModel<boolean>('sideCollapseShowTitle');
|
||||
const sideCollapse = defineModel<boolean>('sideCollapse');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchItem v-model="sideVisible" :disabled="disabled">
|
||||
{{ $t('preference.side-visible') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="sideCollapse" :disabled="!sideVisible">
|
||||
{{ $t('preference.collapse') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="sideCollapseShowTitle" :disabled="!sideVisible">
|
||||
{{ $t('preference.collapse-show-title') }}
|
||||
</SwitchItem>
|
||||
</template>
|
||||
@@ -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 tabsVisible = defineModel<boolean>('tabsVisible');
|
||||
const tabsIcon = defineModel<boolean>('tabsIcon');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchItem v-model="tabsVisible" :disabled="disabled">
|
||||
{{ $t('preference.tabs-visible') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="tabsIcon" :disabled="!tabsVisible">
|
||||
{{ $t('preference.tabs-icon') }}
|
||||
</SwitchItem>
|
||||
<!-- <SwitchItem v-model="sideCollapseShowTitle" :disabled="!tabsVisible">
|
||||
{{ $t('preference.collapse-show-title') }}
|
||||
</SwitchItem> -->
|
||||
</template>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectListItem } from '@vben-core/typings';
|
||||
|
||||
import { MdiQuestionMarkCircleOutline } from '@vben-core/iconify';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
VbenTooltip,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
import { useSlots } from 'vue';
|
||||
|
||||
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="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"
|
||||
:class="{
|
||||
'hover:bg-accent': !slots.tip,
|
||||
'pointer-events-none opacity-50': disabled,
|
||||
}"
|
||||
>
|
||||
<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>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { MdiQuestionMarkCircleOutline } from '@vben-core/iconify';
|
||||
import { Switch, VbenTooltip } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { useSlots } from 'vue';
|
||||
|
||||
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="hover:bg-accent my-1 flex w-full items-center justify-between rounded-md px-2 py-2"
|
||||
:class="{
|
||||
'pointer-events-none opacity-50': disabled,
|
||||
}"
|
||||
@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>
|
||||
<Switch v-model:checked="checked" @click.stop />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceColorMode',
|
||||
});
|
||||
|
||||
const colorWeakMode = defineModel<boolean>('colorWeakMode', {
|
||||
default: false,
|
||||
});
|
||||
|
||||
const colorGrayMode = defineModel<boolean>('colorGrayMode', {
|
||||
default: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchItem v-model="colorWeakMode">
|
||||
{{ $t('preference.weak-mode') }}
|
||||
</SwitchItem>
|
||||
<SwitchItem v-model="colorGrayMode">
|
||||
{{ $t('preference.gray-mode') }}
|
||||
</SwitchItem>
|
||||
</template>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { MdiEditBoxOutline } from '@vben-core/iconify';
|
||||
import { TinyColor, convertToHsl } from '@vben-core/toolkit';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import { computed, ref, watch, watchEffect } from 'vue';
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
IcRoundMotionPhotosAuto,
|
||||
IcRoundWbSunny,
|
||||
MdiMoonAndStars,
|
||||
} from '@vben-core/iconify';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import SwitchItem from '../switch-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceTheme',
|
||||
});
|
||||
|
||||
const modelValue = defineModel<string>({ default: 'auto' });
|
||||
const semiDarkMenu = defineModel<boolean>('semiDarkMenu', {
|
||||
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('preference.light');
|
||||
}
|
||||
case 'dark': {
|
||||
return $t('preference.dark');
|
||||
}
|
||||
case 'auto': {
|
||||
return $t('preference.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="semiDarkMenu"
|
||||
:disabled="modelValue !== 'light'"
|
||||
class="mt-6"
|
||||
>
|
||||
{{ $t('preference.dark-menu') }}
|
||||
</SwitchItem>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectListItem } from '@vben-core/typings';
|
||||
|
||||
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
|
||||
disabled
|
||||
class="hover:bg-accent flex w-full items-center justify-between rounded-md px-2 py-2"
|
||||
:class="{
|
||||
'pointer-events-none opacity-50': disabled,
|
||||
}"
|
||||
>
|
||||
<span class="text-sm"><slot></slot></span>
|
||||
<ToggleGroup
|
||||
v-model="modelValue"
|
||||
type="single"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="gap-2"
|
||||
>
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
119
packages/business/common-ui/src/preference/icons/header-nav.vue
Normal file
119
packages/business/common-ui/src/preference/icons/header-nav.vue
Normal 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>
|
||||
10
packages/business/common-ui/src/preference/icons/index.ts
Normal file
10
packages/business/common-ui/src/preference/icons/index.ts
Normal 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 SideMixedNav } from './side-mixed-nav.vue';
|
||||
export { default as SideNav } from './side-nav.vue';
|
||||
|
||||
const ContentWide = HeaderNav;
|
||||
export { ContentWide, HeaderNav };
|
||||
161
packages/business/common-ui/src/preference/icons/mixed-nav.vue
Normal file
161
packages/business/common-ui/src/preference/icons/mixed-nav.vue
Normal 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>
|
||||
13
packages/business/common-ui/src/preference/icons/setting.vue
Normal file
13
packages/business/common-ui/src/preference/icons/setting.vue
Normal 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>
|
||||
@@ -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>
|
||||
153
packages/business/common-ui/src/preference/icons/side-nav.vue
Normal file
153
packages/business/common-ui/src/preference/icons/side-nav.vue
Normal 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>
|
||||
1
packages/business/common-ui/src/preference/index.ts
Normal file
1
packages/business/common-ui/src/preference/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as PreferenceWidget } from './preference-widget.vue';
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PreferenceKeys, SupportLocale } from '@vben-core/typings';
|
||||
|
||||
import { loadLocaleMessages } from '@vben/locales';
|
||||
import {
|
||||
preference,
|
||||
staticPreference,
|
||||
updatePreference,
|
||||
} from '@vben/preference';
|
||||
|
||||
import Preference from './preference.vue';
|
||||
|
||||
function handleUpdate(key: PreferenceKeys, value: boolean | string) {
|
||||
updatePreference({
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
|
||||
function updateLocale(value: string) {
|
||||
const locale = value as SupportLocale;
|
||||
updatePreference({
|
||||
locale,
|
||||
});
|
||||
// 更改预览
|
||||
loadLocaleMessages(locale);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Preference
|
||||
:color-primary-presets="staticPreference.colorPrimaryPresets"
|
||||
:breadcrumb-visible="preference.breadcrumbVisible"
|
||||
:breadcrumb-style="preference.breadcrumbStyle"
|
||||
:color-gray-mode="preference.colorGrayMode"
|
||||
:breadcrumb-icon="preference.breadcrumbIcon"
|
||||
:color-primary="preference.colorPrimary"
|
||||
:color-weak-mode="preference.colorWeakMode"
|
||||
:content-compact="preference.contentCompact"
|
||||
:breadcrumb-home="preference.breadcrumbHome"
|
||||
:side-collapse="preference.sideCollapse"
|
||||
:layout="preference.layout"
|
||||
:semi-dark-menu="preference.semiDarkMenu"
|
||||
:side-visible="preference.sideVisible"
|
||||
:footer-visible="preference.footerVisible"
|
||||
:tabs-visible="preference.tabsVisible"
|
||||
:header-visible="preference.headerVisible"
|
||||
:footer-fixed="preference.footerFixed"
|
||||
:header-mode="preference.headerMode"
|
||||
:theme="preference.theme"
|
||||
:breadcrumb-hide-only-one="preference.breadcrumbHideOnlyOne"
|
||||
:page-transition="preference.pageTransition"
|
||||
:page-progress="preference.pageProgress"
|
||||
:tabs-icon="preference.tabsIcon"
|
||||
:locale="preference.locale"
|
||||
:navigation-style="preference.navigationStyle"
|
||||
:side-collapse-show-title="preference.sideCollapseShowTitle"
|
||||
:page-transition-enable="preference.pageTransitionEnable"
|
||||
@update:navigation-style="(value) => handleUpdate('navigationStyle', value)"
|
||||
@update:tabs-icon="(value) => handleUpdate('tabsIcon', value)"
|
||||
@update:side-collapse="(value) => handleUpdate('sideCollapse', value)"
|
||||
@update:locale="updateLocale"
|
||||
@update:header-visible="(value) => handleUpdate('headerVisible', value)"
|
||||
@update:side-visible="(value) => handleUpdate('sideVisible', value)"
|
||||
@update:footer-visible="(value) => handleUpdate('footerVisible', value)"
|
||||
@update:tabs-visible="(value) => handleUpdate('tabsVisible', value)"
|
||||
@update:header-mode="(value) => handleUpdate('headerMode', value)"
|
||||
@update:footer-fixed="(value) => handleUpdate('footerFixed', value)"
|
||||
@update:breadcrumb-visible="
|
||||
(value) => handleUpdate('breadcrumbVisible', value)
|
||||
"
|
||||
@update:breadcrumb-hide-only-one="
|
||||
(value) => handleUpdate('breadcrumbHideOnlyOne', value)
|
||||
"
|
||||
@update:side-collapse-show-title="
|
||||
(value) => handleUpdate('sideCollapseShowTitle', value)
|
||||
"
|
||||
@update:breadcrumb-home="(value) => handleUpdate('breadcrumbHome', value)"
|
||||
@update:breadcrumb-icon="(value) => handleUpdate('breadcrumbIcon', value)"
|
||||
@update:breadcrumb-style="(value) => handleUpdate('breadcrumbStyle', value)"
|
||||
@update:page-transition-enable="
|
||||
(value) => handleUpdate('pageTransitionEnable', value)
|
||||
"
|
||||
@update:color-gray-mode="(value) => handleUpdate('colorGrayMode', value)"
|
||||
@update:page-transition="(value) => handleUpdate('pageTransition', value)"
|
||||
@update:page-progress="(value) => handleUpdate('pageProgress', value)"
|
||||
@update:color-primary="(value) => handleUpdate('colorPrimary', value)"
|
||||
@update:color-weak-mode="(value) => handleUpdate('colorWeakMode', value)"
|
||||
@update:content-compact="(value) => handleUpdate('contentCompact', value)"
|
||||
@update:layout="(value) => handleUpdate('layout', value)"
|
||||
@update:semi-dark-menu="(value) => handleUpdate('semiDarkMenu', value)"
|
||||
@update:theme="(value) => handleUpdate('theme', value)"
|
||||
/>
|
||||
</template>
|
||||
247
packages/business/common-ui/src/preference/preference.vue
Normal file
247
packages/business/common-ui/src/preference/preference.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<script setup lang="ts">
|
||||
import type { SegmentedItem } from '@vben-core/shadcn-ui';
|
||||
import type { LayoutHeaderMode, LayoutType } from '@vben-core/typings';
|
||||
|
||||
import { IcRoundFolderCopy, IcRoundRestartAlt } from '@vben-core/iconify';
|
||||
import {
|
||||
VbenButton,
|
||||
VbenIconButton,
|
||||
VbenSegmented,
|
||||
VbenSheet,
|
||||
toast,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { preference, resetPreference, usePreference } from '@vben/preference';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import {
|
||||
Animation,
|
||||
Block,
|
||||
Breadcrumb,
|
||||
ColorMode,
|
||||
Content,
|
||||
Footer,
|
||||
General,
|
||||
Header,
|
||||
Layout,
|
||||
Navigation,
|
||||
Sidebar,
|
||||
Tabs,
|
||||
Theme,
|
||||
ThemeColor,
|
||||
} from './blocks';
|
||||
import Trigger from './trigger.vue';
|
||||
|
||||
withDefaults(defineProps<{ colorPrimaryPresets: string[] }>(), {
|
||||
colorPrimaryPresets: () => [],
|
||||
});
|
||||
|
||||
const theme = defineModel<string>('theme');
|
||||
const locale = defineModel<string>('locale');
|
||||
const semiDarkMenu = defineModel<boolean>('semiDarkMenu');
|
||||
const breadcrumbVisible = defineModel<boolean>('breadcrumbVisible');
|
||||
const breadcrumbIcon = defineModel<boolean>('breadcrumbIcon');
|
||||
const breadcrumbHome = defineModel<boolean>('breadcrumbHome');
|
||||
const breadcrumbStyle = defineModel<string>('breadcrumbStyle');
|
||||
const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
|
||||
const sideCollapseShowTitle = defineModel<boolean>('sideCollapseShowTitle');
|
||||
const sideCollapse = defineModel<boolean>('sideCollapse');
|
||||
const colorWeakMode = defineModel<boolean>('colorWeakMode');
|
||||
const colorGrayMode = defineModel<boolean>('colorGrayMode');
|
||||
const colorPrimary = defineModel<string>('colorPrimary');
|
||||
const navigationStyle = defineModel<string>('navigationStyle');
|
||||
const pageProgress = defineModel<boolean>('pageProgress');
|
||||
const pageTransition = defineModel<string>('pageTransition');
|
||||
const pageTransitionEnable = defineModel<boolean>('pageTransitionEnable');
|
||||
const layout = defineModel<LayoutType>('layout');
|
||||
const contentCompact = defineModel<string>('contentCompact');
|
||||
const sideVisible = defineModel<boolean>('sideVisible');
|
||||
const tabsVisible = defineModel<boolean>('tabsVisible');
|
||||
const tabsIcon = defineModel<boolean>('tabsIcon');
|
||||
// const logoVisible = defineModel<boolean>('logoVisible');
|
||||
const headerVisible = defineModel<boolean>('headerVisible');
|
||||
const headerMode = defineModel<LayoutHeaderMode>('headerMode');
|
||||
const footerVisible = defineModel<boolean>('footerVisible');
|
||||
const footerFixed = defineModel<boolean>('footerFixed');
|
||||
|
||||
const { diffPreference, isFullContent, isHeaderNav, isMixedNav, isSideMode } =
|
||||
usePreference();
|
||||
const { copy } = useClipboard();
|
||||
|
||||
const tabs = computed((): SegmentedItem[] => {
|
||||
return [
|
||||
{
|
||||
label: $t('preference.appearance'),
|
||||
value: 'appearance',
|
||||
},
|
||||
{
|
||||
label: $t('preference.layout'),
|
||||
value: 'layout',
|
||||
},
|
||||
{
|
||||
label: $t('preference.general'),
|
||||
value: 'general',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const showBreadcrumbConfig = computed(() => {
|
||||
return (
|
||||
!isFullContent.value &&
|
||||
!isMixedNav.value &&
|
||||
!isHeaderNav.value &&
|
||||
preference.headerVisible
|
||||
);
|
||||
});
|
||||
|
||||
const openSheet = ref(false);
|
||||
|
||||
function handlerOpenSheet() {
|
||||
openSheet.value = true;
|
||||
}
|
||||
|
||||
async function handleCopy() {
|
||||
await copy(JSON.stringify(diffPreference.value, null, 2));
|
||||
|
||||
toast($t('preference.copy-success'));
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
if (!diffPreference.value) {
|
||||
return;
|
||||
}
|
||||
resetPreference();
|
||||
toast($t('preference.reset-success'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="z-100 fixed right-0 top-1/3">
|
||||
<VbenSheet
|
||||
:description="$t('preference.preferences-subtitle')"
|
||||
:title="$t('preference.preferences')"
|
||||
>
|
||||
<template #trigger>
|
||||
<Trigger @click="handlerOpenSheet" />
|
||||
</template>
|
||||
<template #extra>
|
||||
<VbenIconButton
|
||||
class="relative"
|
||||
:disabled="!diffPreference"
|
||||
:tooltip="$t('preference.reset-tip')"
|
||||
>
|
||||
<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-5 pt-4">
|
||||
<VbenSegmented :tabs="tabs" default-value="appearance">
|
||||
<template #appearance>
|
||||
<Block :title="$t('preference.theme')">
|
||||
<Theme v-model="theme" v-model:semi-dark-menu="semiDarkMenu" />
|
||||
</Block>
|
||||
<Block :title="$t('preference.theme-color')">
|
||||
<ThemeColor
|
||||
v-model="colorPrimary"
|
||||
:color-primary-presets="colorPrimaryPresets"
|
||||
/>
|
||||
</Block>
|
||||
<Block :title="$t('preference.other')">
|
||||
<ColorMode
|
||||
v-model:color-gray-mode="colorGrayMode"
|
||||
v-model:color-weak-mode="colorWeakMode"
|
||||
/>
|
||||
</Block>
|
||||
</template>
|
||||
<template #layout>
|
||||
<Block :title="$t('preference.layout')">
|
||||
<Layout v-model="layout" />
|
||||
</Block>
|
||||
<Block :title="$t('preference.content')">
|
||||
<Content v-model="contentCompact" />
|
||||
</Block>
|
||||
|
||||
<Block :title="$t('preference.sidebar')">
|
||||
<Sidebar
|
||||
v-model:side-visible="sideVisible"
|
||||
v-model:side-collapse="sideCollapse"
|
||||
v-model:side-collapse-show-title="sideCollapseShowTitle"
|
||||
:disabled="!isSideMode"
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block :title="$t('preference.header')">
|
||||
<Header
|
||||
v-model:header-visible="headerVisible"
|
||||
v-model:headerMode="headerMode"
|
||||
:disabled="isFullContent"
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block :title="$t('preference.breadcrumb')">
|
||||
<Breadcrumb
|
||||
v-model:breadcrumb-visible="breadcrumbVisible"
|
||||
v-model:breadcrumb-icon="breadcrumbIcon"
|
||||
v-model:breadcrumb-style="breadcrumbStyle"
|
||||
v-model:breadcrumb-home="breadcrumbHome"
|
||||
v-model:breadcrumb-hide-only-one="breadcrumbHideOnlyOne"
|
||||
:disabled="!showBreadcrumbConfig"
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block :title="$t('preference.tabs')">
|
||||
<Tabs
|
||||
v-model:tabs-visible="tabsVisible"
|
||||
v-model:tabs-icon="tabsIcon"
|
||||
/>
|
||||
</Block>
|
||||
<Block :title="$t('preference.footer')">
|
||||
<Footer
|
||||
v-model:footer-visible="footerVisible"
|
||||
v-model:footer-fixed="footerFixed"
|
||||
/>
|
||||
</Block>
|
||||
</template>
|
||||
<template #general>
|
||||
<Block :title="$t('preference.general')">
|
||||
<General v-model:locale="locale" />
|
||||
</Block>
|
||||
<Block :title="$t('preference.navigation-menu')">
|
||||
<Navigation
|
||||
v-model:navigation-style="navigationStyle"
|
||||
:disabled="isFullContent"
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block :title="$t('preference.animation')">
|
||||
<Animation
|
||||
v-model:page-progress="pageProgress"
|
||||
v-model:page-transition="pageTransition"
|
||||
v-model:page-transition-enable="pageTransitionEnable"
|
||||
/>
|
||||
</Block>
|
||||
</template>
|
||||
</VbenSegmented>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<VbenButton
|
||||
class="mx-6 w-full"
|
||||
variant="default"
|
||||
size="sm"
|
||||
:disabled="!diffPreference"
|
||||
@click="handleCopy"
|
||||
>
|
||||
<IcRoundFolderCopy class="mr-2 size-3" />
|
||||
{{ $t('preference.copy') }}
|
||||
</VbenButton>
|
||||
</template>
|
||||
</VbenSheet>
|
||||
</div>
|
||||
</template>
|
||||
20
packages/business/common-ui/src/preference/trigger.vue
Normal file
20
packages/business/common-ui/src/preference/trigger.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { VbenButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import IconSetting from './icons/setting.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PreferenceTrigger',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VbenButton
|
||||
class="bg-primary flex-center h-9 w-9 cursor-pointer flex-col rounded-l-md rounded-r-none border-none"
|
||||
:title="$t('preference.preferences')"
|
||||
>
|
||||
<IconSetting class="text-lg" />
|
||||
</VbenButton>
|
||||
</template>
|
||||
1
packages/business/common-ui/src/spinner/index.ts
Normal file
1
packages/business/common-ui/src/spinner/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Spinner } from './spinner.vue';
|
||||
97
packages/business/common-ui/src/spinner/spinner.vue
Normal file
97
packages/business/common-ui/src/spinner/spinner.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script lang="ts" setup>
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* @zh_CN loading状态开启
|
||||
*/
|
||||
spinning: boolean;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'Spinner',
|
||||
});
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const { b, e } = useNamespace('spinner');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[b(), !spinning ? 'hidden' : '']"
|
||||
class="flex-center bg-overlay absolute left-0 top-0 size-full backdrop-blur-sm"
|
||||
>
|
||||
<div :class="e('loader')"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@vben-core/design/global';
|
||||
|
||||
@include b('spinner') {
|
||||
@keyframes jump-ani {
|
||||
15% {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateY(9px) rotate(22.5deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
border-bottom-right-radius: 40px;
|
||||
transform: translateY(18px) scale(1, 0.9) rotate(45deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateY(9px) rotate(67.5deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0) rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shadow-ani {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@include e('loader') {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
width: 48px;
|
||||
height: 5px;
|
||||
content: '';
|
||||
background: hsl(var(--color-primary) / 50%);
|
||||
border-radius: 50%;
|
||||
animation: shadow-ani 0.5s linear infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: hsl(var(--color-primary));
|
||||
border-radius: 4px;
|
||||
animation: jump-ani 0.5s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
packages/business/common-ui/src/theme-toggle/index.ts
Normal file
1
packages/business/common-ui/src/theme-toggle/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ThemeToggle } from './theme-toggle.vue';
|
||||
196
packages/business/common-ui/src/theme-toggle/theme-button.vue
Normal file
196
packages/business/common-ui/src/theme-toggle/theme-button.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenButton } from '@vben-core/shadcn-ui';
|
||||
import { useNamespace } from '@vben-core/toolkit';
|
||||
|
||||
import { computed, nextTick } from 'vue';
|
||||
|
||||
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',
|
||||
}
|
||||
: {
|
||||
class: 'rounded-full',
|
||||
size: 'icon',
|
||||
style: { padding: '6px' },
|
||||
variant: 'icon',
|
||||
};
|
||||
});
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
IcRoundMotionPhotosAuto,
|
||||
IcRoundWbSunny,
|
||||
MdiMoonAndStars,
|
||||
} from '@vben-core/iconify';
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
VbenTooltip,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { preference, updatePreference, usePreference } from '@vben/preference';
|
||||
|
||||
import ThemeButton from './theme-button.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ThemeToggle',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<{ shouldOnHover?: boolean }>(), {
|
||||
shouldOnHover: false,
|
||||
});
|
||||
|
||||
function handleChange(isDark: boolean) {
|
||||
updatePreference({ theme: isDark ? 'dark' : 'light' });
|
||||
}
|
||||
|
||||
const { isDark } = usePreference();
|
||||
|
||||
const PRESETS = [
|
||||
{
|
||||
icon: IcRoundWbSunny,
|
||||
name: 'light',
|
||||
title: $t('preference.light'),
|
||||
},
|
||||
{
|
||||
icon: MdiMoonAndStars,
|
||||
name: 'dark',
|
||||
title: $t('preference.dark'),
|
||||
},
|
||||
{
|
||||
icon: IcRoundMotionPhotosAuto,
|
||||
name: 'auto',
|
||||
title: $t('preference.follow-system'),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VbenTooltip side="bottom" :disabled="!shouldOnHover">
|
||||
<template #trigger>
|
||||
<ThemeButton
|
||||
:model-value="isDark"
|
||||
type="icon"
|
||||
@update:model-value="handleChange"
|
||||
/>
|
||||
</template>
|
||||
<ToggleGroup
|
||||
:model-value="preference.theme"
|
||||
type="single"
|
||||
variant="outline"
|
||||
class="gap-2"
|
||||
@update:model-value="(value) => updatePreference('theme', value)"
|
||||
>
|
||||
<ToggleGroupItem
|
||||
v-for="item in PRESETS"
|
||||
:key="item.name"
|
||||
:value="item.name"
|
||||
>
|
||||
<component :is="item.icon" class="size-5" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</VbenTooltip>
|
||||
</div>
|
||||
</template>
|
||||
1
packages/business/common-ui/src/user-dropdown/index.ts
Normal file
1
packages/business/common-ui/src/user-dropdown/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as UserDropdown } from './user-dropdown.vue';
|
||||
139
packages/business/common-ui/src/user-dropdown/user-dropdown.vue
Normal file
139
packages/business/common-ui/src/user-dropdown/user-dropdown.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { IcRoundLogout } from '@vben-core/iconify';
|
||||
import {
|
||||
Badge,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
VbenAlertDialog,
|
||||
VbenAvatar,
|
||||
VbenIcon,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
import { AnyFunction } from '@vben-core/typings';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
import { ref } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
avatar?: string;
|
||||
/**
|
||||
* @zh_CN 描述
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* 菜单数组
|
||||
*/
|
||||
menus?: Array<{ handler: AnyFunction; icon?: Component; text: string }>;
|
||||
/**
|
||||
* 标签文本
|
||||
*/
|
||||
tagText?: string;
|
||||
|
||||
/**
|
||||
* 文本
|
||||
*/
|
||||
text?: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'UserDropdown',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
avatar: '',
|
||||
description: '',
|
||||
menus: () => [],
|
||||
tagText: '',
|
||||
text: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ logout: [] }>();
|
||||
const openPopover = ref(false);
|
||||
const openDialog = ref(false);
|
||||
|
||||
function handleLogout() {
|
||||
// emit
|
||||
openDialog.value = true;
|
||||
openPopover.value = false;
|
||||
}
|
||||
|
||||
function handleSubmitLogout() {
|
||||
emit('logout');
|
||||
openDialog.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VbenAlertDialog
|
||||
v-model:open="openDialog"
|
||||
:content="$t('widgets.logout-tip')"
|
||||
:title="$t('common.prompt')"
|
||||
:cancel-text="$t('common.cancel')"
|
||||
:submit-text="$t('common.confirm')"
|
||||
@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">
|
||||
<DropdownMenuLabel class="border-border flex items-center border-b 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="mx-1 rounded-sm py-2.5"
|
||||
@click="menu.handler"
|
||||
>
|
||||
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
|
||||
{{ menu.text }}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem class="w-full p-0">
|
||||
<div
|
||||
class="border-border flex-center hover:bg-accent hover:text-accent-foreground h-10 w-full cursor-pointer border-t"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<IcRoundLogout class="mr-2" />
|
||||
{{ $t('common.logout') }}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
1
packages/business/common-ui/tailwind.config.mjs
Normal file
1
packages/business/common-ui/tailwind.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config';
|
||||
5
packages/business/common-ui/tsconfig.json
Normal file
5
packages/business/common-ui/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/web.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
3
packages/business/common-ui/vite.config.mts
Normal file
3
packages/business/common-ui/vite.config.mts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from '@vben/vite-config';
|
||||
|
||||
export default defineConfig();
|
||||
62
packages/business/layouts/package.json
Normal file
62
packages/business/layouts/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@vben/layouts",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/business/layouts"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/vbenjs/vue-vben-admin/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/iconify": "workspace:*",
|
||||
"@vben-core/layout-ui": "workspace:*",
|
||||
"@vben-core/menu-ui": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/tabs-ui": "workspace:*",
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/preference": "workspace:*",
|
||||
"@vben/stores": "workspace:*",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vben-core/typings": "workspace:*"
|
||||
}
|
||||
}
|
||||
1
packages/business/layouts/postcss.config.mjs
Normal file
1
packages/business/layouts/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@vben/locales';
|
||||
import { preference, usePreference } from '@vben/preference';
|
||||
|
||||
import AuthenticationFromView from './from-view.vue';
|
||||
import SloganIcon from './icons/slogan.vue';
|
||||
import Toolbar from './toolbar.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'Authentication',
|
||||
});
|
||||
|
||||
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreference();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-body flex min-h-full flex-1 select-none overflow-x-hidden">
|
||||
<AuthenticationFromView
|
||||
v-if="authPanelLeft"
|
||||
class="-enter-x min-h-full w-2/5"
|
||||
transition-name="slide-left"
|
||||
/>
|
||||
|
||||
<div class="absolute left-0 top-0 z-10 flex flex-1">
|
||||
<div
|
||||
class="-enter-x text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6"
|
||||
:class="
|
||||
authPanelLeft || authPanelCenter
|
||||
? 'lg:text-foreground'
|
||||
: 'lg:text-white'
|
||||
"
|
||||
>
|
||||
<img
|
||||
:alt="preference.appName"
|
||||
:src="preference.logo"
|
||||
:width="42"
|
||||
class="mr-2"
|
||||
/>
|
||||
<p class="text-xl font-medium">
|
||||
{{ preference.appName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block">
|
||||
<div
|
||||
class="absolute inset-0 h-full w-full bg-gradient-to-r from-[var(--color-authentication-from)] to-[var(--color-authentication-to)]"
|
||||
>
|
||||
<div class="flex-center mr-20 flex h-full flex-col">
|
||||
<SloganIcon
|
||||
:alt="preference.appName"
|
||||
class="animate-float h-64 w-2/5"
|
||||
/>
|
||||
<div class="-enter-x text-1xl mt-6 font-sans text-white lg:text-2xl">
|
||||
{{ $t('authentication.layout-title') }}
|
||||
</div>
|
||||
<div class="-enter-x dark:text-muted-foreground mt-2 text-white/60">
|
||||
{{ $t('authentication.layout-desc') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="authPanelCenter"
|
||||
class="flex-center w-full dark:bg-[var(--color-authentication-to)]"
|
||||
>
|
||||
<AuthenticationFromView
|
||||
class="enter-y md:bg-background w-full rounded-3xl pb-20 shadow-2xl md:w-2/3 lg:w-1/2 xl:w-2/5"
|
||||
>
|
||||
<template #toolbar>
|
||||
<Toolbar class="bg-muted" />
|
||||
</template>
|
||||
</AuthenticationFromView>
|
||||
</div>
|
||||
<AuthenticationFromView
|
||||
v-if="authPanelRight"
|
||||
class="enter-x min-h-full w-2/5 flex-1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- background-image: radial-gradient(
|
||||
rgba(255, 255, 255, 0.1) 1px,
|
||||
transparent 1px
|
||||
); -->
|
||||
36
packages/business/layouts/src/authentication/from-view.vue
Normal file
36
packages/business/layouts/src/authentication/from-view.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { preference } from '@vben/preference';
|
||||
|
||||
import Toolbar from './toolbar.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationFormView',
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="relative flex flex-col items-center justify-center px-6 py-10 lg:flex-initial lg:px-8"
|
||||
>
|
||||
<slot name="toolbar">
|
||||
<Toolbar />
|
||||
</slot>
|
||||
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition name="slide-right" mode="out-in" appear>
|
||||
<KeepAlive :include="['Login']">
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.fullPath"
|
||||
class="mt-6 w-full sm:mx-auto md:max-w-md"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
|
||||
<div
|
||||
class="text-muted-foreground absolute bottom-3 flex text-center text-xs"
|
||||
>
|
||||
{{ preference.copyright }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
4568
packages/business/layouts/src/authentication/icons/slogan.vue
Normal file
4568
packages/business/layouts/src/authentication/icons/slogan.vue
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/business/layouts/src/authentication/index.ts
Normal file
1
packages/business/layouts/src/authentication/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as AuthPageLayout } from './authentication.vue';
|
||||
24
packages/business/layouts/src/authentication/toolbar.vue
Normal file
24
packages/business/layouts/src/authentication/toolbar.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AuthenticationColorToggle,
|
||||
AuthenticationLayoutToggle,
|
||||
LanguageToggle,
|
||||
ThemeToggle,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
defineOptions({
|
||||
name: 'AuthenticationToolbar',
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex-center bg-background absolute right-2 top-4 rounded-3xl px-3 py-1 dark:bg-[var(--color-authentication-to)]"
|
||||
>
|
||||
<div class="hidden md:flex">
|
||||
<AuthenticationColorToggle />
|
||||
<AuthenticationLayoutToggle />
|
||||
</div>
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</template>
|
||||
60
packages/business/layouts/src/basic/content/content.vue
Normal file
60
packages/business/layouts/src/basic/content/content.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
|
||||
import { preference, usePreference } from '@vben/preference';
|
||||
import { storeToRefs, useTabsStore } from '@vben/stores';
|
||||
|
||||
import { IFrameRouterView } from '../../iframe';
|
||||
|
||||
defineOptions({ name: 'LayoutContent' });
|
||||
|
||||
const { keepAlive } = usePreference();
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
const { getCacheTabs, getExcludeTabs, renderRouteView } =
|
||||
storeToRefs(tabsStore);
|
||||
|
||||
// 页面切换动画
|
||||
function getTransitionName(route: RouteLocationNormalizedLoaded) {
|
||||
// 如果偏好设置未设置,则不使用动画
|
||||
const { keepAlive, pageTransition, pageTransitionEnable, tabsVisible } =
|
||||
preference;
|
||||
if (!pageTransition || !pageTransitionEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 标签页未启用或者未开启缓存,则使用全局配置动画
|
||||
if (!tabsVisible || !keepAlive) {
|
||||
return pageTransition;
|
||||
}
|
||||
|
||||
// 如果页面已经加载过,则不使用动画
|
||||
if (route.meta.loaded) {
|
||||
return;
|
||||
}
|
||||
// 已经打开且已经加载过的页面不使用动画
|
||||
const inTabs = getCacheTabs.value.includes(route.name as string);
|
||||
return inTabs && route.meta.loaded ? undefined : pageTransition;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IFrameRouterView />
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<Transition :name="getTransitionName(route)" mode="out-in" appear>
|
||||
<KeepAlive
|
||||
v-if="keepAlive"
|
||||
:include="getCacheTabs"
|
||||
:exclude="getExcludeTabs"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="renderRouteView"
|
||||
:key="route.fullPath"
|
||||
class="h-[1000px]"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component :is="Component" v-else :key="route.fullPath" />
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</template>
|
||||
1
packages/business/layouts/src/basic/content/index.ts
Normal file
1
packages/business/layouts/src/basic/content/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LayoutContent } from './content.vue';
|
||||
11
packages/business/layouts/src/basic/footer/footer.vue
Normal file
11
packages/business/layouts/src/basic/footer/footer.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
defineOptions({
|
||||
name: 'LayoutFooter',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center text-muted-foreground relative h-full w-full text-xs">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
1
packages/business/layouts/src/basic/footer/index.ts
Normal file
1
packages/business/layouts/src/basic/footer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LayoutFooter } from './footer.vue';
|
||||
40
packages/business/layouts/src/basic/header/header.vue
Normal file
40
packages/business/layouts/src/basic/header/header.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenFullScreen } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { GlobalSearch, LanguageToggle, ThemeToggle } from '@vben/common-ui';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Logo 主题
|
||||
*/
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutHeader',
|
||||
});
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
theme: 'light',
|
||||
});
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center hidden lg:block">
|
||||
<slot name="breadcrumb"></slot>
|
||||
</div>
|
||||
<div class="flex h-full min-w-0 flex-1 items-center">
|
||||
<slot name="menu"></slot>
|
||||
</div>
|
||||
<div class="flex h-full min-w-0 flex-shrink-0 items-center">
|
||||
<GlobalSearch class="mr-4" :menus="accessStore.getAccessMenus" />
|
||||
<ThemeToggle class="mr-2" />
|
||||
<LanguageToggle class="mr-2" />
|
||||
<VbenFullScreen class="mr-2" />
|
||||
<slot name="notification"></slot>
|
||||
<slot name="user-dropdown"></slot>
|
||||
</div>
|
||||
</template>
|
||||
1
packages/business/layouts/src/basic/header/index.ts
Normal file
1
packages/business/layouts/src/basic/header/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as LayoutHeader } from './header.vue';
|
||||
1
packages/business/layouts/src/basic/index.ts
Normal file
1
packages/business/layouts/src/basic/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as BasicLayout } from './layout.vue';
|
||||
233
packages/business/layouts/src/basic/layout.vue
Normal file
233
packages/business/layouts/src/basic/layout.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenAdminLayout } from '@vben-core/layout-ui';
|
||||
import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { PreferenceWidget } from '@vben/common-ui';
|
||||
import { preference, updatePreference, usePreference } from '@vben/preference';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { LayoutContent } from './content';
|
||||
import { LayoutFooter } from './footer';
|
||||
import { LayoutHeader } from './header';
|
||||
import {
|
||||
LayoutExtraMenu,
|
||||
LayoutMenu,
|
||||
LayoutMixedMenu,
|
||||
useExtraMenu,
|
||||
useMixedMenu,
|
||||
} from './menu';
|
||||
import { LayoutTabs, LayoutTabsToolbar } from './tabs';
|
||||
import { Breadcrumb } from './widgets';
|
||||
|
||||
defineOptions({ name: 'BasicLayout' });
|
||||
|
||||
const { isDark, isHeaderNav, isMixedNav, isSideMixedNav, layout } =
|
||||
usePreference();
|
||||
|
||||
const headerMenuTheme = computed(() => {
|
||||
return isDark.value ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
const theme = computed(() => {
|
||||
const dark = isDark.value || preference.semiDarkMenu;
|
||||
return dark ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
const logoClass = computed(() => {
|
||||
return preference.sideCollapseShowTitle &&
|
||||
preference.sideCollapse &&
|
||||
!isMixedNav.value
|
||||
? 'mx-auto'
|
||||
: '';
|
||||
});
|
||||
|
||||
const isMenuRounded = computed(() => {
|
||||
return preference.navigationStyle === 'rounded';
|
||||
});
|
||||
|
||||
const logoCollapse = computed(() => {
|
||||
if (isHeaderNav.value || isMixedNav.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { isMobile, sideCollapse } = preference;
|
||||
|
||||
if (!sideCollapse && isMobile) {
|
||||
return false;
|
||||
}
|
||||
return sideCollapse || isSideMixedNav.value;
|
||||
});
|
||||
|
||||
const showHeaderNav = computed(() => {
|
||||
return isHeaderNav.value || isMixedNav.value;
|
||||
});
|
||||
|
||||
const {
|
||||
extraActiveMenu,
|
||||
extraMenus,
|
||||
extraVisible,
|
||||
handleDefaultSelect,
|
||||
handleMenuMouseEnter,
|
||||
handleMixedMenuSelect,
|
||||
handleSideMouseLeave,
|
||||
} = useExtraMenu();
|
||||
|
||||
const {
|
||||
handleMenuSelect,
|
||||
headerActive,
|
||||
headerMenus,
|
||||
sideActive,
|
||||
sideMenus,
|
||||
sideVisible,
|
||||
} = useMixedMenu();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VbenAdminLayout
|
||||
v-model:side-extra-visible="extraVisible"
|
||||
:side-collapse-show-title="preference.sideCollapseShowTitle"
|
||||
:side-collapse="preference.sideCollapse"
|
||||
:side-extra-collapse="preference.sideExtraCollapse"
|
||||
:content-compact="preference.contentCompact"
|
||||
:is-mobile="preference.isMobile"
|
||||
:layout="layout"
|
||||
:header-mode="preference.headerMode"
|
||||
:footer-fixed="preference.footerFixed"
|
||||
:side-semi-dark="preference.semiDarkMenu"
|
||||
:side-theme="theme"
|
||||
:side-visible="sideVisible"
|
||||
:footer-visible="preference.footerVisible"
|
||||
:header-visible="preference.headerVisible"
|
||||
:side-width="preference.sideWidth"
|
||||
:tabs-visible="preference.tabsVisible"
|
||||
:side-expand-on-hover="preference.sideExpandOnHover"
|
||||
@side-mouse-leave="handleSideMouseLeave"
|
||||
@update:side-collapse="
|
||||
(value: boolean) => updatePreference('sideCollapse', value)
|
||||
"
|
||||
@update:side-extra-collapse="
|
||||
(value: boolean) => updatePreference('sideExtraCollapse', value)
|
||||
"
|
||||
@update:side-visible="
|
||||
(value: boolean) => updatePreference('sideVisible', value)
|
||||
"
|
||||
@update:side-expand-on-hover="
|
||||
(value: boolean) => updatePreference('sideExpandOnHover', value)
|
||||
"
|
||||
>
|
||||
<template #preference>
|
||||
<PreferenceWidget />
|
||||
</template>
|
||||
|
||||
<template #back-top>
|
||||
<VbenBackTop />
|
||||
</template>
|
||||
|
||||
<!-- logo -->
|
||||
<template #logo>
|
||||
<VbenLogo
|
||||
:collapse="logoCollapse"
|
||||
:src="preference.logo"
|
||||
:text="preference.appName"
|
||||
:theme="showHeaderNav ? headerMenuTheme : theme"
|
||||
:alt="preference.appName"
|
||||
:class="logoClass"
|
||||
/>
|
||||
</template>
|
||||
<!-- 头部区域 -->
|
||||
<template #header>
|
||||
<LayoutHeader :theme="theme">
|
||||
<template
|
||||
v-if="!showHeaderNav && preference.breadcrumbVisible"
|
||||
#breadcrumb
|
||||
>
|
||||
<Breadcrumb
|
||||
:hide-when-only-one="preference.breadcrumbHideOnlyOne"
|
||||
:type="preference.breadcrumbStyle"
|
||||
:show-icon="preference.breadcrumbIcon"
|
||||
:show-home="preference.breadcrumbHome"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="showHeaderNav" #menu>
|
||||
<LayoutMenu
|
||||
class="w-full"
|
||||
:rounded="isMenuRounded"
|
||||
mode="horizontal"
|
||||
:theme="headerMenuTheme"
|
||||
:menus="headerMenus"
|
||||
:default-active="headerActive"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
<template #user-dropdown>
|
||||
<slot name="user-dropdown"></slot>
|
||||
</template>
|
||||
<template #notification>
|
||||
<slot name="notification"></slot>
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
</template>
|
||||
<!-- 侧边菜单区域 -->
|
||||
<template #menu>
|
||||
<LayoutMenu
|
||||
mode="vertical"
|
||||
:rounded="isMenuRounded"
|
||||
:collapse-show-title="preference.sideCollapseShowTitle"
|
||||
:collapse="preference.sideCollapse"
|
||||
:theme="theme"
|
||||
:menus="sideMenus"
|
||||
:default-active="sideActive"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
<template #mixed-menu>
|
||||
<LayoutMixedMenu
|
||||
:rounded="isMenuRounded"
|
||||
:collapse="!preference.sideCollapseShowTitle"
|
||||
:active-path="extraActiveMenu"
|
||||
:theme="theme"
|
||||
@select="handleMixedMenuSelect"
|
||||
@default-select="handleDefaultSelect"
|
||||
@enter="handleMenuMouseEnter"
|
||||
/>
|
||||
</template>
|
||||
<!-- 侧边额外区域 -->
|
||||
<template #side-extra>
|
||||
<LayoutExtraMenu
|
||||
:rounded="isMenuRounded"
|
||||
:menus="extraMenus"
|
||||
:collapse="preference.sideExtraCollapse"
|
||||
:theme="theme"
|
||||
/>
|
||||
</template>
|
||||
<template #side-extra-title>
|
||||
<VbenLogo
|
||||
v-if="preference.logoVisible"
|
||||
:text="preference.appName"
|
||||
:theme="theme"
|
||||
:alt="preference.appName"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #tabs>
|
||||
<LayoutTabs
|
||||
v-if="preference.tabsVisible"
|
||||
:show-icon="preference.tabsIcon"
|
||||
/>
|
||||
</template>
|
||||
<template #tabs-toolbar>
|
||||
<LayoutTabsToolbar v-if="preference.tabsVisible" />
|
||||
</template>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<template #content>
|
||||
<LayoutContent />
|
||||
</template>
|
||||
<!-- 页脚 -->
|
||||
<template v-if="preference.footerVisible" #footer>
|
||||
<LayoutFooter v-if="preference.copyright">
|
||||
{{ preference.copyright }}
|
||||
</LayoutFooter>
|
||||
</template>
|
||||
</VbenAdminLayout>
|
||||
</template>
|
||||
33
packages/business/layouts/src/basic/menu/extra-menu.vue
Normal file
33
packages/business/layouts/src/basic/menu/extra-menu.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { Menu, MenuProps } from '@vben-core/menu-ui';
|
||||
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
interface Props extends MenuProps {
|
||||
collspae?: boolean;
|
||||
menus: MenuRecordRaw[];
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function handleSelect(key: string) {
|
||||
router.push(key);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menu
|
||||
:rounded="rounded"
|
||||
:collapse="collapse"
|
||||
:default-active="route.path"
|
||||
:menus="menus"
|
||||
:theme="theme"
|
||||
mode="vertical"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
||||
37
packages/business/layouts/src/basic/menu/helper.ts
Normal file
37
packages/business/layouts/src/basic/menu/helper.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
function findMenuByPath(
|
||||
list: MenuRecordRaw[],
|
||||
path?: string,
|
||||
): MenuRecordRaw | null {
|
||||
for (const menu of list) {
|
||||
if (menu.path === path) {
|
||||
return menu;
|
||||
}
|
||||
if (menu?.children?.length) {
|
||||
const findMenu = findMenuByPath(menu.children, path);
|
||||
if (findMenu) {
|
||||
return findMenu;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找根菜单
|
||||
* @param menus
|
||||
* @param path
|
||||
*/
|
||||
function findRootMenuByPath(menus: MenuRecordRaw[], path?: string) {
|
||||
const findMenu = findMenuByPath(menus, path);
|
||||
const rootMenuPath = findMenu?.parents?.[0];
|
||||
const rootMenu = menus.find((item) => item.path === rootMenuPath);
|
||||
return {
|
||||
findMenu,
|
||||
rootMenu,
|
||||
rootMenuPath,
|
||||
};
|
||||
}
|
||||
|
||||
export { findMenuByPath, findRootMenuByPath };
|
||||
5
packages/business/layouts/src/basic/menu/index.ts
Normal file
5
packages/business/layouts/src/basic/menu/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as LayoutExtraMenu } from './extra-menu.vue';
|
||||
export { default as LayoutMenu } from './menu.vue';
|
||||
export { default as LayoutMixedMenu } from './mixed-menu.vue';
|
||||
export * from './use-extra-menu';
|
||||
export * from './use-mixed-menu';
|
||||
34
packages/business/layouts/src/basic/menu/menu.vue
Normal file
34
packages/business/layouts/src/basic/menu/menu.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { Menu, MenuProps } from '@vben-core/menu-ui';
|
||||
|
||||
interface Props extends MenuProps {
|
||||
menus?: MenuRecordRaw[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
menus: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [string, string?];
|
||||
}>();
|
||||
|
||||
function handleMenuSelect(key: string) {
|
||||
emit('select', key, props.mode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menu
|
||||
:rounded="rounded"
|
||||
:collapse-show-title="collapseShowTitle"
|
||||
:collapse="collapse"
|
||||
:default-active="defaultActive"
|
||||
:menus="menus"
|
||||
:theme="theme"
|
||||
:mode="mode"
|
||||
@select="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
53
packages/business/layouts/src/basic/menu/mixed-menu.vue
Normal file
53
packages/business/layouts/src/basic/menu/mixed-menu.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NormalMenuProps } from '@vben-core/menu-ui';
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { NormalMenu } from '@vben-core/menu-ui';
|
||||
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { computed, onBeforeMount } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { findMenuByPath } from './helper';
|
||||
|
||||
interface Props extends NormalMenuProps {}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
defaultSelect: [MenuRecordRaw, MenuRecordRaw?];
|
||||
enter: [MenuRecordRaw];
|
||||
select: [MenuRecordRaw];
|
||||
}>();
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const route = useRoute();
|
||||
|
||||
const menus = computed(() => accessStore.getAccessMenus);
|
||||
|
||||
function handleSelect(menu: MenuRecordRaw) {
|
||||
emit('select', menu);
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
const menu = findMenuByPath(menus.value, route.path);
|
||||
if (menu) {
|
||||
const rootMenu = menus.value.find(
|
||||
(item) => item.path === menu.parents?.[0],
|
||||
);
|
||||
emit('defaultSelect', menu, rootMenu);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NormalMenu
|
||||
:rounded="rounded"
|
||||
:collapse="collapse"
|
||||
:menus="menus"
|
||||
:active-path="activePath"
|
||||
:theme="theme"
|
||||
@select="handleSelect"
|
||||
@enter="(menu) => emit('enter', menu)"
|
||||
/>
|
||||
</template>
|
||||
90
packages/business/layouts/src/basic/menu/use-extra-menu.ts
Normal file
90
packages/business/layouts/src/basic/menu/use-extra-menu.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { preference } from '@vben/preference';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { findRootMenuByPath } from './helper';
|
||||
|
||||
function useExtraMenu() {
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const menus = computed(() => accessStore.getAccessMenus);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const extraMenus = ref<MenuRecordRaw[]>([]);
|
||||
const extraVisible = ref<boolean>(false);
|
||||
const extraActiveMenu = ref('');
|
||||
|
||||
/**
|
||||
* 选择混合菜单事件
|
||||
* @param menu
|
||||
*/
|
||||
const handleMixedMenuSelect = (menu: MenuRecordRaw) => {
|
||||
extraMenus.value = menu?.children ?? [];
|
||||
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
|
||||
const hasChildren = extraMenus.value.length > 0;
|
||||
|
||||
extraVisible.value = hasChildren;
|
||||
if (!hasChildren) {
|
||||
router.push(menu.path);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 选择默认菜单事件
|
||||
* @param menu
|
||||
* @param rootMenu
|
||||
*/
|
||||
const handleDefaultSelect = (
|
||||
menu: MenuRecordRaw,
|
||||
rootMenu?: MenuRecordRaw,
|
||||
) => {
|
||||
extraMenus.value = rootMenu?.children ?? [];
|
||||
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
|
||||
|
||||
if (preference.sideExpandOnHover) {
|
||||
extraVisible.value = extraMenus.value.length > 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 侧边菜单鼠标移出事件
|
||||
*/
|
||||
const handleSideMouseLeave = () => {
|
||||
if (preference.sideExpandOnHover) {
|
||||
return;
|
||||
}
|
||||
extraVisible.value = false;
|
||||
|
||||
const { findMenu, rootMenu, rootMenuPath } = findRootMenuByPath(
|
||||
menus.value,
|
||||
route.path,
|
||||
);
|
||||
extraActiveMenu.value = rootMenuPath ?? findMenu?.path ?? '';
|
||||
extraMenus.value = rootMenu?.children ?? [];
|
||||
};
|
||||
|
||||
const handleMenuMouseEnter = (menu: MenuRecordRaw) => {
|
||||
if (!preference.sideExpandOnHover) {
|
||||
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
|
||||
extraMenus.value = findMenu?.children ?? [];
|
||||
extraActiveMenu.value = menu.parents?.[0] ?? menu.path;
|
||||
extraVisible.value = extraMenus.value.length > 0;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
extraActiveMenu,
|
||||
extraMenus,
|
||||
extraVisible,
|
||||
handleDefaultSelect,
|
||||
handleMenuMouseEnter,
|
||||
handleMixedMenuSelect,
|
||||
handleSideMouseLeave,
|
||||
};
|
||||
}
|
||||
|
||||
export { useExtraMenu };
|
||||
118
packages/business/layouts/src/basic/menu/use-mixed-menu.ts
Normal file
118
packages/business/layouts/src/basic/menu/use-mixed-menu.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { preference, usePreference } from '@vben/preference';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { computed, onBeforeMount, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { findRootMenuByPath } from './helper';
|
||||
|
||||
function useMixedMenu() {
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const splitSideMenus = ref<MenuRecordRaw[]>([]);
|
||||
const rootMenuPath = ref<string>('');
|
||||
|
||||
const { isMixedNav } = usePreference();
|
||||
|
||||
const sideVisible = computed(() => {
|
||||
if (isMixedNav.value) {
|
||||
return preference.sideVisible && splitSideMenus.value.length > 0;
|
||||
}
|
||||
return preference.sideVisible;
|
||||
});
|
||||
const menus = computed(() => accessStore.getAccessMenus);
|
||||
|
||||
/**
|
||||
* 头部菜单
|
||||
*/
|
||||
const headerMenus = computed(() => {
|
||||
if (!isMixedNav.value) {
|
||||
return menus.value;
|
||||
}
|
||||
return menus.value.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 侧边菜单
|
||||
*/
|
||||
const sideMenus = computed(() => {
|
||||
if (!isMixedNav.value) {
|
||||
return menus.value;
|
||||
}
|
||||
|
||||
return splitSideMenus.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* 侧边菜单激活路径
|
||||
*/
|
||||
const sideActive = computed(() => {
|
||||
return route.path;
|
||||
});
|
||||
|
||||
/**
|
||||
* 头部菜单激活路径
|
||||
*/
|
||||
const headerActive = computed(() => {
|
||||
if (!isMixedNav.value) {
|
||||
return route.path;
|
||||
}
|
||||
return rootMenuPath.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* 菜单点击事件处理
|
||||
* @param key 菜单路径
|
||||
* @param mode 菜单模式
|
||||
*/
|
||||
const handleMenuSelect = (key: string, mode?: string) => {
|
||||
if (!isMixedNav.value || mode === 'vertical') {
|
||||
router.push(key);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootMenu = menus.value.find((item) => item.path === key);
|
||||
rootMenuPath.value = rootMenu?.path ?? '';
|
||||
splitSideMenus.value = rootMenu?.children ?? [];
|
||||
if (splitSideMenus.value.length === 0) {
|
||||
router.push(key);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算侧边菜单
|
||||
* @param path 路由路径
|
||||
*/
|
||||
function calcSideMenus(path: string = route.path) {
|
||||
let { rootMenu } = findRootMenuByPath(menus.value, path);
|
||||
if (!rootMenu) {
|
||||
rootMenu = menus.value.find((item) => item.path === path);
|
||||
}
|
||||
rootMenuPath.value = rootMenu?.path ?? '';
|
||||
splitSideMenus.value = rootMenu?.children ?? [];
|
||||
}
|
||||
|
||||
// 初始化计算侧边菜单
|
||||
onBeforeMount(() => {
|
||||
calcSideMenus();
|
||||
});
|
||||
|
||||
return {
|
||||
handleMenuSelect,
|
||||
headerActive,
|
||||
headerMenus,
|
||||
sideActive,
|
||||
sideMenus,
|
||||
sideVisible,
|
||||
};
|
||||
}
|
||||
|
||||
export { useMixedMenu };
|
||||
3
packages/business/layouts/src/basic/tabs/index.ts
Normal file
3
packages/business/layouts/src/basic/tabs/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as LayoutTabs } from './tabs.vue';
|
||||
export { default as LayoutTabsToolbar } from './tabs-toolbar.vue';
|
||||
export * from './use-tabs';
|
||||
35
packages/business/layouts/src/basic/tabs/tabs-toolbar.vue
Normal file
35
packages/business/layouts/src/basic/tabs/tabs-toolbar.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { TabsMore, TabsScreen } from '@vben-core/tabs-ui';
|
||||
|
||||
import { preference, updatePreference } from '@vben/preference';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { useTabs } from './use-tabs';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { createContextMenus } = useTabs();
|
||||
|
||||
const menus = computed(() => {
|
||||
return createContextMenus(route);
|
||||
});
|
||||
|
||||
function handleScreenChange(screen: boolean) {
|
||||
updatePreference({
|
||||
headerVisible: !screen,
|
||||
sideVisible: !screen,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-center h-full">
|
||||
<TabsMore :menus="menus" />
|
||||
<TabsScreen
|
||||
:screen="!preference.headerVisible && !preference.sideVisible"
|
||||
@change="handleScreenChange"
|
||||
@update:screen="handleScreenChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
32
packages/business/layouts/src/basic/tabs/tabs.vue
Normal file
32
packages/business/layouts/src/basic/tabs/tabs.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { TabsView } from '@vben-core/tabs-ui';
|
||||
|
||||
import { useTabs } from './use-tabs';
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutTabs',
|
||||
});
|
||||
|
||||
defineProps<{ showIcon?: boolean }>();
|
||||
|
||||
const {
|
||||
createContextMenus,
|
||||
currentActive,
|
||||
currentTabs,
|
||||
handleClick,
|
||||
handleClose,
|
||||
handleUnPushPin,
|
||||
} = useTabs();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsView
|
||||
:show-icon="showIcon"
|
||||
:tabs="currentTabs"
|
||||
:menus="createContextMenus"
|
||||
:active="currentActive"
|
||||
@update:active="handleClick"
|
||||
@close="handleClose"
|
||||
@un-push-pin="handleUnPushPin"
|
||||
/>
|
||||
</template>
|
||||
184
packages/business/layouts/src/basic/tabs/use-tabs.ts
Normal file
184
packages/business/layouts/src/basic/tabs/use-tabs.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import type { IContextMenuItem } from '@vben-core/tabs-ui';
|
||||
import type { TabItem } from '@vben-core/typings';
|
||||
|
||||
import {
|
||||
IcRoundClose,
|
||||
IcRoundMultipleStop,
|
||||
IcRoundRefresh,
|
||||
MdiArrowExpandHorizontal,
|
||||
MdiFormatHorizontalAlignLeft,
|
||||
MdiFormatHorizontalAlignRight,
|
||||
MdiPin,
|
||||
MdiPinOff,
|
||||
} from '@vben-core/iconify';
|
||||
import { filterTree } from '@vben-core/toolkit';
|
||||
|
||||
import { storeToRefs, useAccessStore, useTabsStore } from '@vben/stores';
|
||||
import { computed, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
function useTabs() {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const accessStore = useAccessStore();
|
||||
const tabsStore = useTabsStore();
|
||||
const { accessMenus } = storeToRefs(accessStore);
|
||||
|
||||
const currentActive = computed(() => {
|
||||
return route.path;
|
||||
});
|
||||
|
||||
const currentTabs = computed(() => {
|
||||
return tabsStore.getTabs;
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化固定标签页
|
||||
*/
|
||||
const initAffixTabs = () => {
|
||||
const affixTabs = filterTree(router.getRoutes(), (route) => {
|
||||
return !!route.meta?.affixTab;
|
||||
});
|
||||
tabsStore.setAffixTabs(affixTabs);
|
||||
};
|
||||
|
||||
// 点击tab,跳转路由
|
||||
const handleClick = (key: string) => {
|
||||
router.push(key);
|
||||
};
|
||||
|
||||
// 关闭tab
|
||||
const handleClose = async (key: string) => {
|
||||
await tabsStore.closeTabByKey(key, router);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => accessMenus.value,
|
||||
() => {
|
||||
initAffixTabs();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
tabsStore.addTab(route);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const createContextMenus = (tab: TabItem) => {
|
||||
const tabs = tabsStore.getTabs;
|
||||
const affixTabs = tabsStore.affixTabs;
|
||||
const index = tabs.findIndex((item) => item.path === tab.path);
|
||||
|
||||
const disabled = tabs.length <= 1;
|
||||
|
||||
const { meta } = tab;
|
||||
const affixTab = meta?.affixTab ?? false;
|
||||
const isCurrentTab = route.path === tab.path;
|
||||
|
||||
// 当前处于最左侧或者减去固定标签页的数量等于0
|
||||
const closeLeftDisabled =
|
||||
index === 0 || index - affixTabs.length <= 0 || !isCurrentTab;
|
||||
|
||||
const closeRightDisabled = !isCurrentTab || index === tabs.length - 1;
|
||||
|
||||
const closeOtherDisabled =
|
||||
disabled || !isCurrentTab || tabs.length - affixTabs.length <= 1;
|
||||
|
||||
const menus: IContextMenuItem[] = [
|
||||
{
|
||||
disabled: !isCurrentTab,
|
||||
handler: async () => {
|
||||
await tabsStore.refreshTab(router);
|
||||
},
|
||||
icon: IcRoundRefresh,
|
||||
key: 'reload',
|
||||
text: '重新加载',
|
||||
},
|
||||
{
|
||||
disabled: !!affixTab || disabled,
|
||||
handler: async () => {
|
||||
await tabsStore.closeTab(tab, router);
|
||||
},
|
||||
icon: IcRoundClose,
|
||||
key: 'close',
|
||||
text: '关闭标签页',
|
||||
},
|
||||
{
|
||||
handler: async () => {
|
||||
await (affixTab
|
||||
? tabsStore.unPushPinTab(tab)
|
||||
: tabsStore.pushPinTab(tab));
|
||||
},
|
||||
icon: affixTab ? MdiPinOff : MdiPin,
|
||||
key: 'affix',
|
||||
separator: true,
|
||||
text: affixTab ? '取消固定标签页' : '固定标签页',
|
||||
},
|
||||
{
|
||||
disabled: closeLeftDisabled,
|
||||
handler: async () => {
|
||||
await tabsStore.closeLeftTabs(tab);
|
||||
},
|
||||
icon: MdiFormatHorizontalAlignLeft,
|
||||
key: 'close-left',
|
||||
text: '关闭左侧标签页',
|
||||
},
|
||||
{
|
||||
disabled: closeRightDisabled,
|
||||
handler: async () => {
|
||||
await tabsStore.closeRightTabs(tab);
|
||||
},
|
||||
icon: MdiFormatHorizontalAlignRight,
|
||||
key: 'close-right',
|
||||
separator: true,
|
||||
text: '关闭右侧标签页',
|
||||
},
|
||||
{
|
||||
disabled: closeOtherDisabled,
|
||||
handler: async () => {
|
||||
await tabsStore.closeOtherTabs(tab);
|
||||
},
|
||||
icon: MdiArrowExpandHorizontal,
|
||||
key: 'close-other',
|
||||
text: '关闭其他标签页',
|
||||
},
|
||||
{
|
||||
disabled,
|
||||
handler: async () => {
|
||||
await tabsStore.closeAllTabs(router);
|
||||
},
|
||||
icon: IcRoundMultipleStop,
|
||||
key: 'close-all',
|
||||
text: '关闭全部标签页',
|
||||
},
|
||||
// {
|
||||
// icon: 'icon-[material-symbols--back-to-tab-sharp]',
|
||||
// key: 'close-all',
|
||||
// text: '新窗口打开',
|
||||
// },
|
||||
];
|
||||
return menus;
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消固定标签页
|
||||
*/
|
||||
const handleUnPushPin = async (tab: TabItem) => {
|
||||
await tabsStore.unPushPinTab(tab);
|
||||
};
|
||||
|
||||
return {
|
||||
createContextMenus,
|
||||
currentActive,
|
||||
currentTabs,
|
||||
handleClick,
|
||||
handleClose,
|
||||
handleUnPushPin,
|
||||
};
|
||||
}
|
||||
|
||||
export { useTabs };
|
||||
88
packages/business/layouts/src/basic/widgets/breadcrumb.vue
Normal file
88
packages/business/layouts/src/basic/widgets/breadcrumb.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IBreadcrumb } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { VbenBackgroundBreadcrumb, VbenBreadcrumb } from '@vben-core/shadcn-ui';
|
||||
import { BreadcrumbStyle } from '@vben-core/typings';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
interface Props {
|
||||
hideWhenOnlyOne?: boolean;
|
||||
showHome?: boolean;
|
||||
showIcon?: boolean;
|
||||
type?: BreadcrumbStyle;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showHome: true,
|
||||
showIcon: false,
|
||||
type: 'normal',
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const breadcrumbs = computed((): IBreadcrumb[] => {
|
||||
const matched = route.matched;
|
||||
|
||||
const resultBreadcrumb: IBreadcrumb[] = [];
|
||||
|
||||
for (const match of matched) {
|
||||
const {
|
||||
meta,
|
||||
path,
|
||||
// children = []
|
||||
} = match;
|
||||
const { hideChildrenInMenu, hideInBreadcrumb, icon, name, title } =
|
||||
meta || {};
|
||||
if (hideInBreadcrumb || hideChildrenInMenu || !path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
resultBreadcrumb.push({
|
||||
icon: icon as string,
|
||||
path: path || route.path,
|
||||
title: (title || name) as string,
|
||||
// items: children.map((child) => {
|
||||
// return {
|
||||
// icon: child?.meta?.icon as string,
|
||||
// path: child.path,
|
||||
// title: child?.meta?.title as string,
|
||||
// };
|
||||
// }),
|
||||
});
|
||||
}
|
||||
if (props.showHome) {
|
||||
resultBreadcrumb.unshift({
|
||||
icon: 'mdi:home-outline',
|
||||
isHome: true,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
if (props.hideWhenOnlyOne && resultBreadcrumb.length === 1) {
|
||||
return [];
|
||||
}
|
||||
return resultBreadcrumb;
|
||||
});
|
||||
|
||||
function handleSelect(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VbenBreadcrumb
|
||||
v-if="type === 'normal'"
|
||||
:breadcrumbs="breadcrumbs"
|
||||
class="ml-2"
|
||||
:show-icon="showIcon"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
<VbenBackgroundBreadcrumb
|
||||
v-if="type === 'background'"
|
||||
:breadcrumbs="breadcrumbs"
|
||||
class="ml-2"
|
||||
:show-icon="showIcon"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
||||
1
packages/business/layouts/src/basic/widgets/index.ts
Normal file
1
packages/business/layouts/src/basic/widgets/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Breadcrumb } from './breadcrumb.vue';
|
||||
84
packages/business/layouts/src/iframe/iframe-router-view.vue
Normal file
84
packages/business/layouts/src/iframe/iframe-router-view.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { Spinner } from '@vben/common-ui';
|
||||
import { preference } from '@vben/preference';
|
||||
import { useTabsStore } from '@vben/stores';
|
||||
import { computed, ref } from 'vue';
|
||||
import { type RouteLocationNormalized, useRoute } from 'vue-router';
|
||||
|
||||
defineOptions({ name: 'IFrameRouterView' });
|
||||
|
||||
const spinning = ref(true);
|
||||
const tabsStore = useTabsStore();
|
||||
const route = useRoute();
|
||||
|
||||
const iframeRoutes = computed(() => {
|
||||
if (!preference.tabsVisible) {
|
||||
return route.meta.iframeSrc ? [route] : [];
|
||||
}
|
||||
const tabs = tabsStore.getTabs.filter((tab) => !!tab.meta?.iframeSrc);
|
||||
return tabs;
|
||||
});
|
||||
|
||||
const tabNames = computed(() => {
|
||||
const names = new Set<string>();
|
||||
iframeRoutes.value.forEach((item) => {
|
||||
names.add(item.name as string);
|
||||
});
|
||||
return names;
|
||||
});
|
||||
|
||||
const showIframe = computed(() => iframeRoutes.value.length > 0);
|
||||
|
||||
function routeShow(tabItem: RouteLocationNormalized) {
|
||||
const { name } = tabItem;
|
||||
return name === route.name;
|
||||
}
|
||||
|
||||
function canRender(tabItem: RouteLocationNormalized) {
|
||||
const { meta, name } = tabItem;
|
||||
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tabsStore.renderRouteView) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preference.tabsVisible) {
|
||||
return routeShow(tabItem);
|
||||
}
|
||||
|
||||
// 跟随 keepAlive 状态,与其他tab页保持一致
|
||||
if (
|
||||
!meta?.keepAlive &&
|
||||
tabNames.value.has(name as string) &&
|
||||
name !== route.name
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return tabsStore.getTabs.findIndex((tab) => tab.name === name) !== -1;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
spinning.value = false;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<template v-if="showIframe">
|
||||
<template v-for="item in iframeRoutes" :key="item.fullPath">
|
||||
<div
|
||||
v-show="routeShow(item)"
|
||||
v-if="canRender(item)"
|
||||
class="relative size-full"
|
||||
>
|
||||
<Spinner :spinning="spinning" />
|
||||
<iframe
|
||||
:src="item.meta.iframeSrc as string"
|
||||
class="size-full"
|
||||
@load="hideLoading"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
6
packages/business/layouts/src/iframe/iframe-view.vue
Normal file
6
packages/business/layouts/src/iframe/iframe-view.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'IFrameView' });
|
||||
</script>
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
2
packages/business/layouts/src/iframe/index.ts
Normal file
2
packages/business/layouts/src/iframe/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as IFrameRouterView } from './iframe-router-view.vue';
|
||||
export { default as IFrameView } from './iframe-view.vue';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user