Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben/constants": "workspace:*",
|
||||
"@vben/hooks": "workspace:*",
|
||||
"@vben/icons": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/preferences": "workspace:*",
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import type { ClassType } from '@vben/types';
|
||||
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
export interface CaptchaData {
|
||||
@@ -72,7 +74,7 @@ export interface PointSelectionCaptchaProps
|
||||
}
|
||||
|
||||
export interface SliderCaptchaProps {
|
||||
class?: any;
|
||||
class?: ClassType;
|
||||
/**
|
||||
* @description 滑块的样式
|
||||
* @default {}
|
||||
|
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useTemplateRef, watch, watchEffect } from 'vue';
|
||||
|
||||
import { usePagination } from '@vben/hooks';
|
||||
import { EmptyIcon, Grip } from '@vben/icons';
|
||||
import {
|
||||
Button,
|
||||
Pagination,
|
||||
PaginationEllipsis,
|
||||
PaginationFirst,
|
||||
PaginationLast,
|
||||
PaginationList,
|
||||
PaginationListItem,
|
||||
PaginationNext,
|
||||
PaginationPrev,
|
||||
VbenIcon,
|
||||
VbenIconButton,
|
||||
VbenPopover,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props {
|
||||
value?: string;
|
||||
pageSize?: number;
|
||||
/**
|
||||
* 图标列表
|
||||
*/
|
||||
icons?: string[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
value: '',
|
||||
pageSize: 36,
|
||||
icons: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [string];
|
||||
'update:value': [string];
|
||||
}>();
|
||||
|
||||
const refTrigger = useTemplateRef<HTMLElement>('refTrigger');
|
||||
const currentSelect = ref('');
|
||||
const currentList = ref(props.icons);
|
||||
|
||||
watch(
|
||||
() => props.icons,
|
||||
(newIcons) => {
|
||||
currentList.value = newIcons;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const { paginationList, total, setCurrentPage } = usePagination(
|
||||
currentList,
|
||||
props.pageSize,
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
currentSelect.value = props.value;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => currentSelect.value,
|
||||
(v) => {
|
||||
emit('update:value', v);
|
||||
emit('change', v);
|
||||
},
|
||||
);
|
||||
|
||||
const handleClick = (icon: string) => {
|
||||
currentSelect.value = icon;
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
function changeOpenState() {
|
||||
refTrigger.value?.click?.();
|
||||
}
|
||||
|
||||
defineExpose({ changeOpenState });
|
||||
</script>
|
||||
<template>
|
||||
<VbenPopover
|
||||
:content-props="{ align: 'end', alignOffset: -11, sideOffset: 8 }"
|
||||
content-class="p-0 pt-3"
|
||||
>
|
||||
<template #trigger>
|
||||
<div ref="refTrigger">
|
||||
<VbenIcon :icon="currentSelect || Grip" class="size-5" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="paginationList.length > 0">
|
||||
<div class="grid max-h-[360px] w-full grid-cols-6 justify-items-center">
|
||||
<VbenIconButton
|
||||
v-for="(item, index) in paginationList"
|
||||
:key="index"
|
||||
:tooltip="item"
|
||||
tooltip-side="top"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<VbenIcon
|
||||
:class="{
|
||||
'text-primary transition-all': currentSelect === item,
|
||||
}"
|
||||
:icon="item"
|
||||
/>
|
||||
</VbenIconButton>
|
||||
</div>
|
||||
<div
|
||||
v-if="total >= pageSize"
|
||||
class="flex-center flex justify-end overflow-hidden border-t py-2 pr-3"
|
||||
>
|
||||
<Pagination
|
||||
v-slot="{ page }"
|
||||
:items-per-page="36"
|
||||
:sibling-count="1"
|
||||
:total="total"
|
||||
show-edges
|
||||
size="small"
|
||||
@update:page="handlePageChange"
|
||||
>
|
||||
<PaginationList
|
||||
v-slot="{ items }"
|
||||
class="flex w-full items-center gap-1"
|
||||
>
|
||||
<PaginationFirst class="size-5" />
|
||||
<PaginationPrev class="size-5" />
|
||||
<template v-for="(item, index) in items">
|
||||
<PaginationListItem
|
||||
v-if="item.type === 'page'"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
as-child
|
||||
>
|
||||
<Button
|
||||
:variant="item.value === page ? 'default' : 'outline'"
|
||||
class="size-5 p-0 text-sm"
|
||||
>
|
||||
{{ item.value }}
|
||||
</Button>
|
||||
</PaginationListItem>
|
||||
<PaginationEllipsis
|
||||
v-else
|
||||
:key="item.type"
|
||||
:index="index"
|
||||
class="size-5"
|
||||
/>
|
||||
</template>
|
||||
<PaginationNext class="size-5" />
|
||||
<PaginationLast class="size-5" />
|
||||
</PaginationList>
|
||||
</Pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex-col-center text-muted-foreground min-h-[150px] w-full">
|
||||
<EmptyIcon class="size-10" />
|
||||
<div class="mt-1 text-sm">{{ $t('common.noData') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</VbenPopover>
|
||||
</template>
|
@@ -0,0 +1 @@
|
||||
export { default as IconPicker } from './icon-picker.vue';
|
@@ -1,6 +1,7 @@
|
||||
export * from './captcha';
|
||||
export * from './code-mirror';
|
||||
export * from './ellipsis-text';
|
||||
export * from './icon-picker';
|
||||
export * from './json-preview';
|
||||
export * from './markdown';
|
||||
export * from './page';
|
||||
|
@@ -15,6 +15,7 @@ interface WorkbenchProjectItem {
|
||||
group: string;
|
||||
icon: Component | string;
|
||||
title: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface WorkbenchTrendItem {
|
||||
@@ -35,6 +36,7 @@ interface WorkbenchQuickNavItem {
|
||||
color?: string;
|
||||
icon: Component | string;
|
||||
title: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export type {
|
||||
|
@@ -21,6 +21,8 @@ defineOptions({
|
||||
withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -43,6 +45,7 @@ withDefaults(defineProps<Props>(), {
|
||||
:color="item.color"
|
||||
:icon="item.icon"
|
||||
class="size-8 transition-all duration-300 group-hover:scale-110"
|
||||
@click="$emit('click', item)"
|
||||
/>
|
||||
<span class="ml-4 text-lg font-medium">{{ item.title }}</span>
|
||||
</div>
|
||||
|
@@ -21,6 +21,8 @@ defineOptions({
|
||||
withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,6 +39,7 @@ withDefaults(defineProps<Props>(), {
|
||||
'border-b-0': index < 3,
|
||||
}"
|
||||
class="flex-col-center border-border group w-1/3 cursor-pointer border-b border-r border-t py-8 hover:shadow-xl"
|
||||
@click="$emit('click', item)"
|
||||
>
|
||||
<VbenIcon
|
||||
:color="item.color"
|
||||
|
@@ -1,6 +1,7 @@
|
||||
export * from './use-app-config';
|
||||
export * from './use-content-maximize';
|
||||
export * from './use-design-tokens';
|
||||
export * from './use-pagination';
|
||||
export * from './use-refresh';
|
||||
export * from './use-tabs';
|
||||
export * from './use-watermark';
|
||||
|
57
packages/effects/hooks/src/use-pagination.ts
Normal file
57
packages/effects/hooks/src/use-pagination.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Ref } from 'vue';
|
||||
import { computed, ref, unref } from 'vue';
|
||||
|
||||
/**
|
||||
* Paginates an array of items
|
||||
* @param list The array to paginate
|
||||
* @param pageNo The current page number (1-based)
|
||||
* @param pageSize Number of items per page
|
||||
* @returns Paginated array slice
|
||||
* @throws {Error} If pageNo or pageSize are invalid
|
||||
*/
|
||||
function pagination<T = any>(list: T[], pageNo: number, pageSize: number): T[] {
|
||||
if (pageNo < 1) throw new Error('Page number must be positive');
|
||||
if (pageSize < 1) throw new Error('Page size must be positive');
|
||||
|
||||
const offset = (pageNo - 1) * Number(pageSize);
|
||||
const ret =
|
||||
offset + pageSize >= list.length
|
||||
? list.slice(offset)
|
||||
: list.slice(offset, offset + pageSize);
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function usePagination<T = any>(list: Ref<T[]>, pageSize: number) {
|
||||
const currentPage = ref(1);
|
||||
const pageSizeRef = ref(pageSize);
|
||||
|
||||
const totalPages = computed(() =>
|
||||
Math.ceil(unref(list).length / unref(pageSizeRef)),
|
||||
);
|
||||
|
||||
const paginationList = computed(() => {
|
||||
return pagination(unref(list), unref(currentPage), unref(pageSizeRef));
|
||||
});
|
||||
|
||||
const total = computed(() => {
|
||||
return unref(list).length;
|
||||
});
|
||||
|
||||
function setCurrentPage(page: number) {
|
||||
if (page < 1 || page > unref(totalPages)) {
|
||||
throw new Error('Invalid page number');
|
||||
}
|
||||
currentPage.value = page;
|
||||
}
|
||||
|
||||
function setPageSize(pageSize: number) {
|
||||
if (pageSize < 1) {
|
||||
throw new Error('Page size must be positive');
|
||||
}
|
||||
pageSizeRef.value = pageSize;
|
||||
// Reset to first page to prevent invalid state
|
||||
currentPage.value = 1;
|
||||
}
|
||||
|
||||
return { setCurrentPage, total, setPageSize, paginationList };
|
||||
}
|
@@ -37,6 +37,7 @@
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"default-passive-events": "^2.0.0",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import 'default-passive-events';
|
||||
|
||||
export * from './authentication';
|
||||
export * from './basic';
|
||||
export * from './iframe';
|
||||
|
@@ -2,9 +2,7 @@ import type { VxeGridProps, VxeUIExport } from 'vxe-table';
|
||||
|
||||
import type { VxeGridApi } from './api';
|
||||
|
||||
import { isFunction } from '@vben/utils';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { formatDate, formatDateTime, isFunction } from '@vben/utils';
|
||||
|
||||
export function extendProxyOptions(
|
||||
api: VxeGridApi,
|
||||
@@ -54,13 +52,13 @@ function extendProxyOption(
|
||||
export function extendsDefaultFormatter(vxeUI: VxeUIExport) {
|
||||
vxeUI.formats.add('formatDate', {
|
||||
tableCellFormatMethod({ cellValue }) {
|
||||
return dayjs(cellValue).format('YYYY-MM-DD');
|
||||
return formatDate(cellValue);
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.formats.add('formatDateTime', {
|
||||
tableCellFormatMethod({ cellValue }) {
|
||||
return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
|
||||
return formatDateTime(cellValue);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -87,3 +87,11 @@
|
||||
.vxe-table-custom--checkbox-option:hover {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.vxe-toolbar {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.vxe-tools--operate:not(:has(button)) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { DeepPartial } from '@vben/types';
|
||||
import type { ClassType, DeepPartial } from '@vben/types';
|
||||
import type { VbenFormProps } from '@vben-core/form-ui';
|
||||
import type {
|
||||
VxeGridListeners,
|
||||
@@ -30,11 +30,11 @@ export interface VxeGridProps {
|
||||
/**
|
||||
* 组件class
|
||||
*/
|
||||
class?: any;
|
||||
class?: ClassType;
|
||||
/**
|
||||
* vxe-grid class
|
||||
*/
|
||||
gridClass?: any;
|
||||
gridClass?: ClassType;
|
||||
/**
|
||||
* vxe-grid 配置
|
||||
*/
|
||||
|
@@ -139,10 +139,6 @@ const options = computed(() => {
|
||||
mergedOptions.proxyConfig.autoLoad = false;
|
||||
}
|
||||
|
||||
if (!showToolbar.value && mergedOptions.toolbarConfig) {
|
||||
mergedOptions.toolbarConfig.enabled = false;
|
||||
}
|
||||
|
||||
if (mergedOptions.pagerConfig) {
|
||||
const mobileLayouts = [
|
||||
'PrevJump',
|
||||
|
Reference in New Issue
Block a user