This commit is contained in:
dap
2024-11-09 16:45:50 +08:00
58 changed files with 1512 additions and 111 deletions

View File

@@ -31,7 +31,7 @@
#app,
body,
html {
@apply size-full overscroll-none;
@apply size-full;
/* scrollbar-gutter: stable; */
}

View File

@@ -3,19 +3,5 @@ import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: [
{
builder: 'mkdist',
input: './src',
loaders: ['vue'],
pattern: ['**/*.vue'],
},
{
builder: 'mkdist',
format: 'esm',
input: './src',
loaders: ['js'],
pattern: ['**/*.ts'],
},
],
entries: ['src/index'],
});

View File

@@ -1,10 +1,14 @@
export { default as EmptyIcon } from './components/empty.vue';
export * from './create-icon';
export * from './lucide';
export type { IconifyIcon as IconifyIconStructure } from '@iconify/vue';
export { addCollection, addIcon, Icon as IconifyIcon } from '@iconify/vue';
export {
addCollection,
addIcon,
Icon as IconifyIcon,
listIcons,
} from '@iconify/vue';
/**
* 从@iconify/vue/dist/offline'导出的组件为离线ICON 不支持在线

View File

@@ -27,6 +27,7 @@ export {
FoldHorizontal,
Fullscreen,
Github,
Grip,
Info,
InspectionPanel,
Languages,

View File

@@ -84,6 +84,7 @@
"@types/lodash.get": "catalog:",
"@vue/shared": "catalog:",
"clsx": "catalog:",
"dayjs": "^1.11.13",
"defu": "catalog:",
"lodash.clonedeep": "catalog:",
"lodash.get": "catalog:",

View File

@@ -0,0 +1,18 @@
import dayjs from 'dayjs';
export function formatDate(time: number | string, format = 'YYYY-MM-DD') {
try {
const date = dayjs(time);
if (!date.isValid()) {
throw new Error('Invalid date');
}
return date.format(format);
} catch (error) {
console.error(`Error formatting date: ${error}`);
return time;
}
}
export function formatDateTime(time: number | string) {
return formatDate(time, 'YYYY-MM-DD HH:mm:ss');
}

View File

@@ -1,4 +1,5 @@
export * from './cn';
export * from './date';
export * from './diff';
export * from './dom';
export * from './inference';

View File

@@ -34,4 +34,6 @@ interface BasicUserInfo {
username: string;
}
export type { BasicOption, BasicUserInfo, SelectOption, TabOption };
type ClassType = Array<object | string> | object | string;
export type { BasicOption, BasicUserInfo, ClassType, SelectOption, TabOption };

View File

@@ -40,6 +40,7 @@
"@vben-core/composables": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vee-validate/zod": "catalog:",
"@vueuse/core": "catalog:",
"vee-validate": "catalog:",

View File

@@ -3,7 +3,12 @@ import { computed, toRaw, unref, watch } from 'vue';
import { useSimpleLocale } from '@vben-core/composables';
import { VbenExpandableArrow } from '@vben-core/shadcn-ui';
import { cn, isFunction, triggerWindowResize } from '@vben-core/shared/utils';
import {
cn,
formatDate,
isFunction,
triggerWindowResize,
} from '@vben-core/shared/utils';
import { COMPONENT_MAP } from '../config';
import { injectFormProps } from '../use-form-context';
@@ -52,20 +57,64 @@ async function handleSubmit(e: Event) {
if (!valid) {
return;
}
await unref(rootProps).handleSubmit?.(toRaw(form.values));
const values = handleRangeTimeValue(toRaw(form.values));
await unref(rootProps).handleSubmit?.(values);
}
async function handleReset(e: Event) {
e?.preventDefault();
e?.stopPropagation();
const props = unref(rootProps);
const values = toRaw(form.values);
// 清理时间字段
props.fieldMappingTime &&
props.fieldMappingTime.forEach(([_, [startTimeKey, endTimeKey]]) => {
delete values[startTimeKey];
delete values[endTimeKey];
});
if (isFunction(props.handleReset)) {
await props.handleReset?.(form.values);
await props.handleReset?.(values);
} else {
form.resetForm();
}
}
function handleRangeTimeValue(values: Record<string, any>) {
const fieldMappingTime = unref(rootProps).fieldMappingTime;
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
return values;
}
fieldMappingTime.forEach(
([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
if (!values[field]) {
delete values[field];
return;
}
const [startTime, endTime] = values[field];
const [startTimeFormat, endTimeFormat] = Array.isArray(format)
? format
: [format, format];
values[startTimeKey] = startTime
? formatDate(startTime, startTimeFormat)
: undefined;
values[endTimeKey] = endTime
? formatDate(endTime, endTimeFormat)
: undefined;
delete values[field];
},
);
return values;
}
watch(
() => collapsed.value,
() => {

View File

@@ -303,4 +303,13 @@ export class FormApi {
}
return validateResult;
}
async validateAndSubmitForm() {
const form = await this.getForm();
const { valid } = await form.validate();
if (!valid) {
return;
}
return await this.submitForm();
}
}

View File

@@ -1,4 +1,5 @@
import type { VbenButtonProps } from '@vben-core/shadcn-ui';
import type { ClassType } from '@vben-core/typings';
import type { FieldOptions, FormContext, GenericObject } from 'vee-validate';
import type { ZodTypeAny } from 'zod';
@@ -205,6 +206,12 @@ export type HandleResetFn = (
values: Record<string, any>,
) => Promise<void> | void;
export type FieldMappingTime = [
string,
[string, string],
([string, string] | string)?,
][];
export interface FormSchema<
T extends BaseFormComponentType = BaseFormComponentType,
> extends FormCommonConfig {
@@ -303,7 +310,11 @@ export interface VbenFormProps<
/**
* 表单操作区域class
*/
actionWrapperClass?: any;
actionWrapperClass?: ClassType;
/**
* 表单字段映射成时间格式
*/
fieldMappingTime?: FieldMappingTime;
/**
* 表单重置回调
*/

View File

@@ -41,6 +41,7 @@
"@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "catalog:",
"vue": "catalog:"
}

View File

@@ -1,3 +1,5 @@
import type { ClassType } from '@vben-core/typings';
import type { DrawerApi } from './drawer-api';
import type { Component, Ref } from 'vue';
@@ -7,7 +9,7 @@ export interface DrawerProps {
* 取消按钮文字
*/
cancelText?: string;
class?: string;
class?: ClassType;
/**
* 是否显示右上角的关闭按钮
* @default true
@@ -42,6 +44,20 @@ export interface DrawerProps {
* @default true
*/
footer?: boolean;
/**
* 弹窗底部样式
*/
footerClass?: ClassType;
/**
* 是否显示顶栏
* @default true
*/
header?: boolean;
/**
* 弹窗头部样式
*/
headerClass?: ClassType;
/**
* 弹窗是否显示
* @default false

View File

@@ -56,6 +56,9 @@ const {
contentClass,
description,
footer: showFooter,
footerClass,
header: showHeader,
headerClass,
loading: showLoading,
modal,
openAutoFocus,
@@ -129,10 +132,15 @@ function handleFocusOutside(e: Event) {
@pointer-down-outside="pointerDownOutside"
>
<SheetHeader
v-if="showHeader"
:class="
cn('!flex flex-row items-center justify-between border-b px-6 py-5', {
'px-4 py-3': closable,
})
cn(
'!flex flex-row items-center justify-between border-b px-6 py-5',
headerClass,
{
'px-4 py-3': closable,
},
)
"
>
<div>
@@ -171,6 +179,12 @@ function handleFocusOutside(e: Event) {
</div>
</SheetHeader>
<template v-else>
<VisuallyHidden>
<SheetTitle />
<SheetDescription />
</VisuallyHidden>
</template>
<div
ref="wrapperRef"
:class="
@@ -186,7 +200,12 @@ function handleFocusOutside(e: Event) {
<SheetFooter
v-if="showFooter"
class="w-full flex-row items-center justify-end border-t p-2 px-3"
:class="
cn(
'w-full flex-row items-center justify-end border-t p-2 px-3',
footerClass,
)
"
>
<slot name="prepend-footer"></slot>
<slot name="footer">

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import type {
AvatarFallbackProps,
AvatarImageProps,
@@ -11,9 +12,9 @@ import { Avatar, AvatarFallback, AvatarImage } from '../../ui';
interface Props extends AvatarRootProps, AvatarFallbackProps, AvatarImageProps {
alt?: string;
class?: any;
class?: ClassType;
dot?: boolean;
dotClass?: any;
dotClass?: ClassType;
}
defineOptions({

View File

@@ -14,6 +14,7 @@ interface Props extends VbenButtonProps {
disabled?: boolean;
onClick?: () => void;
tooltip?: string;
tooltipDelayDuration?: number;
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
variant?: ButtonVariants;
}
@@ -21,6 +22,7 @@ interface Props extends VbenButtonProps {
const props = withDefaults(defineProps<Props>(), {
disabled: false,
onClick: () => {},
tooltipDelayDuration: 200,
tooltipSide: 'bottom',
variant: 'icon',
});
@@ -42,7 +44,11 @@ const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
<slot></slot>
</VbenButton>
<VbenTooltip v-else :side="tooltipSide">
<VbenTooltip
v-else
:delay-duration="tooltipDelayDuration"
:side="tooltipSide"
>
<template #trigger>
<VbenButton
:class="cn('rounded-full', props.class)"

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import type {
ContextMenuContentProps,
ContextMenuRootEmits,
@@ -22,11 +23,11 @@ import {
const props = defineProps<
{
class?: any;
contentClass?: any;
class?: ClassType;
contentClass?: ClassType;
contentProps?: ContextMenuContentProps;
handlerData?: Record<string, any>;
itemClass?: any;
itemClass?: ClassType;
menus: (data: any) => IContextMenuItem[];
} & ContextMenuRootProps
>();

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import type {
HoverCardContentProps,
HoverCardRootEmits,
@@ -12,8 +13,8 @@ import { useForwardPropsEmits } from 'radix-vue';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '../../ui';
interface Props extends HoverCardRootProps {
class?: any;
contentClass?: any;
class?: ClassType;
contentClass?: ClassType;
contentProps?: HoverCardContentProps;
}

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import type {
PopoverContentProps,
PopoverRootEmits,
@@ -16,8 +17,8 @@ import {
} from '../../ui';
interface Props extends PopoverRootProps {
class?: any;
contentClass?: any;
class?: ClassType;
contentClass?: ClassType;
contentProps?: PopoverContentProps;
}

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
@@ -6,9 +8,9 @@ import { cn } from '@vben-core/shared/utils';
import { ScrollArea, ScrollBar } from '../../ui';
interface Props {
class?: any;
class?: ClassType;
horizontal?: boolean;
scrollBarClass?: any;
scrollBarClass?: ClassType;
shadow?: boolean;
shadowBorder?: boolean;
shadowBottom?: boolean;

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import type { TooltipContentProps } from 'radix-vue';
import type { StyleValue } from 'vue';
@@ -11,7 +12,7 @@ import {
} from '../../ui';
interface Props {
contentClass?: any;
contentClass?: ClassType;
contentStyle?: StyleValue;
delayDuration?: number;
side?: TooltipContentProps['side'];

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { ClassType } from '@vben-core/typings';
import { computed, ref } from 'vue';
import { cn } from '@vben-core/shared/utils';
@@ -18,8 +20,8 @@ import DialogOverlay from './DialogOverlay.vue';
const props = withDefaults(
defineProps<
{
class?: any;
closeClass?: any;
class?: ClassType;
closeClass?: ClassType;
modal?: boolean;
open?: boolean;
showClose?: boolean;

View File

@@ -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:*",

View File

@@ -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 {}

View File

@@ -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>

View File

@@ -0,0 +1 @@
export { default as IconPicker } from './icon-picker.vue';

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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"

View File

@@ -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';

View 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 };
}

View File

@@ -37,6 +37,7 @@
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"default-passive-events": "^2.0.0",
"vue": "catalog:",
"vue-router": "catalog:"
}

View File

@@ -1,3 +1,5 @@
import 'default-passive-events';
export * from './authentication';
export * from './basic';
export * from './iframe';

View File

@@ -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);
},
});
}

View File

@@ -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;
}

View File

@@ -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 配置
*/

View File

@@ -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',

View File

@@ -1,4 +1,5 @@
export * from './iconify/index.js';
export * from './iconify-offline/index.js';
export { default as EmptyIcon } from './icons/empty-icon.vue';
export * from './svg/index.js';
export { VbenIcon } from '@vben-core/shadcn-ui';