mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-03-31 02:43:23 +08:00
Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev
This commit is contained in:
@@ -32,6 +32,7 @@ export {
|
|||||||
Grip,
|
Grip,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
Menu as IconDefault,
|
Menu as IconDefault,
|
||||||
|
Inbox,
|
||||||
Info,
|
Info,
|
||||||
InspectionPanel,
|
InspectionPanel,
|
||||||
Languages,
|
Languages,
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ function handleComplete(e: string[]) {
|
|||||||
async function handleSend(e: Event) {
|
async function handleSend(e: Event) {
|
||||||
try {
|
try {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
await handleSendCode();
|
|
||||||
countdown.value = maxTime;
|
countdown.value = maxTime;
|
||||||
startCountdown();
|
startCountdown();
|
||||||
|
await handleSendCode();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send code:', error);
|
console.error('Failed to send code:', error);
|
||||||
// Consider emitting an error event or showing a notification
|
// Consider emitting an error event or showing a notification
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export { default as VbenTree } from './tree.vue';
|
export { default as VbenTree } from './tree.vue';
|
||||||
|
export type { TreeProps } from './types';
|
||||||
|
export { treePropsDefaults } from './types';
|
||||||
export type { FlattenedItem } from 'radix-vue';
|
export type { FlattenedItem } from 'radix-vue';
|
||||||
|
|||||||
@@ -14,25 +14,9 @@ import { cn, get } from '@vben-core/shared/utils';
|
|||||||
import { TreeItem, TreeRoot } from 'radix-vue';
|
import { TreeItem, TreeRoot } from 'radix-vue';
|
||||||
|
|
||||||
import { Checkbox } from '../checkbox';
|
import { Checkbox } from '../checkbox';
|
||||||
|
import { treePropsDefaults } from './types';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<TreeProps>(), {
|
const props = withDefaults(defineProps<TreeProps>(), treePropsDefaults());
|
||||||
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 emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
expand: [value: FlattenedItem<Recordable<any>>];
|
expand: [value: FlattenedItem<Recordable<any>>];
|
||||||
@@ -41,7 +25,9 @@ const emits = defineEmits<{
|
|||||||
|
|
||||||
interface InnerFlattenItem<T = Recordable<any>, P = number | string> {
|
interface InnerFlattenItem<T = Recordable<any>, P = number | string> {
|
||||||
hasChildren: boolean;
|
hasChildren: boolean;
|
||||||
|
id: P;
|
||||||
level: number;
|
level: number;
|
||||||
|
parentId: null | P;
|
||||||
parents: P[];
|
parents: P[];
|
||||||
value: T;
|
value: T;
|
||||||
}
|
}
|
||||||
@@ -50,24 +36,25 @@ function flatten<T = Recordable<any>, P = number | string>(
|
|||||||
items: T[],
|
items: T[],
|
||||||
childrenField: string = 'children',
|
childrenField: string = 'children',
|
||||||
level = 0,
|
level = 0,
|
||||||
|
parentId: null | P = null,
|
||||||
parents: P[] = [],
|
parents: P[] = [],
|
||||||
): InnerFlattenItem<T, P>[] {
|
): InnerFlattenItem<T, P>[] {
|
||||||
const result: InnerFlattenItem<T, P>[] = [];
|
const result: InnerFlattenItem<T, P>[] = [];
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
const children = get(item, childrenField) as Array<T>;
|
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,
|
hasChildren: Array.isArray(children) && children.length > 0,
|
||||||
|
id,
|
||||||
level,
|
level,
|
||||||
|
parentId,
|
||||||
parents: [...parents],
|
parents: [...parents],
|
||||||
value: item,
|
value: item,
|
||||||
};
|
};
|
||||||
result.push(val);
|
result.push(val);
|
||||||
if (val.hasChildren)
|
if (val.hasChildren)
|
||||||
result.push(
|
result.push(
|
||||||
...flatten(children, childrenField, level + 1, [
|
...flatten(children, childrenField, level + 1, id, [...parents, id]),
|
||||||
...parents,
|
|
||||||
get(item, props.valueField),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
@@ -103,15 +90,10 @@ function updateTreeValue() {
|
|||||||
treeValue.value = undefined;
|
treeValue.value = undefined;
|
||||||
} else {
|
} else {
|
||||||
if (Array.isArray(val)) {
|
if (Array.isArray(val)) {
|
||||||
let filteredValues = val.filter((v) => {
|
const filteredValues = val.filter((v) => {
|
||||||
const item = getItemByValue(v);
|
const item = getItemByValue(v);
|
||||||
return item && !get(item, props.disabledField);
|
return item && !get(item, props.disabledField);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!props.checkStrictly && props.autoCheckParent) {
|
|
||||||
filteredValues = processParentSelection(filteredValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
treeValue.value = filteredValues.map((v) => getItemByValue(v));
|
treeValue.value = filteredValues.map((v) => getItemByValue(v));
|
||||||
|
|
||||||
if (filteredValues.length !== val.length) {
|
if (filteredValues.length !== val.length) {
|
||||||
@@ -128,35 +110,7 @@ function updateTreeValue() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function processParentSelection(
|
|
||||||
selectedValues: Array<number | string>,
|
|
||||||
): Array<number | string> {
|
|
||||||
if (props.checkStrictly) return selectedValues;
|
|
||||||
|
|
||||||
const result = [...selectedValues];
|
|
||||||
|
|
||||||
for (let i = result.length - 1; i >= 0; i--) {
|
|
||||||
const currentValue = result[i];
|
|
||||||
if (currentValue === undefined) continue;
|
|
||||||
const currentItem = getItemByValue(currentValue);
|
|
||||||
|
|
||||||
if (!currentItem) continue;
|
|
||||||
|
|
||||||
const children = get(currentItem, props.childrenField);
|
|
||||||
if (Array.isArray(children) && children.length > 0) {
|
|
||||||
const hasSelectedChildren = children.some((child) => {
|
|
||||||
const childValue = get(child, props.valueField);
|
|
||||||
return result.includes(childValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasSelectedChildren) {
|
|
||||||
result.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
function updateModelValue(val: Arrayable<Recordable<any>>) {
|
function updateModelValue(val: Arrayable<Recordable<any>>) {
|
||||||
if (Array.isArray(val)) {
|
if (Array.isArray(val)) {
|
||||||
const filteredVal = val.filter((v) => !get(v, props.disabledField));
|
const filteredVal = val.filter((v) => !get(v, props.disabledField));
|
||||||
@@ -204,6 +158,24 @@ function collapseAll() {
|
|||||||
expanded.value = [];
|
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>>) {
|
function isNodeDisabled(item: FlattenedItem<Recordable<any>>) {
|
||||||
return props.disabled || get(item.value, props.disabledField);
|
return props.disabled || get(item.value, props.disabledField);
|
||||||
}
|
}
|
||||||
@@ -228,12 +200,51 @@ function onSelect(item: FlattenedItem<Recordable<any>>, isSelected: boolean) {
|
|||||||
get(i.value, props.valueField) === get(item.value, props.valueField)
|
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)) {
|
if (Array.isArray(modelValue.value) && !modelValue.value.includes(p)) {
|
||||||
modelValue.value.push(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();
|
updateTreeValue();
|
||||||
emits('select', item);
|
emits('select', item);
|
||||||
}
|
}
|
||||||
@@ -243,6 +254,8 @@ defineExpose({
|
|||||||
collapseNodes,
|
collapseNodes,
|
||||||
expandAll,
|
expandAll,
|
||||||
expandNodes,
|
expandNodes,
|
||||||
|
checkAll,
|
||||||
|
unCheckAll,
|
||||||
expandToLevel,
|
expandToLevel,
|
||||||
getItemByValue,
|
getItemByValue,
|
||||||
});
|
});
|
||||||
@@ -263,15 +276,41 @@ defineExpose({
|
|||||||
v-slot="{ flattenItems }"
|
v-slot="{ flattenItems }"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
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,
|
$attrs.class as unknown as ClassType,
|
||||||
bordered ? 'border' : '',
|
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>
|
<slot name="header"> </slot>
|
||||||
</div>
|
</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' : ''">
|
<TransitionGroup :name="transition ? 'fade' : ''">
|
||||||
<TreeItem
|
<TreeItem
|
||||||
v-for="item in flattenItems"
|
v-for="item in flattenItems"
|
||||||
@@ -283,11 +322,11 @@ defineExpose({
|
|||||||
handleToggle,
|
handleToggle,
|
||||||
}"
|
}"
|
||||||
:key="item._id"
|
:key="item._id"
|
||||||
:style="{ 'padding-left': `${item.level - 0.5}rem` }"
|
:style="{ 'margin-left': `${item.level - 1}rem` }"
|
||||||
:class="
|
:class="
|
||||||
cn('cursor-pointer', getNodeClass?.(item), {
|
cn('cursor-pointer', getNodeClass?.(item), {
|
||||||
'data-[selected]:bg-accent': !multiple,
|
'data-[selected]:bg-accent': !multiple,
|
||||||
'cursor-not-allowed': isNodeDisabled(item),
|
'text-foreground/50 cursor-not-allowed': isNodeDisabled(item),
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
v-bind="
|
v-bind="
|
||||||
@@ -317,7 +356,7 @@ defineExpose({
|
|||||||
!isNodeDisabled(item) && onToggle(item);
|
!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
|
<ChevronRight
|
||||||
v-if="
|
v-if="
|
||||||
@@ -325,7 +364,7 @@ defineExpose({
|
|||||||
Array.isArray(item.value[childrenField]) &&
|
Array.isArray(item.value[childrenField]) &&
|
||||||
item.value[childrenField].length > 0
|
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 }"
|
:class="{ 'rotate-90': isExpanded }"
|
||||||
@click.stop="
|
@click.stop="
|
||||||
() => {
|
() => {
|
||||||
@@ -334,52 +373,56 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div v-else class="h-4 w-4">
|
<div v-else class="h-4 w-4"></div>
|
||||||
<!-- <IconifyIcon v-if="item.value.icon" :icon="item.value.icon" /> -->
|
<div class="flex items-center gap-1">
|
||||||
</div>
|
<Checkbox
|
||||||
<Checkbox
|
v-if="multiple"
|
||||||
v-if="multiple"
|
:checked="isSelected && !isNodeDisabled(item)"
|
||||||
:checked="isSelected && !isNodeDisabled(item)"
|
:disabled="isNodeDisabled(item)"
|
||||||
:disabled="isNodeDisabled(item)"
|
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
|
||||||
:indeterminate="isIndeterminate && !isNodeDisabled(item)"
|
@click="
|
||||||
@click="
|
(event: MouseEvent) => {
|
||||||
(event: MouseEvent) => {
|
if (isNodeDisabled(item)) {
|
||||||
if (isNodeDisabled(item)) {
|
event.preventDefault();
|
||||||
event.preventDefault();
|
event.stopPropagation();
|
||||||
event.stopPropagation();
|
return;
|
||||||
return;
|
}
|
||||||
|
handleSelect();
|
||||||
}
|
}
|
||||||
handleSelect();
|
"
|
||||||
}
|
/>
|
||||||
"
|
<div
|
||||||
/>
|
class="flex items-center gap-1"
|
||||||
<div
|
@click="
|
||||||
class="flex items-center gap-1 pl-2"
|
(event: MouseEvent) => {
|
||||||
@click="
|
if (isNodeDisabled(item)) {
|
||||||
(event: MouseEvent) => {
|
event.preventDefault();
|
||||||
if (isNodeDisabled(item)) {
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
return;
|
||||||
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)"
|
||||||
<slot name="node" v-bind="item">
|
:icon="get(item.value, iconField)"
|
||||||
<IconifyIcon
|
/>
|
||||||
class="size-4"
|
{{ get(item.value, labelField) }}
|
||||||
v-if="showIcon && get(item.value, iconField)"
|
</slot>
|
||||||
:icon="get(item.value, iconField)"
|
</div>
|
||||||
/>
|
|
||||||
{{ get(item.value, labelField) }}
|
|
||||||
</slot>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="h-4 w-4"></div>
|
||||||
</TreeItem>
|
</TreeItem>
|
||||||
</TransitionGroup>
|
</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>
|
<slot name="footer"> </slot>
|
||||||
</div>
|
</div>
|
||||||
</TreeRoot>
|
</TreeRoot>
|
||||||
|
|||||||
@@ -40,3 +40,23 @@ export interface TreeProps {
|
|||||||
/** 值字段 */
|
/** 值字段 */
|
||||||
valueField?: string;
|
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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export * from './markdown';
|
|||||||
export * from './page';
|
export * from './page';
|
||||||
export * from './resize';
|
export * from './resize';
|
||||||
export * from './tippy';
|
export * from './tippy';
|
||||||
|
export * from './tree';
|
||||||
export * from '@vben-core/form-ui';
|
export * from '@vben-core/form-ui';
|
||||||
export * from '@vben-core/popup-ui';
|
export * from '@vben-core/popup-ui';
|
||||||
|
|
||||||
@@ -30,7 +31,6 @@ export {
|
|||||||
VbenPinInput,
|
VbenPinInput,
|
||||||
VbenSelect,
|
VbenSelect,
|
||||||
VbenSpinner,
|
VbenSpinner,
|
||||||
VbenTree,
|
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
export type { FlattenedItem } from '@vben-core/shadcn-ui';
|
export type { FlattenedItem } from '@vben-core/shadcn-ui';
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
|
|||||||
const contentStyle = computed<StyleValue>(() => {
|
const contentStyle = computed<StyleValue>(() => {
|
||||||
if (autoContentHeight) {
|
if (autoContentHeight) {
|
||||||
return {
|
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',
|
overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative flex min-h-full flex-col">
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
description ||
|
description ||
|
||||||
@@ -89,16 +89,10 @@ onMounted(() => {
|
|||||||
<div :class="cn('h-full p-4', contentClass)" :style="contentStyle">
|
<div :class="cn('h-full p-4', contentClass)" :style="contentStyle">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="$slots.footer"
|
v-if="$slots.footer"
|
||||||
ref="footerRef"
|
ref="footerRef"
|
||||||
:class="
|
:class="cn('bg-card align-center flex px-6 py-4', footerClass)"
|
||||||
cn(
|
|
||||||
'bg-card align-center absolute bottom-0 left-0 right-0 flex px-6 py-4',
|
|
||||||
footerClass,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
packages/effects/common-ui/src/components/tree/index.ts
Normal file
1
packages/effects/common-ui/src/components/tree/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Tree } from './tree.vue';
|
||||||
25
packages/effects/common-ui/src/components/tree/tree.vue
Normal file
25
packages/effects/common-ui/src/components/tree/tree.vue
Normal 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>
|
||||||
@@ -33,6 +33,10 @@ interface Props {
|
|||||||
* @zh_CN 按钮文本
|
* @zh_CN 按钮文本
|
||||||
*/
|
*/
|
||||||
submitButtonText?: string;
|
submitButtonText?: string;
|
||||||
|
/**
|
||||||
|
* @zh_CN 是否显示返回按钮
|
||||||
|
*/
|
||||||
|
showBack?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -41,6 +45,7 @@ defineOptions({
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
showBack: true,
|
||||||
loginPath: '/auth/login',
|
loginPath: '/auth/login',
|
||||||
submitButtonText: '',
|
submitButtonText: '',
|
||||||
subTitle: '',
|
subTitle: '',
|
||||||
@@ -112,7 +117,12 @@ defineExpose({
|
|||||||
{{ submitButtonText || $t('common.login') }}
|
{{ submitButtonText || $t('common.login') }}
|
||||||
</slot>
|
</slot>
|
||||||
</VbenButton>
|
</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') }}
|
{{ $t('common.back') }}
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ interface Props {
|
|||||||
* @zh_CN 描述
|
* @zh_CN 描述
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/**
|
||||||
|
* @zh_CN 是否显示返回按钮
|
||||||
|
*/
|
||||||
|
showBack?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -41,6 +45,7 @@ defineOptions({
|
|||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
description: '',
|
description: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
|
showBack: true,
|
||||||
loginPath: '/auth/login',
|
loginPath: '/auth/login',
|
||||||
submitButtonText: '',
|
submitButtonText: '',
|
||||||
subTitle: '',
|
subTitle: '',
|
||||||
@@ -85,7 +90,12 @@ function goToLogin() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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') }}
|
{{ $t('common.back') }}
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
142
packages/effects/request/src/request-client/modules/sse.test.ts
Normal file
142
packages/effects/request/src/request-client/modules/sse.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
136
packages/effects/request/src/request-client/modules/sse.ts
Normal file
136
packages/effects/request/src/request-client/modules/sse.ts
Normal 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 };
|
||||||
@@ -9,6 +9,7 @@ import qs from 'qs';
|
|||||||
|
|
||||||
import { FileDownloader } from './modules/downloader';
|
import { FileDownloader } from './modules/downloader';
|
||||||
import { InterceptorManager } from './modules/interceptor';
|
import { InterceptorManager } from './modules/interceptor';
|
||||||
|
import { SSE } from './modules/sse';
|
||||||
import { FileUploader } from './modules/uploader';
|
import { FileUploader } from './modules/uploader';
|
||||||
|
|
||||||
function getParamsSerializer(
|
function getParamsSerializer(
|
||||||
@@ -41,12 +42,14 @@ class RequestClient {
|
|||||||
public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
|
public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
|
||||||
public download: FileDownloader['download'];
|
public download: FileDownloader['download'];
|
||||||
|
|
||||||
|
public readonly instance: AxiosInstance;
|
||||||
// 是否正在刷新token
|
// 是否正在刷新token
|
||||||
public isRefreshing = false;
|
public isRefreshing = false;
|
||||||
|
public postSSE: SSE['postSSE'];
|
||||||
// 刷新token队列
|
// 刷新token队列
|
||||||
public refreshTokenQueue: ((token: string) => void)[] = [];
|
public refreshTokenQueue: ((token: string) => void)[] = [];
|
||||||
|
public requestSSE: SSE['requestSSE'];
|
||||||
public upload: FileUploader['upload'];
|
public upload: FileUploader['upload'];
|
||||||
private readonly instance: AxiosInstance;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造函数,用于创建Axios实例
|
* 构造函数,用于创建Axios实例
|
||||||
@@ -84,6 +87,10 @@ class RequestClient {
|
|||||||
// 实例化文件下载器
|
// 实例化文件下载器
|
||||||
const fileDownloader = new FileDownloader(this);
|
const fileDownloader = new FileDownloader(this);
|
||||||
this.download = fileDownloader.download.bind(fileDownloader);
|
this.download = fileDownloader.download.bind(fileDownloader);
|
||||||
|
// 实例化SSE模块
|
||||||
|
const sse = new SSE(this);
|
||||||
|
this.postSSE = sse.postSSE.bind(sse);
|
||||||
|
this.requestSSE = sse.requestSSE.bind(sse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,6 +124,13 @@ class RequestClient {
|
|||||||
return this.request<T>(url, { ...config, method: 'GET' });
|
return this.request<T>(url, { ...config, method: 'GET' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取基础URL
|
||||||
|
*/
|
||||||
|
public getBaseUrl() {
|
||||||
|
return this.instance.defaults.baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST请求方法
|
* POST请求方法
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ type RequestContentType =
|
|||||||
|
|
||||||
type RequestClientOptions = CreateAxiosDefaults & ExtendOptions;
|
type RequestClientOptions = CreateAxiosDefaults & ExtendOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE 请求选项
|
||||||
|
*/
|
||||||
|
interface SseRequestOptions extends RequestInit {
|
||||||
|
onMessage?: (message: string) => void;
|
||||||
|
onEnd?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface RequestInterceptorConfig {
|
interface RequestInterceptorConfig {
|
||||||
fulfilled?: (
|
fulfilled?: (
|
||||||
config: ExtendOptions & InternalAxiosRequestConfig,
|
config: ExtendOptions & InternalAxiosRequestConfig,
|
||||||
@@ -74,6 +82,7 @@ export type {
|
|||||||
RequestInterceptorConfig,
|
RequestInterceptorConfig,
|
||||||
RequestResponse,
|
RequestResponse,
|
||||||
ResponseInterceptorConfig,
|
ResponseInterceptorConfig,
|
||||||
|
SseRequestOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ErrorMessageMode = 'message' | 'modal' | 'none' | undefined;
|
export type ErrorMessageMode = 'message' | 'modal' | 'none' | undefined;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { SystemRoleApi } from '#/api/system/role';
|
|||||||
|
|
||||||
import { computed, nextTick, ref } from 'vue';
|
import { computed, nextTick, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenDrawer, VbenTree } from '@vben/common-ui';
|
import { Tree, useVbenDrawer } from '@vben/common-ui';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { Spin } from 'ant-design-vue';
|
import { Spin } from 'ant-design-vue';
|
||||||
@@ -92,9 +92,6 @@ function getNodeClass(node: Recordable<any>) {
|
|||||||
const classes: string[] = [];
|
const classes: string[] = [];
|
||||||
if (node.value?.type === 'button') {
|
if (node.value?.type === 'button') {
|
||||||
classes.push('inline-flex');
|
classes.push('inline-flex');
|
||||||
if (node.index % 3 >= 1) {
|
|
||||||
classes.push('!pl-0');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
@@ -105,7 +102,7 @@ function getNodeClass(node: Recordable<any>) {
|
|||||||
<Form>
|
<Form>
|
||||||
<template #permissions="slotProps">
|
<template #permissions="slotProps">
|
||||||
<Spin :spinning="loadingPermissions" wrapper-class-name="w-full">
|
<Spin :spinning="loadingPermissions" wrapper-class-name="w-full">
|
||||||
<VbenTree
|
<Tree
|
||||||
:tree-data="permissions"
|
:tree-data="permissions"
|
||||||
multiple
|
multiple
|
||||||
bordered
|
bordered
|
||||||
@@ -120,7 +117,7 @@ function getNodeClass(node: Recordable<any>) {
|
|||||||
<IconifyIcon v-if="value.meta.icon" :icon="value.meta.icon" />
|
<IconifyIcon v-if="value.meta.icon" :icon="value.meta.icon" />
|
||||||
{{ $t(value.meta.title) }}
|
{{ $t(value.meta.title) }}
|
||||||
</template>
|
</template>
|
||||||
</VbenTree>
|
</Tree>
|
||||||
</Spin>
|
</Spin>
|
||||||
</template>
|
</template>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ catalog:
|
|||||||
vue-router: ^4.5.1
|
vue-router: ^4.5.1
|
||||||
vue-tippy: ^6.7.1
|
vue-tippy: ^6.7.1
|
||||||
vue-tsc: 2.2.10
|
vue-tsc: 2.2.10
|
||||||
vxe-pc-ui: ^4.7.12
|
vxe-pc-ui: ^4.9.29
|
||||||
vxe-table: ^4.14.4
|
vxe-table: ^4.16.11
|
||||||
watermark-js-plus: ^1.6.2
|
watermark-js-plus: ^1.6.2
|
||||||
zod: ^3.25.67
|
zod: ^3.25.67
|
||||||
zod-defaults: ^0.1.3
|
zod-defaults: ^0.1.3
|
||||||
|
|||||||
Reference in New Issue
Block a user