Merge branch 'main' into fix-downloader

This commit is contained in:
Jin Mao
2025-10-26 15:16:32 +08:00
committed by GitHub
146 changed files with 1856 additions and 667 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/design",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,5 +1,5 @@
.side-content {
animation-duration: 0.2s;
animation-duration: 0.3s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
@@ -37,7 +37,7 @@
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-10px);
transform: translateY(50px);
}
to {
@@ -49,7 +49,7 @@
@keyframes slide-left {
from {
opacity: 0;
transform: translateX(-10px);
transform: translateX(-50px);
}
to {
@@ -61,7 +61,7 @@
@keyframes slide-right {
from {
opacity: 0;
transform: translateX(-10px);
transform: translateX(50px);
}
to {
@@ -73,7 +73,7 @@
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(10px);
transform: translateY(-50px);
}
to {
@@ -85,3 +85,17 @@
.z-popup {
z-index: var(--popup-z-index);
}
@keyframes shrink {
0% {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/icons",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -32,6 +32,7 @@ export {
Grip,
GripVertical,
Menu as IconDefault,
Inbox,
Info,
InspectionPanel,
Languages,

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -30,7 +30,7 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
function openRouteInNewWindow(path: string) {
const { hash, origin } = location;
const fullPath = path.startsWith('/') ? path : `/${path}`;
const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
openWindow(url, { target: '_blank' });
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/composables",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -22,6 +22,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"enableCheckUpdates": true,
"enablePreferences": true,
"enableRefreshToken": false,
"enableStickyPreferencesNavigationBar": true,
"isMobile": false,
"layout": "sidebar-nav",
"locale": "zh-CN",
@@ -29,6 +30,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"name": "Vben Admin",
"preferencesButtonPosition": "auto",
"watermark": false,
"watermarkContent": "",
"zIndex": 200,
},
"breadcrumb": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/preferences",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,4 +1,4 @@
import type { Preferences } from './types';
import type { Preferences } from "./types";
const defaultPreferences: Preferences = {
app: {
@@ -22,6 +22,7 @@ const defaultPreferences: Preferences = {
enableCheckUpdates: true,
enablePreferences: true,
enableRefreshToken: false,
enableStickyPreferencesNavigationBar: true,
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',
@@ -29,7 +30,9 @@ const defaultPreferences: Preferences = {
name: 'Vben Admin',
preferencesButtonPosition: 'auto',
watermark: false,
watermarkContent: '',
zIndex: 200,
},
breadcrumb: {
enable: true,

View File

@@ -59,6 +59,10 @@ interface AppPreferences {
* @zh_CN 是否开启refreshToken
*/
enableRefreshToken: boolean;
/**
* @zh_CN 是否开启首选项导航栏吸顶效果
*/
enableStickyPreferencesNavigationBar: boolean;
/** 是否移动端 */
isMobile: boolean;
/** 布局方式 */
@@ -75,6 +79,10 @@ interface AppPreferences {
* @zh_CN 是否开启水印
*/
watermark: boolean;
/**
* @zh_CN 水印文案
*/
watermarkContent: string;
/** z-index */
zIndex: number;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/form-ui",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -82,11 +82,11 @@ const actionWrapperClass = computed(() => {
const cls = [
'flex',
'w-full',
'items-center',
'gap-3',
props.compact ? 'pb-2' : 'pb-4',
props.layout === 'vertical' ? 'self-end' : 'self-center',
props.layout === 'inline' ? '' : 'w-full',
props.actionWrapperClass,
];

View File

@@ -342,13 +342,12 @@ export class FormApi {
isObject(obj[key]) &&
!isDayjsObject(obj[key]) &&
!isDate(obj[key])
? fieldMergeFn(obj[key], value)
? fieldMergeFn(value, obj[key])
: value;
}
return true;
});
const filteredFields = fieldMergeFn(fields, form.values);
this.handleStringToArrayFields(filteredFields);
form.setValues(filteredFields, shouldValidate);
}
@@ -358,7 +357,6 @@ export class FormApi {
const form = await this.getForm();
await form.submitForm();
const rawValues = toRaw(await this.getValues());
this.handleArrayToStringFields(rawValues);
await this.state?.handleSubmit?.(rawValues);
return rawValues;
@@ -458,16 +456,31 @@ export class FormApi {
return this.form;
}
private handleArrayToStringFields = (originValues: Record<string, any>) => {
private handleMultiFields = (originValues: Record<string, any>) => {
const arrayToStringFields = this.state?.arrayToStringFields;
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
return;
}
const processFields = (fields: string[], separator: string = ',') => {
this.processFields(fields, separator, originValues, (value, sep) =>
Array.isArray(value) ? value.join(sep) : value,
);
this.processFields(fields, separator, originValues, (value, sep) => {
if (Array.isArray(value)) {
return value.join(sep);
} else if (typeof value === 'string') {
// 处理空字符串的情况
if (value === '') {
return [];
}
// 处理复杂分隔符的情况
const escapedSeparator = sep.replaceAll(
/[.*+?^${}()|[\]\\]/g,
String.raw`\$&`,
);
return value.split(new RegExp(escapedSeparator));
} else {
return value;
}
});
};
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
@@ -503,8 +516,7 @@ export class FormApi {
const values = { ...originValues };
const fieldMappingTime = this.state?.fieldMappingTime;
this.handleStringToArrayFields(values);
this.handleMultiFields(values);
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
return values;
}
@@ -550,65 +562,6 @@ export class FormApi {
return values;
};
private handleStringToArrayFields = (originValues: Record<string, any>) => {
const arrayToStringFields = this.state?.arrayToStringFields;
if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) {
return;
}
const processFields = (fields: string[], separator: string = ',') => {
this.processFields(fields, separator, originValues, (value, sep) => {
if (typeof value !== 'string') {
return value;
}
// 处理空字符串的情况
if (value === '') {
return [];
}
// 处理复杂分隔符的情况
const escapedSeparator = sep.replaceAll(
/[.*+?^${}()|[\]\\]/g,
String.raw`\$&`,
);
return value.split(new RegExp(escapedSeparator));
});
};
// 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2']
if (arrayToStringFields.every((item) => typeof item === 'string')) {
const lastItem =
arrayToStringFields[arrayToStringFields.length - 1] || '';
const fields =
lastItem.length === 1
? arrayToStringFields.slice(0, -1)
: arrayToStringFields;
const separator = lastItem.length === 1 ? lastItem : ',';
processFields(fields, separator);
return;
}
// 处理嵌套数组格式 [['field1'], ';']
arrayToStringFields.forEach((fieldConfig) => {
if (Array.isArray(fieldConfig)) {
const [fields, separator = ','] = fieldConfig;
if (Array.isArray(fields)) {
processFields(fields, separator);
} else if (typeof originValues[fields] === 'string') {
const value = originValues[fields];
if (value === '') {
originValues[fields] = [];
} else {
const escapedSeparator = separator.replaceAll(
/[.*+?^${}()|[\]\\]/g,
String.raw`\$&`,
);
originValues[fields] = value.split(new RegExp(escapedSeparator));
}
}
}
});
};
private processFields = (
fields: string[],
separator: string,

View File

@@ -41,6 +41,7 @@ const {
emptyStateValue,
fieldName,
formFieldProps,
hide,
label,
labelClass,
labelWidth,
@@ -59,7 +60,7 @@ const values = useFormValues();
const errors = useFieldError(fieldName);
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
const formApi = formRenderProps.form;
const compact = formRenderProps.compact;
const compact = computed(() => formRenderProps.compact);
const isInValid = computed(() => errors.value?.length > 0);
const FieldComponent = computed(() => {
@@ -95,7 +96,7 @@ const currentRules = computed(() => {
});
const visible = computed(() => {
return isIf.value && isShow.value;
return !hide && isIf.value && isShow.value;
});
const shouldRequired = computed(() => {
@@ -283,7 +284,7 @@ onUnmounted(() => {
<template>
<FormField
v-if="isIf"
v-if="!hide && isIf"
v-bind="fieldProps"
v-slot="slotProps"
:name="fieldName"

View File

@@ -42,11 +42,11 @@ const emits = defineEmits<{
}>();
const wrapperClass = computed(() => {
const cls = ['flex flex-col'];
if (props.layout === 'vertical') {
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4');
const cls = ['flex'];
if (props.layout === 'inline') {
cls.push('flex-wrap gap-x-2');
} else {
cls.push('gap-2');
cls.push(props.compact ? 'gap-x-2' : 'gap-x-4', 'flex-col grid');
}
return cn(...cls, props.wrapperClass);
});
@@ -170,7 +170,7 @@ const computedSchema = computed(
<template>
<component :is="formComponent" v-bind="formComponentProps">
<div ref="wrapperRef" :class="wrapperClass" class="grid">
<div ref="wrapperRef" :class="wrapperClass">
<template v-for="cSchema in computedSchema" :key="cSchema.fieldName">
<!-- <div v-if="$slots[cSchema.fieldName]" :class="cSchema.formItemClass">
<slot :definition="cSchema" :name="cSchema.fieldName"> </slot>

View File

@@ -8,7 +8,7 @@ import type { ClassType, MaybeComputedRef } from '@vben-core/typings';
import type { FormApi } from './form-api';
export type FormLayout = 'horizontal' | 'vertical';
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
export type BaseFormComponentType =
| 'DefaultButton'
@@ -255,6 +255,8 @@ export interface FormSchema<
fieldName: string;
/** 帮助信息 */
help?: CustomRenderType;
/** 是否隐藏表单项 */
hide?: boolean;
/** 表单项 */
label?: CustomRenderType;
// 自定义组件内部渲染
@@ -277,7 +279,8 @@ export interface FormRenderProps<
*/
arrayToStringFields?: ArrayToStringFields;
/**
* 是否展开在showCollapseButton=true下生效
* 是否折叠在showCollapseButton=true下生效
* true:折叠 false:展开
*/
collapsed?: boolean;
/**

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/layout-ui",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -10,7 +10,7 @@ import {
useLayoutFooterStyle,
useLayoutHeaderStyle,
} from '@vben-core/composables';
import { Menu } from '@vben-core/icons';
import { IconifyIcon } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
@@ -559,7 +559,8 @@ const idMainContent = ELEMENT_ID_MAIN_CONTENT;
class="my-0 mr-1 rounded-md"
@click="handleHeaderToggle"
>
<Menu class="size-4" />
<IconifyIcon v-if="showSidebar" icon="ep:fold" />
<IconifyIcon v-else icon="ep:expand" />
</VbenIconButton>
</template>
<slot name="header"></slot>

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/menu-ui",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -107,7 +107,6 @@ export class ModalApi {
this.store.setState((prev) => ({
...prev,
isOpen: false,
submitting: false,
}));
}
}
@@ -162,7 +161,11 @@ export class ModalApi {
}
open() {
this.store.setState((prev) => ({ ...prev, isOpen: true }));
this.store.setState((prev) => ({
...prev,
isOpen: true,
submitting: false,
}));
}
setData<T>(payload: T) {

View File

@@ -180,7 +180,7 @@ function escapeKeyDown(e: KeyboardEvent) {
}
}
function handerOpenAutoFocus(e: Event) {
function handleOpenAutoFocus(e: Event) {
if (!openAutoFocus.value) {
e?.preventDefault();
}
@@ -209,6 +209,12 @@ const getForceMount = computed(() => {
return !unref(destroyOnClose) && unref(firstOpened);
});
const handleOpened = () => {
requestAnimationFrame(() => {
props.modalApi?.onOpened();
});
};
function handleClosed() {
isClosed.value = true;
props.modalApi?.onClosed();
@@ -253,8 +259,8 @@ function handleClosed() {
@escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside"
@interact-outside="interactOutside"
@open-auto-focus="handerOpenAutoFocus"
@opened="() => modalApi?.onOpened()"
@open-auto-focus="handleOpenAutoFocus"
@opened="handleOpened"
@pointer-down-outside="pointerDownOutside"
>
<DialogHeader

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.5.8",
"version": "5.5.9",
"#main": "./dist/index.mjs",
"#module": "./dist/index.mjs",
"homepage": "https://github.com/vbenjs/vue-vben-admin",

View File

@@ -21,7 +21,10 @@ isFullscreen.value = !!(
);
</script>
<template>
<VbenIconButton @click="toggle">
<VbenIconButton
class="hover:animate-[shrink_0.3s_ease-in-out]"
@click="toggle"
>
<Minimize v-if="isFullscreen" class="text-foreground size-4" />
<Maximize v-else class="text-foreground size-4" />
</VbenIconButton>

View File

@@ -59,9 +59,9 @@ function handleComplete(e: string[]) {
async function handleSend(e: Event) {
try {
e?.preventDefault();
await handleSendCode();
countdown.value = maxTime;
startCountdown();
await handleSendCode();
} catch (error) {
console.error('Failed to send code:', error);
// Consider emitting an error event or showing a notification

View File

@@ -35,16 +35,24 @@ const tabsIndicatorStyle = computed(() => {
width: `${(100 / props.tabs.length).toFixed(0)}%`,
};
});
function activeClass(tab: string): string[] {
return tab === activeTab.value ? ['!font-bold', 'text-primary'] : [];
}
</script>
<template>
<Tabs v-model="activeTab" :default-value="getDefaultValue">
<TabsList :style="tabsStyle" class="bg-accent relative grid w-full">
<TabsList
:style="tabsStyle"
class="bg-accent !outline-heavy relative grid w-full !outline !outline-2"
>
<TabsIndicator :style="tabsIndicatorStyle" />
<template v-for="tab in tabs" :key="tab.value">
<TabsTrigger
:value="tab.value"
class="z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50"
:class="activeClass(tab.value)"
class="hover:text-primary z-20 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium disabled:pointer-events-none disabled:opacity-50"
>
{{ tab.label }}
</TabsTrigger>

View File

@@ -23,7 +23,7 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps"
:class="
cn(
'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-1 transition-[width,transform] duration-300',
'absolute bottom-0 left-0 z-10 h-full w-1/2 translate-x-[--radix-tabs-indicator-position] rounded-full px-0 py-1 pr-0.5 transition-[width,transform] duration-300',
props.class,
)
"

View File

@@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground border-border peer h-4 w-4 shrink-0 rounded-sm border transition focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
'focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground border-border hover:border-primary peer h-4 w-4 shrink-0 rounded-sm border transition focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"

View File

@@ -8,12 +8,7 @@ import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { X } from 'lucide-vue-next';
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from 'radix-vue';
import { DialogClose, DialogContent, useForwardPropsEmits } from 'radix-vue';
import DialogOverlay from './DialogOverlay.vue';
@@ -87,7 +82,7 @@ defineExpose({
</script>
<template>
<DialogPortal :to="appendTo">
<Teleport defer :to="appendTo">
<Transition name="fade">
<DialogOverlay
v-if="open && modal"
@@ -132,5 +127,5 @@ defineExpose({
<X class="h-4 w-4" />
</DialogClose>
</DialogContent>
</DialogPortal>
</Teleport>
</template>

View File

@@ -24,7 +24,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
v-model="modelValue"
:class="
cn(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
'border-input bg-background ring-offset-background placeholder:text-muted-foreground/50 focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"

View File

@@ -7,7 +7,7 @@ import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { DialogContent, DialogPortal, useForwardPropsEmits } from 'radix-vue';
import { DialogContent, useForwardPropsEmits } from 'radix-vue';
import { sheetVariants } from './sheet';
import SheetOverlay from './SheetOverlay.vue';
@@ -73,7 +73,7 @@ function onAnimationEnd(event: AnimationEvent) {
</script>
<template>
<DialogPortal :to="appendTo">
<Teleport defer :to="appendTo">
<Transition name="fade">
<SheetOverlay
v-if="open && modal"
@@ -103,5 +103,5 @@ function onAnimationEnd(event: AnimationEvent) {
<Cross2Icon class="h-5 w-" />
</DialogClose> -->
</DialogContent>
</DialogPortal>
</Teleport>
</template>

View File

@@ -21,7 +21,7 @@ const delegatedProps = computed(() => {
v-bind="delegatedProps"
:class="
cn(
'bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1',
'bg-muted text-muted-foreground inline-flex h-9 items-center justify-center rounded-md p-1',
props.class,
)
"

View File

@@ -1,2 +1,4 @@
export { default as VbenTree } from './tree.vue';
export type { TreeProps } from './types';
export { treePropsDefaults } from './types';
export type { FlattenedItem } from 'radix-vue';

View File

@@ -14,25 +14,9 @@ import { cn, get } from '@vben-core/shared/utils';
import { TreeItem, TreeRoot } from 'radix-vue';
import { Checkbox } from '../checkbox';
import { treePropsDefaults } from './types';
const props = withDefaults(defineProps<TreeProps>(), {
allowClear: false,
autoCheckParent: true,
bordered: false,
checkStrictly: false,
defaultExpandedKeys: () => [],
defaultExpandedLevel: 0,
disabled: false,
disabledField: 'disabled',
expanded: () => [],
iconField: 'icon',
labelField: 'label',
multiple: false,
showIcon: true,
transition: true,
valueField: 'value',
childrenField: 'children',
});
const props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());
const emits = defineEmits<{
expand: [value: FlattenedItem<Recordable<any>>];
@@ -41,7 +25,9 @@ const emits = defineEmits<{
interface InnerFlattenItem<T = Recordable<any>, P = number | string> {
hasChildren: boolean;
id: P;
level: number;
parentId: null | P;
parents: P[];
value: T;
}
@@ -50,24 +36,25 @@ function flatten<T = Recordable<any>, P = number | string>(
items: T[],
childrenField: string = 'children',
level = 0,
parentId: null | P = null,
parents: P[] = [],
): InnerFlattenItem<T, P>[] {
const result: InnerFlattenItem<T, P>[] = [];
items.forEach((item) => {
const children = get(item, childrenField) as Array<T>;
const val = {
const id = get(item, props.valueField) as P;
const val: InnerFlattenItem<T, P> = {
hasChildren: Array.isArray(children) && children.length > 0,
id,
level,
parentId,
parents: [...parents],
value: item,
};
result.push(val);
if (val.hasChildren)
result.push(
...flatten(children, childrenField, level + 1, [
...parents,
get(item, props.valueField),
]),
...flatten(children, childrenField, level + 1, id, [...parents, id]),
);
});
return result;
@@ -171,6 +158,24 @@ function collapseAll() {
expanded.value = [];
}
function checkAll() {
if (!props.multiple) return;
modelValue.value = [
...new Set(
flattenData.value
.filter((item) => !get(item.value, props.disabledField))
.map((item) => get(item.value, props.valueField)),
),
];
updateTreeValue();
}
function unCheckAll() {
if (!props.multiple) return;
modelValue.value = [];
updateTreeValue();
}
function isNodeDisabled(item: FlattenedItem<Recordable<any>>) {
return props.disabled || get(item.value, props.disabledField);
}
@@ -195,12 +200,51 @@ function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
get(i.value, props.valueField) === get(item.value, props.valueField)
);
})
?.parents?.forEach((p) => {
?.parents?.filter((item) => !get(item, props.disabledField))
?.forEach((p) => {
if (Array.isArray(modelValue.value) && !modelValue.value.includes(p)) {
modelValue.value.push(p);
}
});
}
if (
!props.checkStrictly &&
props.multiple &&
props.autoCheckParent &&
!isSelected
) {
flattenData.value
.find((i) => {
return (
get(i.value, props.valueField) === get(item.value, props.valueField)
);
})
?.parents?.filter((item) => !get(item, props.disabledField))
?.reverse()
.forEach((p) => {
const children = flattenData.value.filter((i) => {
return (
i.parents.length > 0 &&
i.parents.includes(p) &&
i.id !== item._id &&
i.parentId === p
);
});
if (Array.isArray(modelValue.value)) {
const hasSelectedChild = children.some((child) =>
(modelValue.value as unknown[]).includes(
get(child.value, props.valueField),
),
);
if (!hasSelectedChild) {
const index = modelValue.value.indexOf(p);
if (index !== -1) {
modelValue.value.splice(index, 1);
}
}
}
});
}
updateTreeValue();
emits('select', item);
}
@@ -210,6 +254,8 @@ defineExpose({
collapseNodes,
expandAll,
expandNodes,
checkAll,
unCheckAll,
expandToLevel,
getItemByValue,
});
@@ -230,15 +276,41 @@ defineExpose({
v-slot="{ flattenItems }"
:class="
cn(
'text-blackA11 container select-none list-none rounded-lg p-2 text-sm font-medium',
'text-blackA11 container select-none list-none rounded-lg text-sm font-medium',
$attrs.class as unknown as ClassType,
bordered ? 'border' : '',
)
"
>
<div class="w-full" v-if="$slots.header">
<div
:class="
cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-b' : '')
"
v-if="$slots.header"
>
<slot name="header"> </slot>
</div>
<div
:class="
cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-b' : '')
"
v-if="treeData.length > 0"
>
<div
class="flex size-5 flex-1 cursor-pointer items-center"
@click="() => (expanded?.length > 0 ? collapseAll() : expandAll())"
>
<ChevronRight
:class="{ 'rotate-90': expanded?.length > 0 }"
class="text-foreground/80 hover:text-foreground size-4 cursor-pointer transition"
/>
<Checkbox
v-if="multiple"
@click.stop
@update:checked="(checked) => (checked ? checkAll() : unCheckAll())"
/>
</div>
</div>
<TransitionGroup :name="transition ? 'fade' : ''">
<TreeItem
v-for="item in flattenItems"
@@ -250,11 +322,11 @@ defineExpose({
handleToggle,
}"
:key="item._id"
:style="{ 'padding-left': `${item.level - 0.5}rem` }"
:style="{ 'margin-left': `${item.level - 1}rem` }"
:class="
cn('cursor-pointer', getNodeClass?.(item), {
'data-[selected]:bg-accent': !multiple,
'cursor-not-allowed': isNodeDisabled(item),
'text-foreground/50 cursor-not-allowed': isNodeDisabled(item),
})
"
v-bind="
@@ -284,7 +356,7 @@ defineExpose({
!isNodeDisabled(item) && onToggle(item);
}
"
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded px-2 py-1 outline-none focus:ring-2"
class="tree-node focus:ring-grass8 my-0.5 flex items-center rounded p-1 outline-none focus:ring-2"
>
<ChevronRight
v-if="
@@ -292,7 +364,7 @@ defineExpose({
Array.isArray(item.value[childrenField]) &&
item.value[childrenField].length > 0
"
class="size-4 cursor-pointer transition"
class="text-foreground/80 hover:text-foreground size-4 cursor-pointer transition"
:class="{ 'rotate-90': isExpanded }"
@click.stop="
() => {
@@ -301,52 +373,56 @@ defineExpose({
}
"
/>
<div v-else class="h-4 w-4">
<!-- <IconifyIcon v-if="item.value.icon" :icon="item.value.icon" /> -->
</div>
<Checkbox
v-if="multiple"
:checked="isSelected && !isNodeDisabled(item)"
:disabled="isNodeDisabled(item)"
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
@click="
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
<div v-else class="h-4 w-4"></div>
<div class="flex items-center gap-1">
<Checkbox
v-if="multiple"
:checked="isSelected && !isNodeDisabled(item)"
:disabled="isNodeDisabled(item)"
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
@click="
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
}
handleSelect();
}
handleSelect();
}
"
/>
<div
class="flex items-center gap-1 pl-2"
@click="
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
"
/>
<div
class="flex items-center gap-1"
@click="
(event: MouseEvent) => {
if (isNodeDisabled(item)) {
event.preventDefault();
event.stopPropagation();
return;
}
handleSelect();
}
event.stopPropagation();
event.preventDefault();
handleSelect();
}
"
>
<slot name="node" v-bind="item">
<IconifyIcon
class="size-4"
v-if="showIcon && get(item.value, iconField)"
:icon="get(item.value, iconField)"
/>
{{ get(item.value, labelField) }}
</slot>
"
>
<slot name="node" v-bind="item">
<IconifyIcon
class="size-4"
v-if="showIcon && get(item.value, iconField)"
:icon="get(item.value, iconField)"
/>
{{ get(item.value, labelField) }}
</slot>
</div>
</div>
<div class="h-4 w-4"></div>
</TreeItem>
</TransitionGroup>
<div class="w-full" v-if="$slots.footer">
<div
:class="
cn('my-0.5 flex w-full items-center p-1', bordered ? 'border-t' : '')
"
v-if="$slots.footer"
>
<slot name="footer"> </slot>
</div>
</TreeRoot>

View File

@@ -40,3 +40,23 @@ export interface TreeProps {
/** 值字段 */
valueField?: string;
}
export function treePropsDefaults() {
return {
allowClear: false,
autoCheckParent: true,
bordered: false,
checkStrictly: false,
defaultExpandedKeys: () => [],
defaultExpandedLevel: 0,
disabled: false,
disabledField: 'disabled',
iconField: 'icon',
labelField: 'label',
multiple: false,
showIcon: true,
transition: true,
valueField: 'value',
childrenField: 'children',
};
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/tabs-ui",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/constants",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/access",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/common-ui",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -9,6 +9,7 @@ export * from './loading';
export * from './page';
export * from './resize';
export * from './tippy';
export * from './tree';
export * from '@vben-core/form-ui';
export * from '@vben-core/popup-ui';
@@ -27,7 +28,6 @@ export {
VbenPinInput,
VbenSelect,
VbenSpinner,
VbenTree,
} from '@vben-core/shadcn-ui';
export type { FlattenedItem } from '@vben-core/shadcn-ui';

View File

@@ -25,7 +25,7 @@ const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
const contentStyle = computed<StyleValue>(() => {
if (autoContentHeight) {
return {
height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px - ${typeof heightOffset === 'number' ? `${heightOffset}px` : heightOffset})`,
height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px - ${footerHeight.value}px - ${typeof heightOffset === 'number' ? `${heightOffset}px` : heightOffset})`,
overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
};
}
@@ -50,7 +50,7 @@ onMounted(() => {
</script>
<template>
<div class="relative">
<div class="relative flex min-h-full flex-col">
<div
v-if="
description ||
@@ -89,16 +89,10 @@ onMounted(() => {
<div :class="cn('h-full p-4', contentClass)" :style="contentStyle">
<slot></slot>
</div>
<div
v-if="$slots.footer"
ref="footerRef"
:class="
cn(
'bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4',
footerClass,
)
"
:class="cn('bg-card align-center flex px-6 py-4', footerClass)"
>
<slot name="footer"></slot>
</div>

View File

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

View File

@@ -0,0 +1,25 @@
<script setup lang="ts">
import type { TreeProps } from '@vben-core/shadcn-ui';
import { Inbox } from '@vben/icons';
import { $t } from '@vben/locales';
import { treePropsDefaults, VbenTree } from '@vben-core/shadcn-ui';
const props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());
</script>
<template>
<VbenTree v-if="props.treeData?.length > 0" v-bind="props">
<template v-for="(_, key) in $slots" :key="key" #[key]="slotProps">
<slot :name="key" v-bind="slotProps"> </slot>
</template>
</VbenTree>
<div
v-else
class="flex-col-center text-muted-foreground cursor-pointer rounded-lg border p-10 text-sm font-medium"
>
<Inbox class="size-10" />
<div class="mt-1">{{ $t('common.noData') }}</div>
</div>
</template>

View File

@@ -35,6 +35,10 @@ interface Props {
* @zh_CN 按钮文本
*/
submitButtonText?: string;
/**
* @zh_CN 是否显示返回按钮
*/
showBack?: boolean;
}
defineOptions({
@@ -43,6 +47,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
loading: false,
showBack: true,
loginPath: '/auth/login',
submitButtonText: '',
subTitle: '',
@@ -110,7 +115,12 @@ defineExpose({
{{ submitButtonText || $t('common.login') }}
</slot>
</VbenButton>
<VbenButton class="mt-4 w-full" variant="outline" @click="goToLogin()">
<VbenButton
v-if="showBack"
class="mt-4 w-full"
variant="outline"
@click="goToLogin()"
>
{{ $t('common.back') }}
</VbenButton>
</div>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { RiDingding } from '@vben/icons';
import { SvgDingDingIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { alert, useVbenModal } from '@vben-core/popup-ui';
@@ -96,7 +96,7 @@ const handleLogin = () => {
:tooltip="$t('authentication.dingdingLogin')"
tooltip-side="top"
>
<RiDingding />
<SvgDingDingIcon />
</VbenIconButton>
<Modal>
<div id="dingding_qrcode_login_element"></div>

View File

@@ -35,6 +35,10 @@ interface Props {
* @zh_CN 描述
*/
description?: string;
/**
* @zh_CN 是否显示返回按钮
*/
showBack?: boolean;
}
defineOptions({
@@ -44,6 +48,7 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
description: '',
loading: false,
showBack: true,
loginPath: '/auth/login',
submitButtonText: '',
subTitle: '',
@@ -88,7 +93,12 @@ function goToLogin() {
</p>
</div>
<VbenButton class="mt-4 w-full" variant="outline" @click="goToLogin()">
<VbenButton
v-if="showBack"
class="mt-4 w-full"
variant="outline"
@click="goToLogin()"
>
{{ $t('common.back') }}
</VbenButton>
</div>

View File

@@ -1,6 +1,11 @@
<script setup lang="ts">
import { useAppConfig } from '@vben/hooks';
import { MdiGithub, MdiGoogle, MdiQqchat, MdiWechat } from '@vben/icons';
import {
SvgGithubIcon,
SvgGoogleIcon,
SvgQQChatIcon,
SvgWeChatIcon,
} from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenIconButton } from '@vben-core/shadcn-ui';
@@ -32,28 +37,28 @@ const {
tooltip-side="top"
class="mb-3"
>
<MdiWechat />
<SvgWeChatIcon />
</VbenIconButton>
<VbenIconButton
:tooltip="$t('authentication.qqLogin')"
tooltip-side="top"
class="mb-3"
>
<MdiQqchat />
<SvgQQChatIcon />
</VbenIconButton>
<VbenIconButton
:tooltip="$t('authentication.githubLogin')"
tooltip-side="top"
class="mb-3"
>
<MdiGithub />
<SvgGithubIcon />
</VbenIconButton>
<VbenIconButton
:tooltip="$t('authentication.googleLogin')"
tooltip-side="top"
class="mb-3"
>
<MdiGoogle />
<SvgGoogleIcon />
</VbenIconButton>
<DingdingLogin
v-if="dingdingAuthConfig"

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/hooks",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/layouts",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -50,7 +50,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<AuthenticationFormView
v-if="authPanelLeft"
class="min-h-full w-2/5 flex-1"
transition-name="slide-left"
data-side="left"
>
<template v-if="copyright" #copyright>
<slot name="copyright">
@@ -86,7 +86,14 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
class="bg-background-deep absolute inset-0 h-full w-full dark:bg-[#070709]"
>
<div class="login-background absolute left-0 top-0 size-full"></div>
<div class="flex-col-center -enter-x mr-20 h-full">
<div
:key="authPanelLeft ? 'left' : authPanelRight ? 'right' : 'center'"
class="flex-col-center mr-20 h-full"
:class="{
'enter-x': authPanelLeft,
'-enter-x': authPanelRight,
}"
>
<template v-if="sloganImage">
<img
:alt="appName"
@@ -110,6 +117,7 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<div class="login-background absolute left-0 top-0 size-full"></div>
<AuthenticationFormView
class="md:bg-background shadow-primary/5 shadow-float w-full rounded-3xl pb-20 md:w-2/3 lg:w-1/2 xl:w-[36%]"
data-side="bottom"
>
<template v-if="copyright" #copyright>
<slot name="copyright">
@@ -125,7 +133,8 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<!-- 右侧认证面板 -->
<AuthenticationFormView
v-if="authPanelRight"
class="min-h-full w-[34%] flex-1"
class="min-h-full w-2/5 flex-1"
data-side="right"
>
<template v-if="copyright" #copyright>
<slot name="copyright">

View File

@@ -2,6 +2,10 @@
defineOptions({
name: 'AuthenticationFormView',
});
defineProps<{
dataSide?: 'bottom' | 'left' | 'right' | 'top';
}>();
</script>
<template>
@@ -16,7 +20,8 @@ defineOptions({
<component
:is="Component"
:key="route.fullPath"
class="enter-x mt-6 w-full sm:mx-auto md:max-w-md"
class="side-content mt-6 w-full sm:mx-auto md:max-w-md"
:data-side="dataSide"
/>
</KeepAlive>
</Transition>

View File

@@ -158,7 +158,9 @@ function clickLogo() {
function autoCollapseMenuByRouteMeta(route: RouteLocationNormalizedLoaded) {
// 只在双列模式下生效
if (
preferences.app.layout === 'sidebar-mixed-nav' &&
['header-mixed-nav', 'sidebar-mixed-nav'].includes(
preferences.app.layout,
) &&
route.meta &&
route.meta.hideInMenu
) {

View File

@@ -29,7 +29,8 @@ function useNavigation() {
return true;
}
const route = routeMetaMap.get(path);
return route?.meta?.openInNewWindow ?? false;
// 如果有外链或者设置了在新窗口打开,返回 true
return !!(route?.meta?.link || route?.meta?.openInNewWindow);
};
const resolveHref = (path: string): string => {
@@ -39,7 +40,13 @@ function useNavigation() {
const navigation = async (path: string) => {
try {
const route = routeMetaMap.get(path);
const { openInNewWindow = false, query = {} } = route?.meta ?? {};
const { openInNewWindow = false, query = {}, link } = route?.meta ?? {};
// 检查是否有外链
if (link && typeof link === 'string') {
openWindow(link, { target: '_blank' });
return;
}
if (isHttpUrl(path)) {
openWindow(path, { target: '_blank' });

View File

@@ -31,7 +31,7 @@ async function handleUpdate(value: string | undefined) {
:model-value="preferences.app.locale"
@update:model-value="handleUpdate"
>
<VbenIconButton>
<VbenIconButton class="hover:animate-[shrink_0.3s_ease-in-out]">
<Languages class="text-foreground size-4" />
</VbenIconButton>
</VbenDropdownRadioMenu>

View File

@@ -27,29 +27,30 @@ const emit = defineEmits<{
submit: [Recordable<any>];
}>();
const [Form, { resetForm, validate, getValues }] = useVbenForm(
reactive({
commonConfig: {
hideLabel: true,
hideRequiredMark: true,
},
schema: computed(() => [
{
component: 'VbenInputPassword' as const,
componentProps: {
placeholder: $t('ui.widgets.lockScreen.placeholder'),
},
fieldName: 'lockScreenPassword',
formFieldProps: { validateOnBlur: false },
label: $t('authentication.password'),
rules: z
.string()
.min(1, { message: $t('ui.widgets.lockScreen.placeholder') }),
const [Form, { resetForm, validate, getValues, getFieldComponentRef }] =
useVbenForm(
reactive({
commonConfig: {
hideLabel: true,
hideRequiredMark: true,
},
]),
showDefaultActions: false,
}),
);
schema: computed(() => [
{
component: 'VbenInputPassword' as const,
componentProps: {
placeholder: $t('ui.widgets.lockScreen.placeholder'),
},
fieldName: 'lockScreenPassword',
formFieldProps: { validateOnBlur: false },
label: $t('authentication.password'),
rules: z
.string()
.min(1, { message: $t('ui.widgets.lockScreen.placeholder') }),
},
]),
showDefaultActions: false,
}),
);
const [Modal] = useVbenModal({
onConfirm() {
@@ -60,6 +61,13 @@ const [Modal] = useVbenModal({
resetForm();
}
},
onOpened() {
requestAnimationFrame(() => {
getFieldComponentRef('lockScreenPassword')
?.$el?.querySelector('[name="lockScreenPassword"]')
?.focus();
});
},
});
async function handleSubmit() {

View File

@@ -37,7 +37,7 @@ const date = useDateFormat(now, 'YYYY-MM-DD dddd', { locales: locale.value });
const showUnlockForm = ref(false);
const { lockScreenPassword } = storeToRefs(accessStore);
const [Form, { form, validate }] = useVbenForm(
const [Form, { form, validate, getFieldComponentRef }] = useVbenForm(
reactive({
commonConfig: {
hideLabel: true,
@@ -75,6 +75,13 @@ async function handleSubmit() {
function toggleUnlockForm() {
showUnlockForm.value = !showUnlockForm.value;
if (showUnlockForm.value) {
requestAnimationFrame(() => {
getFieldComponentRef('password')
?.$el?.querySelector('[name="password"]')
?.focus();
});
}
}
useScrollLock();

View File

@@ -2,6 +2,7 @@
import { SUPPORT_LANGUAGES } from '@vben/constants';
import { $t } from '@vben/locales';
import InputItem from '../input-item.vue';
import SelectItem from '../select-item.vue';
import SwitchItem from '../switch-item.vue';
@@ -12,6 +13,7 @@ defineOptions({
const appLocale = defineModel<string>('appLocale');
const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
const appWatermark = defineModel<boolean>('appWatermark');
const appWatermarkContent = defineModel<string>('appWatermarkContent');
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
</script>
@@ -22,9 +24,23 @@ const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
<SwitchItem v-model="appDynamicTitle">
{{ $t('preferences.dynamicTitle') }}
</SwitchItem>
<SwitchItem v-model="appWatermark">
<SwitchItem
v-model="appWatermark"
@update:model-value="
(val) => {
if (!val) appWatermarkContent = '';
}
"
>
{{ $t('preferences.watermark') }}
</SwitchItem>
<InputItem
v-if="appWatermark"
v-model="appWatermarkContent"
:placeholder="$t('preferences.watermarkContent')"
>
{{ $t('preferences.watermarkContent') }}
</InputItem>
<SwitchItem v-model="appEnableCheckUpdates">
{{ $t('preferences.checkUpdates') }}
</SwitchItem>

View File

@@ -3,7 +3,7 @@ import type { SelectOption } from '@vben/types';
import { useSlots } from 'vue';
import { CircleHelp } from '@vben/icons';
import { CircleHelp, CircleX } from '@vben/icons';
import { Input, VbenTooltip } from '@vben-core/shadcn-ui';
@@ -47,6 +47,17 @@ const slots = useSlots();
<slot name="tip"></slot>
</VbenTooltip>
</span>
<Input v-model="inputValue" class="h-8 w-[165px]" />
<div class="relative">
<Input
v-model="inputValue"
class="h-8 w-[165px]"
:placeholder="placeholder"
/>
<CircleX
v-if="inputValue"
class="hover:text-foreground text-foreground/60 absolute right-2 top-1/2 size-3 -translate-y-1/2 transform cursor-pointer"
@click="() => (inputValue = '')"
/>
</div>
</div>
</template>

View File

@@ -104,7 +104,7 @@ function selectColor() {
watch(
() => [modelValue.value, props.isDark] as [BuiltinThemeType, boolean],
([themeType, isDark]) => {
([themeType, isDark], [_, isDarkPrev]) => {
const theme = builtinThemePresets.value.find(
(item) => item.type === themeType,
);
@@ -113,7 +113,9 @@ watch(
? theme.darkPrimaryColor || theme.primaryColor
: theme.primaryColor;
themeColorPrimary.value = primaryColor || theme.color;
if (!(theme.type === 'custom' && isDark !== isDarkPrev)) {
themeColorPrimary.value = primaryColor || theme.color;
}
}
},
);
@@ -132,14 +134,14 @@ watch(
<template v-if="theme.type !== 'custom'">
<div
:style="{ backgroundColor: theme.color }"
class="mx-10 my-2 size-5 rounded-md"
class="mx-9 my-2 size-5 rounded-md"
></div>
</template>
<template v-else>
<div class="size-full px-10 py-2" @click.stop="selectColor">
<div class="size-full px-9 py-2" @click.stop="selectColor">
<div class="flex-center relative size-5 rounded-sm">
<UserRoundPen
class="absolute z-10 size-5 opacity-60 group-hover:opacity-100"
class="z-1 absolute size-5 opacity-60 group-hover:opacity-100"
/>
<input
ref="colorInput"

View File

@@ -13,7 +13,7 @@ function clearPreferencesAndLogout() {
</script>
<template>
<Preferences @clear-preferences-and-logout="clearPreferencesAndLogout">
<VbenIconButton>
<VbenIconButton class="hover:animate-[shrink_0.3s_ease-in-out]">
<Settings class="text-foreground size-4" />
</VbenIconButton>
</Preferences>

View File

@@ -16,7 +16,7 @@ import type { SegmentedItem } from '@vben-core/shadcn-ui';
import { computed, ref } from 'vue';
import { Copy, RotateCw } from '@vben/icons';
import { Copy, Pin, PinOff, RotateCw } from '@vben/icons';
import { $t, loadLocaleMessages } from '@vben/locales';
import {
clearPreferencesCache,
@@ -67,7 +67,11 @@ const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
const appWatermark = defineModel<boolean>('appWatermark');
const appWatermarkContent = defineModel<string>('appWatermarkContent');
const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
const appEnableStickyPreferencesNavigationBar = defineModel<boolean>(
'appEnableStickyPreferencesNavigationBar',
);
const appPreferencesButtonPosition = defineModel<PreferencesButtonPositionType>(
'appPreferencesButtonPosition',
);
@@ -240,7 +244,7 @@ async function handleReset() {
<Drawer
:description="$t('preferences.subtitle')"
:title="$t('preferences.title')"
class="sm:max-w-sm"
class="!border-0 sm:max-w-sm"
>
<template #extra>
<div class="flex items-center">
@@ -248,18 +252,44 @@ async function handleReset() {
:disabled="!diffPreference"
:tooltip="$t('preferences.resetTip')"
class="relative"
@click="handleReset"
>
<span
v-if="diffPreference"
class="bg-primary absolute right-0.5 top-0.5 h-2 w-2 rounded"
></span>
<RotateCw class="size-4" @click="handleReset" />
<RotateCw class="size-4" />
</VbenIconButton>
<VbenIconButton
:tooltip="
appEnableStickyPreferencesNavigationBar
? $t('preferences.disableStickyPreferencesNavigationBar')
: $t('preferences.enableStickyPreferencesNavigationBar')
"
class="relative"
@click="
() =>
(appEnableStickyPreferencesNavigationBar =
!appEnableStickyPreferencesNavigationBar)
"
>
<PinOff
v-if="appEnableStickyPreferencesNavigationBar"
class="size-4"
/>
<Pin v-else class="size-4" />
</VbenIconButton>
</div>
</template>
<div class="p-1">
<VbenSegmented v-model="activeTab" :tabs="tabs">
<div>
<VbenSegmented
v-model="activeTab"
:tabs="tabs"
:class="{
'sticky-tabs-header': appEnableStickyPreferencesNavigationBar,
}"
>
<template #general>
<Block :title="$t('preferences.general')">
<General
@@ -267,6 +297,7 @@ async function handleReset() {
v-model:app-enable-check-updates="appEnableCheckUpdates"
v-model:app-locale="appLocale"
v-model:app-watermark="appWatermark"
v-model:app-watermark-content="appWatermarkContent"
/>
</Block>
@@ -447,3 +478,11 @@ async function handleReset() {
</Drawer>
</div>
</template>
<style scoped>
:deep(.sticky-tabs-header [role='tablist']) {
position: sticky;
top: -12px;
z-index: 10;
}
</style>

View File

@@ -64,7 +64,7 @@ function toggleTheme(event: MouseEvent) {
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
];
document.documentElement.animate(
const animate = document.documentElement.animate(
{
clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
},
@@ -76,6 +76,9 @@ function toggleTheme(event: MouseEvent) {
: '::view-transition-new(root)',
},
);
animate.onfinish = () => {
transition.skipTransition();
};
});
}
</script>
@@ -85,7 +88,7 @@ function toggleTheme(event: MouseEvent) {
:aria-label="theme"
:class="[`is-${theme}`]"
aria-live="polite"
class="theme-toggle cursor-pointer border-none bg-none"
class="theme-toggle cursor-pointer border-none bg-none hover:animate-[shrink_0.3s_ease-in-out]"
v-bind="bindProps"
@click.stop="toggleTheme"
>

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/plugins",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,24 +1,23 @@
import type { EChartsOption } from 'echarts';
import type { EChartsOption } from "echarts";
import type { Ref } from 'vue';
import type { Ref } from "vue";
import { computed, nextTick, watch } from "vue";
import type { Nullable } from '@vben/types';
import type { Nullable } from "@vben/types";
import type EchartsUI from './echarts-ui.vue';
import type EchartsUI from "./echarts-ui.vue";
import { computed, nextTick, watch } from 'vue';
import { usePreferences } from '@vben/preferences';
import { usePreferences } from "@vben/preferences";
import {
tryOnUnmounted,
useDebounceFn,
useResizeObserver,
useTimeoutFn,
useWindowSize,
} from '@vueuse/core';
useWindowSize
} from "@vueuse/core";
import echarts from './echarts';
import echarts from "./echarts";
type EchartsUIType = typeof EchartsUI | undefined;
@@ -32,6 +31,21 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
const { height, width } = useWindowSize();
const resizeHandler: () => void = useDebounceFn(resize, 200);
const getChartEl = (): HTMLElement | null => {
const refValue = chartRef?.value as unknown;
if (!refValue) return null;
if (refValue instanceof HTMLElement) {
return refValue;
}
const maybeComponent = refValue as { $el?: HTMLElement };
return maybeComponent.$el ?? null;
};
const isElHidden = (el: HTMLElement | null): boolean => {
if (!el) return true;
return el.offsetHeight === 0 || el.offsetWidth === 0;
};
const getOptions = computed((): EChartsOption => {
if (!isDark.value) {
return {};
@@ -54,7 +68,7 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
const renderEcharts = (
options: EChartsOption,
clear = true,
clear = true
): Promise<Nullable<echarts.ECharts>> => {
cacheOptions = options;
const currentOptions = {
@@ -69,6 +83,13 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
return;
}
nextTick(() => {
const el = getChartEl();
if (isElHidden(el)) {
useTimeoutFn(async () => {
resolve(await renderEcharts(currentOptions));
}, 30);
return;
}
useTimeoutFn(() => {
if (!chartInstance) {
const instance = initCharts();
@@ -83,6 +104,10 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
};
function resize() {
const el = getChartEl();
if (isElHidden(el)) {
return;
}
chartInstance?.resize({
animation: {
duration: 300,

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/request",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -0,0 +1,142 @@
import type { RequestClient } from '../request-client';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SSE } from './sse';
// 模拟 TextDecoder
const OriginalTextDecoder = globalThis.TextDecoder;
beforeEach(() => {
vi.stubGlobal(
'TextDecoder',
class {
private decoder = new OriginalTextDecoder();
decode(value: Uint8Array, opts?: any) {
return this.decoder.decode(value, opts);
}
},
);
});
// 创建 fetch mock
const createFetchMock = (chunks: string[], ok = true) => {
const encoder = new TextEncoder();
let index = 0;
return vi.fn().mockResolvedValue({
ok,
status: ok ? 200 : 500,
body: {
getReader: () => ({
read: async () => {
if (index < chunks.length) {
return { done: false, value: encoder.encode(chunks[index++]) };
}
return { done: true, value: undefined };
},
}),
},
});
};
describe('sSE', () => {
let client: RequestClient;
let sse: SSE;
beforeEach(() => {
vi.restoreAllMocks();
client = {
getBaseUrl: () => 'http://localhost',
instance: {
interceptors: {
request: {
handlers: [],
},
},
},
} as unknown as RequestClient;
sse = new SSE(client);
});
it('should call requestSSE when postSSE is used', async () => {
const spy = vi.spyOn(sse, 'requestSSE').mockResolvedValue(undefined);
await sse.postSSE('/test', { foo: 'bar' }, { headers: { a: '1' } });
expect(spy).toHaveBeenCalledWith(
'/test',
{ foo: 'bar' },
{
headers: { a: '1' },
method: 'POST',
},
);
});
it('should throw error if fetch response not ok', async () => {
vi.stubGlobal('fetch', createFetchMock([], false));
await expect(sse.requestSSE('/bad')).rejects.toThrow(
'HTTP error! status: 500',
);
});
it('should trigger onMessage and onEnd callbacks', async () => {
const messages: string[] = [];
const onMessage = vi.fn((msg: string) => messages.push(msg));
const onEnd = vi.fn();
vi.stubGlobal('fetch', createFetchMock(['hello', ' world']));
await sse.requestSSE('/sse', undefined, { onMessage, onEnd });
expect(onMessage).toHaveBeenCalledTimes(2);
expect(messages.join('')).toBe('hello world');
// onEnd 不再带参数
expect(onEnd).toHaveBeenCalled();
});
it('should apply request interceptors', async () => {
const interceptor = vi.fn(async (config) => {
config.headers['x-test'] = 'intercepted';
return config;
});
(client.instance.interceptors.request as any).handlers.push({
fulfilled: interceptor,
});
// 创建 fetch mock并挂到全局
const fetchMock = createFetchMock(['data']);
vi.stubGlobal('fetch', fetchMock);
await sse.requestSSE('/sse', undefined, {});
expect(interceptor).toHaveBeenCalled();
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost/sse',
expect.objectContaining({
headers: expect.any(Headers),
}),
);
const calls = fetchMock.mock?.calls;
expect(calls).toBeDefined();
expect(calls?.length).toBeGreaterThan(0);
const init = calls?.[0]?.[1] as RequestInit;
expect(init).toBeDefined();
const headers = init?.headers as Headers;
expect(headers?.get('x-test')).toBe('intercepted');
expect(headers?.get('accept')).toBe('text/event-stream');
});
it('should throw error when no reader', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
status: 200,
body: null,
}),
);
await expect(sse.requestSSE('/sse')).rejects.toThrow('No reader');
});
});

View File

@@ -0,0 +1,136 @@
import type { AxiosRequestHeaders, InternalAxiosRequestConfig } from 'axios';
import type { RequestClient } from '../request-client';
import type { SseRequestOptions } from '../types';
/**
* SSE模块
*/
class SSE {
private client: RequestClient;
constructor(client: RequestClient) {
this.client = client;
}
public async postSSE(
url: string,
data?: any,
requestOptions?: SseRequestOptions,
) {
return this.requestSSE(url, data, {
...requestOptions,
method: 'POST',
});
}
/**
* SSE请求方法
* @param url - 请求URL
* @param data - 请求数据
* @param requestOptions - SSE请求选项
*/
public async requestSSE(
url: string,
data?: any,
requestOptions?: SseRequestOptions,
) {
const baseUrl = this.client.getBaseUrl() || '';
let axiosConfig: InternalAxiosRequestConfig<any> = {
url,
method: (requestOptions?.method as any) ?? 'GET',
headers: {} as AxiosRequestHeaders,
};
const requestInterceptors = this.client.instance.interceptors
.request as any;
if (
requestInterceptors.handlers &&
requestInterceptors.handlers.length > 0
) {
for (const handler of requestInterceptors.handlers) {
if (typeof handler?.fulfilled === 'function') {
const next = await handler.fulfilled(axiosConfig as any);
if (next) axiosConfig = next as InternalAxiosRequestConfig<any>;
}
}
}
const merged = new Headers();
Object.entries(
(axiosConfig.headers ?? {}) as Record<string, string>,
).forEach(([k, v]) => merged.set(k, String(v)));
if (requestOptions?.headers) {
new Headers(requestOptions.headers).forEach((v, k) => merged.set(k, v));
}
if (!merged.has('accept')) {
merged.set('accept', 'text/event-stream');
}
let bodyInit = requestOptions?.body ?? data;
const ct = (merged.get('content-type') || '').toLowerCase();
if (
bodyInit &&
typeof bodyInit === 'object' &&
!ArrayBuffer.isView(bodyInit as any) &&
!(bodyInit instanceof ArrayBuffer) &&
!(bodyInit instanceof Blob) &&
!(bodyInit instanceof FormData) &&
ct.includes('application/json')
) {
bodyInit = JSON.stringify(bodyInit);
}
const requestInit: RequestInit = {
...requestOptions,
method: axiosConfig.method,
headers: merged,
body: bodyInit,
};
const response = await fetch(safeJoinUrl(baseUrl, url), requestInit);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error('No reader');
}
let isEnd = false;
while (!isEnd) {
const { done, value } = await reader.read();
if (done) {
isEnd = true;
decoder.decode(new Uint8Array(0), { stream: false });
requestOptions?.onEnd?.();
reader.releaseLock?.();
break;
}
const content = decoder.decode(value, { stream: true });
requestOptions?.onMessage?.(content);
}
}
}
function safeJoinUrl(baseUrl: string | undefined, url: string): string {
if (!baseUrl) {
return url; // 没有 baseUrl直接返回 url
}
// 如果 url 本身就是绝对地址,直接返回
if (/^https?:\/\//i.test(url)) {
return url;
}
// 如果 baseUrl 是完整 URL就用 new URL
if (/^https?:\/\//i.test(baseUrl)) {
return new URL(url, baseUrl).toString();
}
// 否则,当作路径拼接
return `${baseUrl.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}`;
}
export { SSE };

View File

@@ -9,6 +9,7 @@ import qs from 'qs';
import { FileDownloader } from './modules/downloader';
import { InterceptorManager } from './modules/interceptor';
import { SSE } from './modules/sse';
import { FileUploader } from './modules/uploader';
function getParamsSerializer(
@@ -41,12 +42,14 @@ class RequestClient {
public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
public download: FileDownloader['download'];
public readonly instance: AxiosInstance;
// 是否正在刷新token
public isRefreshing = false;
public postSSE: SSE['postSSE'];
// 刷新token队列
public refreshTokenQueue: ((token: string) => void)[] = [];
public requestSSE: SSE['requestSSE'];
public upload: FileUploader['upload'];
private readonly instance: AxiosInstance;
/**
* 构造函数用于创建Axios实例
@@ -84,6 +87,10 @@ class RequestClient {
// 实例化文件下载器
const fileDownloader = new FileDownloader(this);
this.download = fileDownloader.download.bind(fileDownloader);
// 实例化SSE模块
const sse = new SSE(this);
this.postSSE = sse.postSSE.bind(sse);
this.requestSSE = sse.requestSSE.bind(sse);
}
/**
@@ -103,6 +110,13 @@ class RequestClient {
return this.request<T>(url, { ...config, method: 'GET' });
}
/**
* 获取基础URL
*/
public getBaseUrl() {
return this.instance.defaults.baseURL;
}
/**
* POST请求方法
*/

View File

@@ -41,6 +41,14 @@ type RequestContentType =
type RequestClientOptions = CreateAxiosDefaults & ExtendOptions;
/**
* SSE 请求选项
*/
interface SseRequestOptions extends RequestInit {
onMessage?: (message: string) => void;
onEnd?: () => void;
}
interface RequestInterceptorConfig {
fulfilled?: (
config: ExtendOptions & InternalAxiosRequestConfig,
@@ -78,4 +86,5 @@ export type {
RequestInterceptorConfig,
RequestResponse,
ResponseInterceptorConfig,
SseRequestOptions,
};

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/icons",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -3,13 +3,3 @@ import { createIconifyIcon } from '@vben-core/icons';
export * from '@vben-core/icons';
export const MdiKeyboardEsc = createIconifyIcon('mdi:keyboard-esc');
export const MdiWechat = createIconifyIcon('mdi:wechat');
export const MdiGithub = createIconifyIcon('mdi:github');
export const MdiGoogle = createIconifyIcon('mdi:google');
export const MdiQqchat = createIconifyIcon('mdi:qqchat');
export const RiDingding = createIconifyIcon('ri:dingding-fill');

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M512 2C230.2 2 2 230.2 2 512s228.2 510 510 510 510-228.2 510-510S793.3 2 512 2z m235.9 442c-1 4.6-3.6 10.8-7.2 19.1l-0.5 0.5c-21.6 45.8-77.3 135.5-77.3 135.5l-0.5-0.5-16.5 28.3h78.8L574.3 826.8l34-136h-61.8l21.6-90.2c-17.5 4.1-38.1 9.8-62.3 18 0 0-33 19.1-94.8-37.1 0 0-41.7-37.1-17.5-45.8 10.3-4.1 50-8.8 81.4-12.9 42.2-5.7 68.5-8.8 68.5-8.8s-130.3 2.1-161.2-3.1c-30.9-4.6-70.1-56.7-78.3-102 0 0-12.9-24.7 27.8-12.9 40.2 11.8 209.2 45.8 209.2 45.8S321.4 375 307 358.5c-14.4-16.5-42.8-89.6-39.2-134.5 0 0 1.5-11.3 12.9-8.2 0 0 161.8 74.2 272.5 114.4C664.5 371.4 760.8 392 747.9 444z" fill="#3296FA"/></svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M517.30912244 23.0407001c-277.94803947-0.11230224-503.114027 225.0536853-503.11402702 503.00172479 0 219.66317786 140.82700667 406.42179996 337.01901675 475.03846748 26.39102597 6.73813429 22.34814539-12.24094396 22.3481454-25.04339914v-87.14653682c-152.61874168 17.9683581-158.68306256-83.10365624-169.01486845-99.94899196-20.77591406-35.37520501-69.62738767-44.35938408-55.0280967-61.20471981 34.81369383-17.9683581 70.18889885 4.49208953 111.17921577 65.13529813 29.76009311 43.91017513 87.48344355 36.49822743 116.90662996 29.19858195 6.40122758-26.39102597 20.21440287-49.97449598 38.96887665-68.39206307-157.89694687-28.07555954-223.93066292-124.65548435-223.93066294-239.42837176 0-55.5896079 18.30526482-106.7994285 54.35428331-148.01434994-22.90965658-68.16745859 2.13374251-126.34001793 5.50280964-134.98729027 65.3599026-5.95201861 133.07815223 46.71773107 138.46865968 50.87291388 37.17204084-9.99489919 79.50998463-15.38540662 126.90152914-15.38540659 47.72845123 0 90.29099949 5.50280967 127.57534256 15.61001106 12.69015291-9.65799248 75.69170854-54.80349223 136.33491714-49.30068255 3.25676491 8.64727234 27.73865283 65.47220485 6.1766231 132.74124553 36.49822743 41.32722364 54.91579446 92.87395098 54.91579447 148.80046559 0 114.99749189-66.25832052 211.57741672-224.82908082 239.54067401 26.5033282 26.05411926 42.89945497 62.32774217 42.89945498 102.41964122v126.45232019c0.89841792 9.99489919 0 20.21440287 16.95763795 20.21440287 199.22417052-67.26904065 342.52182644-255.3752896 342.52182644-477.05990776 0-277.94803947-225.27828977-503.114027-503.11402701-503.11402703z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M213.573746 512.08574a296.892457 296.892457 0 0 1 15.663636-94.698456L53.712514 286.089155a504.154105 504.154105 0 0 0 0 451.737229l175.422492-131.502883a296.022255 296.022255 0 0 1-15.356507-94.288949" fill="#FBBC05"/><path d="M521.881206 208.845928a305.696854 305.696854 0 0 1 192.007518 67.210309l151.773472-148.446228A527.547183 527.547183 0 0 0 53.763703 284.195186l175.422491 131.349318a306.771809 306.771809 0 0 1 292.541447-206.698576" fill="#EA4335"/><path d="M521.881206 815.325553a307.130128 307.130128 0 0 1-292.643824-206.698576l-175.524868 131.29813A521.148639 521.148639 0 0 0 521.881206 1023.969287a503.130338 503.130338 0 0 0 339.839486-127.100685l-166.771659-126.128105a327.861412 327.861412 0 0 1-173.221392 44.585056" fill="#34A853"/><path d="M1023.475893 513.774956a415.64944 415.64944 0 0 0-11.875698-92.70211h-486.289369v197.023977h279.744358a229.528582 229.528582 0 0 1-106.522966 152.43892l166.618094 126.128106a502.004194 502.004194 0 0 0 158.069639-382.888893" fill="#4285F4"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M511.09761 957.257c-80.159 0-153.737-25.019-201.11-62.386-24.057 6.702-54.831 17.489-74.252 30.864-16.617 11.439-14.546 23.106-11.55 27.816 13.15 20.689 225.583 13.211 286.912 6.767v-3.061z" fill="#FAAD08"/><path d="M496.65061 957.257c80.157 0 153.737-25.019 201.11-62.386 24.057 6.702 54.83 17.489 74.253 30.864 16.616 11.439 14.543 23.106 11.55 27.816-13.15 20.689-225.584 13.211-286.914 6.767v-3.061z" fill="#FAAD08"/><path d="M497.12861 474.524c131.934-0.876 237.669-25.783 273.497-35.34 8.541-2.28 13.11-6.364 13.11-6.364 0.03-1.172 0.542-20.952 0.542-31.155C784.27761 229.833 701.12561 57.173 496.64061 57.162 292.15661 57.173 209.00061 229.832 209.00061 401.665c0 10.203 0.516 29.983 0.547 31.155 0 0 3.717 3.821 10.529 5.67 33.078 8.98 140.803 35.139 276.08 36.034h0.972z" fill="#000000"/><path d="M860.28261 619.782c-8.12-26.086-19.204-56.506-30.427-85.72 0 0-6.456-0.795-9.718 0.148-100.71 29.205-222.773 47.818-315.792 46.695h-0.962C410.88561 582.017 289.65061 563.617 189.27961 534.698 185.44461 533.595 177.87261 534.063 177.87261 534.063 166.64961 563.276 155.56661 593.696 147.44761 619.782 108.72961 744.168 121.27261 795.644 130.82461 796.798c20.496 2.474 79.78-93.637 79.78-93.637 0 97.66 88.324 247.617 290.576 248.996a718.01 718.01 0 0 1 5.367 0C708.80161 950.778 797.12261 800.822 797.12261 703.162c0 0 59.284 96.111 79.783 93.637 9.55-1.154 22.093-52.63-16.623-177.017" fill="#000000"/><path d="M434.38261 316.917c-27.9 1.24-51.745-30.106-53.24-69.956-1.518-39.877 19.858-73.207 47.764-74.454 27.875-1.224 51.703 30.109 53.218 69.974 1.527 39.877-19.853 73.2-47.742 74.436m206.67-69.956c-1.494 39.85-25.34 71.194-53.24 69.956-27.888-1.238-49.269-34.559-47.742-74.435 1.513-39.868 25.341-71.201 53.216-69.974 27.909 1.247 49.285 34.576 47.767 74.453" fill="#FFFFFF"/><path d="M683.94261 368.627c-7.323-17.609-81.062-37.227-172.353-37.227h-0.98c-91.29 0-165.031 19.618-172.352 37.227a6.244 6.244 0 0 0-0.535 2.505c0 1.269 0.393 2.414 1.006 3.386 6.168 9.765 88.054 58.018 171.882 58.018h0.98c83.827 0 165.71-48.25 171.881-58.016a6.352 6.352 0 0 0 1.002-3.395c0-0.897-0.2-1.736-0.531-2.498" fill="#FAAD08"/><path d="M467.63161 256.377c1.26 15.886-7.377 30-19.266 31.542-11.907 1.544-22.569-10.083-23.836-25.978-1.243-15.895 7.381-30.008 19.25-31.538 11.927-1.549 22.607 10.088 23.852 25.974m73.097 7.935c2.533-4.118 19.827-25.77 55.62-17.886 9.401 2.07 13.75 5.116 14.668 6.316 1.355 1.77 1.726 4.29 0.352 7.684-2.722 6.725-8.338 6.542-11.454 5.226-2.01-0.85-26.94-15.889-49.905 6.553-1.579 1.545-4.405 2.074-7.085 0.242-2.678-1.834-3.786-5.553-2.196-8.135" fill="#000000"/><path d="M504.33261 584.495h-0.967c-63.568 0.752-140.646-7.504-215.286-21.92-6.391 36.262-10.25 81.838-6.936 136.196 8.37 137.384 91.62 223.736 220.118 224.996H506.48461c128.498-1.26 211.748-87.612 220.12-224.996 3.314-54.362-0.547-99.938-6.94-136.203-74.654 14.423-151.745 22.684-215.332 21.927" fill="#FFFFFF"/><path d="M323.27461 577.016v137.468s64.957 12.705 130.031 3.91V591.59c-41.225-2.262-85.688-7.304-130.031-14.574" fill="#EB1C26"/><path d="M788.09761 432.536s-121.98 40.387-283.743 41.539h-0.962c-161.497-1.147-283.328-41.401-283.744-41.539l-40.854 106.952c102.186 32.31 228.837 53.135 324.598 51.926l0.96-0.002c95.768 1.216 222.4-19.61 324.6-51.924l-40.855-106.952z" fill="#EB1C26"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="200" height="200"><path d="M712.149333 352.234667c5.184 0 10.282667 0.064 15.381334 0.341333-26.944-146.837333-178.602667-259.2-361.642667-259.2-202.090667 0-365.888 137.002667-365.888 306.005333 0 99.093333 56.298667 187.178667 143.637333 243.093334l3.349334 2.133333-35.349334 110.72 132.266667-67.370667 6.229333 1.792a431.296 431.296 0 0 0 140.330667 14.848 237.141333 237.141333 0 0 1-11.626667-73.002666c0.021333-154.282667 149.290667-279.36 333.312-279.36z m-218.901333-107.968c28.373333 0 51.349333 22.250667 51.349333 49.728 0 27.456-22.976 49.770667-51.349333 49.770666-28.416 0-51.370667-22.293333-51.370667-49.770666-0.021333-27.498667 22.954667-49.728 51.370667-49.728z m-254.677333 99.477333c-28.394667 0-51.370667-22.293333-51.370667-49.770667 0-27.477333 22.997333-49.728 51.370667-49.728 28.394667 0 51.434667 22.250667 51.434666 49.728s-23.04 49.770667-51.434666 49.770667z" fill="#46AF35"/><path d="M405.76 633.408c0 142.805333 138.453333 258.56 309.162667 258.56a363.392 363.392 0 0 0 103.04-14.762667l111.701333 56.96-29.866667-93.589333 2.816-1.792c73.770667-47.232 121.344-121.621333 121.344-205.397333 0-142.741333-138.389333-258.496-309.056-258.496-170.688 0.042667-309.141333 115.776-309.141333 258.517333z m373.312-89.045333c0-23.168 19.413333-41.962667 43.370667-41.962667 24.021333 0 43.413333 18.816 43.413333 41.962667 0 23.253333-19.413333 42.090667-43.413333 42.090666-23.957333 0-43.370667-18.858667-43.370667-42.090666z m-215.146667 0c0-23.168 19.456-41.962667 43.413334-41.962667 23.978667 0 43.413333 18.816 43.413333 41.962667 0 23.253333-19.434667 42.090667-43.413333 42.090666-23.957333 0-43.413333-18.858667-43.413334-42.090666z" fill="#46AF35"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -11,6 +11,11 @@ const SvgCardIcon = createIconifyIcon('svg:card');
const SvgBellIcon = createIconifyIcon('svg:bell');
const SvgCakeIcon = createIconifyIcon('svg:cake');
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
const SvgGithubIcon = createIconifyIcon('svg:github');
const SvgGoogleIcon = createIconifyIcon('svg:google');
const SvgQQChatIcon = createIconifyIcon('svg:qqchat');
const SvgWeChatIcon = createIconifyIcon('svg:wechat');
const SvgDingDingIcon = createIconifyIcon('svg:dingding');
export {
SvgAntdvLogoIcon,
@@ -21,5 +26,10 @@ export {
SvgBellIcon,
SvgCakeIcon,
SvgCardIcon,
SvgDingDingIcon,
SvgDownloadIcon,
SvgGithubIcon,
SvgGoogleIcon,
SvgQQChatIcon,
SvgWeChatIcon,
};

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/locales",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -51,6 +51,10 @@
"sendCode": "Get Security code",
"sendText": "Resend in {0}s",
"thirdPartyLogin": "Or continue with",
"weChat": "WeChat",
"qq": "QQ",
"gitHub": "GitHub",
"google": "Google",
"loginAgainTitle": "Please Log In Again",
"loginAgainSubTitle": "Your login session has expired. Please log in again to continue.",
"layout": {

View File

@@ -1,6 +1,8 @@
{
"title": "Preferences",
"subtitle": "Customize Preferences & Preview in Real Time",
"enableStickyPreferencesNavigationBar": "Enable sticky preferences navigation bar",
"disableStickyPreferencesNavigationBar": "Disable sticky preferences navigation bar",
"resetTip": "Data has changed, click to reset",
"resetTitle": "Reset Preferences",
"resetSuccess": "Preferences reset successfully",
@@ -37,6 +39,7 @@
"language": "Language",
"dynamicTitle": "Dynamic Title",
"watermark": "Watermark",
"watermarkContent": "Please input Watermark content",
"checkUpdates": "Periodic update check",
"position": {
"title": "Preferences Postion",

View File

@@ -51,6 +51,10 @@
"sendCode": "获取验证码",
"sendText": "{0}秒后重新获取",
"thirdPartyLogin": "其他登录方式",
"weChat": "微信",
"qq": "QQ",
"gitHub": "GitHub",
"google": "Google",
"loginAgainTitle": "重新登录",
"loginAgainSubTitle": "您的登录状态已过期,请重新登录以继续。",
"layout": {

View File

@@ -1,6 +1,8 @@
{
"title": "偏好设置",
"subtitle": "自定义偏好设置 & 实时预览",
"enableStickyPreferencesNavigationBar": "开启首选项导航栏吸顶效果",
"disableStickyPreferencesNavigationBar": "关闭首选项导航栏吸顶效果",
"resetTitle": "重置偏好设置",
"resetTip": "数据有变化,点击可进行重置",
"resetSuccess": "重置偏好设置成功",
@@ -37,6 +39,7 @@
"language": "语言",
"dynamicTitle": "动态标题",
"watermark": "水印",
"watermarkContent": "请输入水印文案",
"checkUpdates": "定时检查更新",
"position": {
"title": "偏好设置位置",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/preferences",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/stores",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/styles",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/types",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/utils",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {