更新最新代码

This commit is contained in:
luob
2025-12-24 23:48:38 +08:00
parent e728cf2c5e
commit 1fd17ef73a
1320 changed files with 83513 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "tailwind.config.mjs",
"css": "src/assets/index.css",
"baseColor": "slate",
"cssVariables": true
},
"framework": "vite",
"aliases": {
"components": "@vben-core/shadcn-ui/components",
"utils": "@vben-core/shared/utils"
}
}

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config/postcss';

View File

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

View File

@@ -0,0 +1,38 @@
export const backtopProps = {
/**
* @zh_CN bottom distance.
*/
bottom: {
default: 40,
type: Number,
},
/**
* @zh_CN right distance.
*/
right: {
default: 40,
type: Number,
},
/**
* @zh_CN the target to trigger scroll.
*/
target: {
default: '',
type: String,
},
/**
* @zh_CN the button will not show until the scroll height reaches this value.
*/
visibilityHeight: {
default: 200,
type: Number,
},
} as const;
export interface BacktopProps {
bottom?: number;
isGroup?: boolean;
right?: number;
target?: string;
visibilityHeight?: number;
}

View File

@@ -0,0 +1 @@
export { default as VbenBackTop } from './back-top.vue';

View File

@@ -0,0 +1,45 @@
import type { BacktopProps } from './backtop';
import { onMounted, ref, shallowRef } from 'vue';
import { useEventListener, useThrottleFn } from '@vueuse/core';
export const useBackTop = (props: BacktopProps) => {
const el = shallowRef<HTMLElement>();
const container = shallowRef<Document | HTMLElement>();
const visible = ref(false);
const handleScroll = () => {
if (el.value) {
visible.value = el.value.scrollTop >= (props?.visibilityHeight ?? 0);
}
};
const handleClick = () => {
el.value?.scrollTo({ behavior: 'smooth', top: 0 });
};
const handleScrollThrottled = useThrottleFn(handleScroll, 300, true);
useEventListener(container, 'scroll', handleScrollThrottled);
onMounted(() => {
container.value = document;
el.value = document.documentElement;
if (props.target) {
el.value = document.querySelector<HTMLElement>(props.target) ?? undefined;
if (!el.value) {
throw new Error(`target does not exist: ${props.target}`);
}
container.value = el.value;
}
// Give visible an initial value, fix #13066
handleScroll();
});
return {
handleClick,
visible,
};
};

View File

@@ -0,0 +1,98 @@
<script lang="ts" setup>
import type { BreadcrumbProps } from './types';
import { ChevronDown } from '@vben-core/icons';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '../../ui';
import { VbenIcon } from '../icon';
interface Props extends BreadcrumbProps {}
defineOptions({ name: 'Breadcrumb' });
withDefaults(defineProps<Props>(), {
showIcon: false,
});
const emit = defineEmits<{ select: [string] }>();
function handleClick(path?: string) {
if (!path) {
return;
}
emit('select', path);
}
</script>
<template>
<Breadcrumb>
<BreadcrumbList>
<TransitionGroup name="breadcrumb-transition">
<template
v-for="(item, index) in breadcrumbs"
:key="`${item.path}-${item.title}-${index}`"
>
<BreadcrumbItem>
<div v-if="item.items?.length ?? 0 > 0">
<DropdownMenu>
<DropdownMenuTrigger class="flex items-center gap-1">
<VbenIcon v-if="showIcon" :icon="item.icon" class="size-5" />
{{ item.title }}
<ChevronDown class="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<template
v-for="menuItem in item.items"
:key="`sub-${menuItem.path}`"
>
<DropdownMenuItem @click.stop="handleClick(menuItem.path)">
{{ menuItem.title }}
</DropdownMenuItem>
</template>
</DropdownMenuContent>
</DropdownMenu>
</div>
<BreadcrumbLink
v-else-if="index !== breadcrumbs.length - 1"
href="javascript:void 0"
@click.stop="handleClick(item.path)"
>
<div class="flex-center">
<VbenIcon
v-if="showIcon"
:class="{ 'size-5': item.isHome }"
:icon="item.icon"
class="mr-1 size-4"
/>
{{ item.title }}
</div>
</BreadcrumbLink>
<BreadcrumbPage v-else>
<div class="flex-center">
<VbenIcon
v-if="showIcon"
:class="{ 'size-5': item.isHome }"
:icon="item.icon"
class="mr-1 size-4"
/>
{{ item.title }}
</div>
</BreadcrumbPage>
<BreadcrumbSeparator
v-if="index < breadcrumbs.length - 1 && !item.isHome"
/>
</BreadcrumbItem>
</template>
</TransitionGroup>
</BreadcrumbList>
</Breadcrumb>
</template>

View File

@@ -0,0 +1,3 @@
export { default as VbenBreadcrumbView } from './breadcrumb-view.vue';
export type * from './types';

View File

@@ -0,0 +1,17 @@
import type { Component } from 'vue';
import type { BreadcrumbStyleType } from '@vben-core/typings';
export interface IBreadcrumb {
icon?: Component | string;
isHome?: boolean;
items?: IBreadcrumb[];
path?: string;
title?: string;
}
export interface BreadcrumbProps {
breadcrumbs: IBreadcrumb[];
showIcon?: boolean;
styleType?: BreadcrumbStyleType;
}

View File

@@ -0,0 +1,98 @@
<script lang="ts" setup>
import { cn } from '@vben-core/shared/utils';
defineOptions({ name: 'VbenButtonGroup' });
withDefaults(
defineProps<{
border?: boolean;
gap?: number;
size?: 'large' | 'middle' | 'small';
}>(),
{ border: false, gap: 0, size: 'middle' },
);
</script>
<template>
<div
:class="
cn(
'vben-button-group rounded-md',
`size-${size}`,
gap ? 'with-gap' : 'no-gap',
$attrs.class as string,
)
"
:style="{ gap: gap ? `${gap}px` : '0px' }"
>
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
.vben-button-group {
display: inline-flex;
&.size-large :deep(button) {
height: 2.25rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
line-height: 1.25rem;
.icon-wrapper {
margin-right: 0.4rem;
svg {
width: 1rem;
height: 1rem;
}
}
}
&.size-middle :deep(button) {
height: 2rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
line-height: 1rem;
.icon-wrapper {
margin-right: 0.2rem;
svg {
width: 0.75rem;
height: 0.75rem;
}
}
}
&.size-small :deep(button) {
height: 1.75rem;
padding: 0.2rem 0.4rem;
font-size: 0.65rem;
line-height: 0.75rem;
.icon-wrapper {
margin-right: 0.1rem;
svg {
width: 0.65rem;
height: 0.65rem;
}
}
}
&.no-gap > :deep(button):nth-of-type(1) {
border-radius: calc(var(--radius) - 2px) 0 0 calc(var(--radius) - 2px);
}
&.no-gap > :deep(button):last-of-type {
border-radius: 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0;
}
&.no-gap {
:deep(button + button) {
border-left-width: 0;
border-radius: 0;
}
}
}
</style>

View File

@@ -0,0 +1,196 @@
<script lang="ts" setup>
import type { Arrayable } from '@vueuse/core';
import type { ValueType, VbenButtonGroupProps } from './button';
import { computed, ref, watch } from 'vue';
import { Circle, CircleCheckBig, LoaderCircle } from '@vben-core/icons';
import { cn, isFunction } from '@vben-core/shared/utils';
import { objectOmit } from '@vueuse/core';
import { VbenRenderContent } from '../render-content';
import VbenButtonGroup from './button-group.vue';
import Button from './button.vue';
const props = withDefaults(defineProps<VbenButtonGroupProps>(), {
gap: 0,
multiple: false,
showIcon: true,
size: 'middle',
allowClear: false,
maxCount: 0,
});
const emit = defineEmits(['btnClick']);
const btnDefaultProps = computed(() => {
return {
...objectOmit(props, ['options', 'btnClass', 'size', 'disabled']),
class: cn(props.btnClass),
};
});
const modelValue = defineModel<Arrayable<ValueType> | undefined>();
const innerValue = ref<Array<ValueType>>([]);
const loadingValues = ref<Array<ValueType>>([]);
watch(
() => props.multiple,
(val) => {
if (val) {
modelValue.value = innerValue.value;
} else {
modelValue.value =
innerValue.value.length > 0 ? innerValue.value[0] : undefined;
}
},
);
watch(
() => modelValue.value,
(val) => {
if (Array.isArray(val)) {
const arrVal = val.filter((v) => v !== undefined);
if (arrVal.length > 0) {
innerValue.value = props.multiple
? [...arrVal]
: [arrVal[0] as ValueType];
} else {
innerValue.value = [];
}
} else {
innerValue.value = val === undefined ? [] : [val as ValueType];
}
},
{ deep: true, immediate: true },
);
async function onBtnClick(value: ValueType) {
if (props.beforeChange && isFunction(props.beforeChange)) {
try {
loadingValues.value.push(value);
const canChange = await props.beforeChange(
value,
!innerValue.value.includes(value),
);
if (canChange === false) {
return;
}
} finally {
loadingValues.value.splice(loadingValues.value.indexOf(value), 1);
}
}
if (props.multiple) {
if (innerValue.value.includes(value)) {
innerValue.value = innerValue.value.filter((item) => item !== value);
} else {
if (props.maxCount > 0 && innerValue.value.length >= props.maxCount) {
innerValue.value = innerValue.value.slice(0, props.maxCount - 1);
}
innerValue.value.push(value);
}
modelValue.value = innerValue.value;
} else {
if (props.allowClear && innerValue.value.includes(value)) {
innerValue.value = [];
modelValue.value = undefined;
emit('btnClick', undefined);
return;
} else {
innerValue.value = [value];
modelValue.value = value;
}
}
emit('btnClick', value);
}
</script>
<template>
<VbenButtonGroup
:size="props.size"
:gap="props.gap"
class="vben-check-button-group"
>
<Button
v-for="(btn, index) in props.options"
:key="index"
:class="cn('border', props.btnClass)"
:disabled="
props.disabled ||
loadingValues.includes(btn.value) ||
(!props.multiple && loadingValues.length > 0)
"
v-bind="btnDefaultProps"
:variant="innerValue.includes(btn.value) ? 'default' : 'outline'"
@click="onBtnClick(btn.value)"
type="button"
>
<div class="icon-wrapper" v-if="props.showIcon">
<slot
name="icon"
:loading="loadingValues.includes(btn.value)"
:checked="innerValue.includes(btn.value)"
>
<LoaderCircle
class="animate-spin"
v-if="loadingValues.includes(btn.value)"
/>
<CircleCheckBig v-else-if="innerValue.includes(btn.value)" />
<Circle v-else />
</slot>
</div>
<slot name="option" :label="btn.label" :value="btn.value" :data="btn">
<VbenRenderContent :content="btn.label" />
</slot>
</Button>
</VbenButtonGroup>
</template>
<style lang="scss" scoped>
.vben-check-button-group {
display: flex;
flex-wrap: wrap;
&:deep(.size-large) button {
.icon-wrapper {
margin-right: 0.3rem;
svg {
width: 1rem;
height: 1rem;
}
}
}
&:deep(.size-middle) button {
.icon-wrapper {
margin-right: 0.2rem;
svg {
width: 0.75rem;
height: 0.75rem;
}
}
}
&:deep(.size-small) button {
.icon-wrapper {
margin-right: 0.1rem;
svg {
width: 0.65rem;
height: 0.65rem;
}
}
}
&.no-gap > :deep(button):nth-of-type(1) {
border-right-width: 0;
}
&.no-gap {
:deep(button + button) {
margin-right: -1px;
border-left-width: 1px;
}
}
}
</style>

View File

@@ -0,0 +1,68 @@
<script setup lang="ts">
import type { ButtonVariants } from '../../ui';
import type { VbenButtonProps } from './button';
import { computed, useSlots } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { VbenTooltip } from '../tooltip';
import VbenButton from './button.vue';
interface Props extends VbenButtonProps {
class?: any;
disabled?: boolean;
onClick?: () => void;
tooltip?: string;
tooltipDelayDuration?: number;
tooltipSide?: 'bottom' | 'left' | 'right' | 'top';
variant?: ButtonVariants;
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
onClick: () => {},
tooltipDelayDuration: 200,
tooltipSide: 'bottom',
variant: 'icon',
});
const slots = useSlots();
const showTooltip = computed(() => !!slots.tooltip || !!props.tooltip);
</script>
<template>
<VbenButton
v-if="!showTooltip"
:class="cn('rounded-full', props.class)"
:disabled="disabled"
:variant="variant"
size="icon"
@click="onClick"
>
<slot></slot>
</VbenButton>
<VbenTooltip
v-else
:delay-duration="tooltipDelayDuration"
:side="tooltipSide"
>
<template #trigger>
<VbenButton
:class="cn('rounded-full', props.class)"
:disabled="disabled"
:variant="variant"
size="icon"
@click="onClick"
>
<slot></slot>
</VbenButton>
</template>
<slot v-if="slots.tooltip" name="tooltip"> </slot>
<template v-else>
{{ tooltip }}
</template>
</VbenTooltip>
</template>

View File

@@ -0,0 +1,5 @@
export type * from './button';
export { default as VbenButtonGroup } from './button-group.vue';
export { default as VbenButton } from './button.vue';
export { default as VbenCheckButtonGroup } from './check-button-group.vue';
export { default as VbenIconButton } from './icon-button.vue';

View File

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

View File

@@ -0,0 +1,3 @@
export { default as VbenContextMenu } from './context-menu.vue';
export type * from './interface';

View File

@@ -0,0 +1,38 @@
import type { Component } from 'vue';
interface IContextMenuItem {
/**
* @zh_CN 是否禁用
*/
disabled?: boolean;
/**
* @zh_CN 点击事件处理
* @param data
*/
handler?: (data: any) => void;
/**
* @zh_CN 图标
*/
icon?: Component;
/**
* @zh_CN 是否显示图标
*/
inset?: boolean;
/**
* @zh_CN 唯一标识
*/
key: string;
/**
* @zh_CN 是否是分割线
*/
separator?: boolean;
/**
* @zh_CN 快捷键
*/
shortcut?: string;
/**
* @zh_CN 标题
*/
text: string;
}
export type { IContextMenuItem };

View File

@@ -0,0 +1,128 @@
<script lang="ts" setup>
import { computed, onMounted, ref, unref, watch, watchEffect } from 'vue';
import { isNumber } from '@vben-core/shared/utils';
import { TransitionPresets, useTransition } from '@vueuse/core';
interface Props {
autoplay?: boolean;
color?: string;
decimal?: string;
decimals?: number;
duration?: number;
endVal?: number;
prefix?: string;
separator?: string;
startVal?: number;
suffix?: string;
transition?: keyof typeof TransitionPresets;
useEasing?: boolean;
}
defineOptions({ name: 'CountToAnimator' });
const props = withDefaults(defineProps<Props>(), {
autoplay: true,
color: '',
decimal: '.',
decimals: 0,
duration: 1500,
endVal: 2021,
prefix: '',
separator: ',',
startVal: 0,
suffix: '',
transition: 'linear',
useEasing: true,
});
const emit = defineEmits<{
finished: [];
/**
* @deprecated 请使用{@link finished}事件
*/
onFinished: [];
/**
* @deprecated 请使用{@link started}事件
*/
onStarted: [];
started: [];
}>();
const source = ref(props.startVal);
const disabled = ref(false);
let outputValue = useTransition(source);
const value = computed(() => formatNumber(unref(outputValue)));
watchEffect(() => {
source.value = props.startVal;
});
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
}
});
onMounted(() => {
props.autoplay && start();
});
function start() {
run();
source.value = props.endVal;
}
function reset() {
source.value = props.startVal;
run();
}
function run() {
outputValue = useTransition(source, {
disabled,
duration: props.duration,
onFinished: () => {
emit('finished');
emit('onFinished');
},
onStarted: () => {
emit('started');
emit('onStarted');
},
...(props.useEasing
? { transition: TransitionPresets[props.transition] }
: {}),
});
}
function formatNumber(num: number | string) {
if (!num && num !== 0) {
return '';
}
const { decimal, decimals, prefix, separator, suffix } = props;
num = Number(num).toFixed(decimals);
num += '';
const x = num.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator) && x1) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, `$1${separator}$2`);
}
}
return prefix + x1 + x2 + suffix;
}
defineExpose({ reset });
</script>
<template>
<span :style="{ color }">
{{ value }}
</span>
</template>

View File

@@ -0,0 +1 @@
export { default as VbenCountToAnimator } from './count-to-animator.vue';

View File

@@ -0,0 +1,4 @@
export { default as VbenDropdownMenu } from './dropdown-menu.vue';
export { default as VbenDropdownRadioMenu } from './dropdown-radio-menu.vue';
export type * from './interface';

View File

@@ -0,0 +1,32 @@
import type { Component } from 'vue';
interface VbenDropdownMenuItem {
disabled?: boolean;
/**
* @zh_CN 点击事件处理
* @param data
*/
handler?: (data: any) => void;
/**
* @zh_CN 图标
*/
icon?: Component;
/**
* @zh_CN 标题
*/
label: string;
/**
* @zh_CN 是否是分割线
*/
separator?: boolean;
/**
* @zh_CN 唯一标识
*/
value: string;
}
interface DropdownMenuProps {
menus: VbenDropdownMenuItem[];
}
export type { DropdownMenuProps, VbenDropdownMenuItem };

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import { ChevronDown } from '@vben-core/icons';
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: string;
}>();
// 控制箭头展开/收起状态
const collapsed = defineModel({ default: false });
</script>
<template>
<div
:class="cn('vben-link inline-flex items-center', props.class)"
@click="collapsed = !collapsed"
>
<slot :is-expanded="collapsed">
{{ collapsed }}
<!-- <span>{{ isExpanded ? '收起' : '展开' }}</span> -->
</slot>
<div
:class="{ 'rotate-180': !collapsed }"
class="transition-transform duration-300"
>
<slot name="icon">
<ChevronDown class="size-4" />
</slot>
</div>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as VbenExpandableArrow } from './expandable-arrow.vue';

View File

@@ -0,0 +1 @@
export { default as VbenFullScreen } from './full-screen.vue';

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import type { Component } from 'vue';
import { computed } from 'vue';
import { IconDefault, IconifyIcon } from '@vben-core/icons';
import {
isFunction,
isHttpUrl,
isObject,
isString,
} from '@vben-core/shared/utils';
const props = defineProps<{
// 没有是否显示默认图标
fallback?: boolean;
icon?: Component | Function | string;
}>();
const isRemoteIcon = computed(() => {
return isString(props.icon) && isHttpUrl(props.icon);
});
const isComponent = computed(() => {
const { icon } = props;
return !isString(icon) && (isObject(icon) || isFunction(icon));
});
</script>
<template>
<component :is="icon as Component" v-if="isComponent" v-bind="$attrs" />
<img v-else-if="isRemoteIcon" :src="icon as string" v-bind="$attrs" />
<IconifyIcon v-else-if="icon" v-bind="$attrs" :icon="icon as string" />
<IconDefault v-else-if="fallback" v-bind="$attrs" />
</template>

View File

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

View File

@@ -0,0 +1,23 @@
export * from './avatar';
export * from './back-top';
export * from './breadcrumb';
export * from './button';
export * from './checkbox';
export * from './context-menu';
export * from './count-to-animator';
export * from './dropdown-menu';
export * from './expandable-arrow';
export * from './full-screen';
export * from './hover-card';
export * from './icon';
export * from './input-password';
export * from './logo';
export * from './pin-input';
export * from './popover';
export * from './render-content';
export * from './scrollbar';
export * from './segmented';
export * from './select';
export * from './spine-text';
export * from './spinner';
export * from './tooltip';

View File

@@ -0,0 +1 @@
export { default as VbenInputPassword } from './input-password.vue';

View File

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

View File

@@ -0,0 +1,3 @@
export { default as VbenPinInput } from './input.vue';
export type * from './types';

View File

@@ -0,0 +1,30 @@
interface PinInputProps {
class?: any;
/**
* 验证码长度
*/
codeLength?: number;
/**
* 发送验证码按钮文本
*/
createText?: (countdown: number) => string;
/**
* 是否禁用
*/
disabled?: boolean;
/**
* 自定义验证码发送逻辑
* @returns
*/
handleSendCode?: () => Promise<void>;
/**
* 发送验证码按钮loading
*/
loading?: boolean;
/**
* 最大重试时间
*/
maxTime?: number;
}
export type { PinInputProps };

View File

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

View File

@@ -0,0 +1 @@
export { default as VbenRenderContent } from './render-content.vue';

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import type { Component, PropType } from 'vue';
import { defineComponent, h } from 'vue';
import { isFunction, isObject, isString } from '@vben-core/shared/utils';
export default defineComponent({
name: 'RenderContent',
props: {
content: {
default: undefined as
| PropType<(() => any) | Component | string>
| undefined,
type: [Object, String, Function],
},
renderBr: {
default: false,
type: Boolean,
},
},
setup(props, { attrs, slots }) {
return () => {
if (!props.content) {
return null;
}
const isComponent =
(isObject(props.content) || isFunction(props.content)) &&
props.content !== null;
if (!isComponent) {
if (props.renderBr && isString(props.content)) {
const lines = props.content.split('\n');
const result = [];
for (const [i, line] of lines.entries()) {
result.push(h('p', { key: i }, line));
// if (i < lines.length - 1) {
// result.push(h('br'));
// }
}
return result;
} else {
return props.content;
}
}
return h(props.content as never, {
...attrs,
props: {
...props,
...attrs,
},
slots,
});
};
},
});
</script>

View File

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

View File

@@ -0,0 +1,3 @@
export { default as VbenSegmented } from './segmented.vue';
export type * from './types';

View File

@@ -0,0 +1,6 @@
interface SegmentedItem {
label: string;
value: string;
}
export type { SegmentedItem };

View File

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

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import { CircleX } from '@vben-core/icons';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../../ui';
interface Props {
allowClear?: boolean;
class?: any;
options?: Array<{ label: string; value: string }>;
placeholder?: string;
}
const props = withDefaults(defineProps<Props>(), {
allowClear: false,
});
const modelValue = defineModel<string>();
function handleClear() {
modelValue.value = undefined;
}
</script>
<template>
<Select v-model="modelValue">
<SelectTrigger :class="props.class" class="flex w-full items-center">
<SelectValue class="flex-auto text-left" :placeholder="placeholder" />
<CircleX
@pointerdown.stop
@click.stop.prevent="handleClear"
v-if="allowClear && modelValue"
data-clear-button
class="mr-1 size-4 cursor-pointer opacity-50 hover:opacity-100"
/>
</SelectTrigger>
<SelectContent>
<template v-for="item in options" :key="item.value">
<SelectItem :value="item.value"> {{ item.label }} </SelectItem>
</template>
</SelectContent>
</Select>
</template>
<style lang="scss" scoped>
button[role='combobox'][data-placeholder] {
color: hsl(var(--muted-foreground));
}
button {
--ring: var(--primary);
}
</style>

View File

@@ -0,0 +1 @@
export { default as VbenSpineText } from './spine-text.vue';

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { computed } from 'vue';
const { animationDuration = 2, animationIterationCount = 'infinite' } =
defineProps<{
// 动画持续时间,单位秒
animationDuration?: number;
// 动画是否只执行一次
animationIterationCount?: 'infinite' | number;
}>();
const style = computed(() => {
return {
animation: `shine ${animationDuration}s linear ${animationIterationCount}`,
};
});
</script>
<template>
<div :style="style" class="vben-spine-text !bg-clip-text text-transparent">
<slot></slot>
</div>
</template>
<style>
.vben-spine-text {
background:
radial-gradient(circle at center, rgb(255 255 255 / 80%), #f000) -200% 50% /
200% 100% no-repeat,
#000;
/* animation: shine 3s linear infinite; */
}
.dark .vben-spine-text {
background:
radial-gradient(circle at center, rgb(24 24 26 / 80%), transparent) -200%
50% / 200% 100% no-repeat,
#f4f4f4;
}
@keyframes shine {
0% {
background-position: 200% 0%;
}
100% {
background-position: -200% 0%;
}
}
</style>

View File

@@ -0,0 +1,2 @@
export { default as VbenLoading } from './loading.vue';
export { default as VbenSpinner } from './spinner.vue';

View File

@@ -0,0 +1,2 @@
export { default as VbenHelpTooltip } from './help-tooltip.vue';
export { default as VbenTooltip } from './tooltip.vue';

View File

@@ -0,0 +1,4 @@
export { default as Accordion } from './Accordion.vue';
export { default as AccordionContent } from './AccordionContent.vue';
export { default as AccordionItem } from './AccordionItem.vue';
export { default as AccordionTrigger } from './AccordionTrigger.vue';

View File

@@ -0,0 +1,6 @@
export { default as AlertDialog } from './AlertDialog.vue';
export { default as AlertDialogAction } from './AlertDialogAction.vue';
export { default as AlertDialogCancel } from './AlertDialogCancel.vue';
export { default as AlertDialogContent } from './AlertDialogContent.vue';
export { default as AlertDialogDescription } from './AlertDialogDescription.vue';
export { default as AlertDialogTitle } from './AlertDialogTitle.vue';

View File

@@ -0,0 +1,22 @@
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
export const avatarVariant = cva(
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
{
variants: {
shape: {
circle: 'rounded-full',
square: 'rounded-md',
},
size: {
base: 'h-16 w-16 text-2xl',
lg: 'h-32 w-32 text-5xl',
sm: 'h-10 w-10 text-xs',
},
},
},
);
export type AvatarVariants = VariantProps<typeof avatarVariant>;

View File

@@ -0,0 +1,4 @@
export * from './avatar';
export { default as Avatar } from './Avatar.vue';
export { default as AvatarFallback } from './AvatarFallback.vue';
export { default as AvatarImage } from './AvatarImage.vue';

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { BadgeVariants } from './badge';
import { cn } from '@vben-core/shared/utils';
import { badgeVariants } from './badge';
const props = defineProps<{
class?: any;
variant?: BadgeVariants['variant'];
}>();
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,25 @@
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
export const badgeVariants = cva(
'inline-flex items-center rounded-md border border-border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
defaultVariants: {
variant: 'default',
},
variants: {
variant: {
default:
'border-transparent bg-accent hover:bg-accent text-primary-foreground shadow',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive-hover',
outline: 'text-foreground',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
},
},
},
);
export type BadgeVariants = VariantProps<typeof badgeVariants>;

View File

@@ -0,0 +1,3 @@
export * from './badge';
export { default as Badge } from './Badge.vue';

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<nav :class="props.class" aria-label="breadcrumb" role="navigation">
<slot></slot>
</nav>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import { cn } from '@vben-core/shared/utils';
import { MoreHorizontal } from 'lucide-vue-next';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<span
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
aria-hidden="true"
role="presentation"
>
<slot>
<MoreHorizontal class="h-4 w-4" />
</slot>
<span class="sr-only">More</span>
</span>
</template>

View File

@@ -0,0 +1,21 @@
<script lang="ts" setup>
import { cn } from '@vben-core/shared/utils';
import { ChevronRight } from 'lucide-vue-next';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<li
:class="cn('[&>svg]:size-3.5', props.class)"
aria-hidden="true"
role="presentation"
>
<slot>
<ChevronRight />
</slot>
</li>
</template>

View File

@@ -0,0 +1,7 @@
export { default as Breadcrumb } from './Breadcrumb.vue';
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue';
export { default as BreadcrumbItem } from './BreadcrumbItem.vue';
export { default as BreadcrumbLink } from './BreadcrumbLink.vue';
export { default as BreadcrumbList } from './BreadcrumbList.vue';
export { default as BreadcrumbPage } from './BreadcrumbPage.vue';
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue';

View File

@@ -0,0 +1,34 @@
import { cva } from 'class-variance-authority';
export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
{
defaultVariants: {
size: 'default',
variant: 'default',
},
variants: {
size: {
default: 'h-9 px-4 py-2',
icon: 'h-8 w-8 rounded-sm px-1 text-lg',
lg: 'h-10 rounded-md px-4',
sm: 'h-8 rounded-md px-2 text-xs',
xs: 'h-8 w-8 rounded-sm px-1 text-xs',
},
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive-hover',
ghost: 'hover:bg-accent hover:text-accent-foreground',
heavy: 'hover:bg-heavy hover:text-heavy-foreground',
icon: 'hover:bg-accent hover:text-accent-foreground text-foreground/80',
link: 'text-primary underline-offset-4 hover:underline',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
},
},
},
);

View File

@@ -0,0 +1,5 @@
export * from './button';
export { default as Button } from './Button.vue';
export type * from './types';

View File

@@ -0,0 +1,20 @@
export type ButtonVariantSize =
| 'default'
| 'icon'
| 'lg'
| 'sm'
| 'xs'
| null
| undefined;
export type ButtonVariants =
| 'default'
| 'destructive'
| 'ghost'
| 'heavy'
| 'icon'
| 'link'
| 'outline'
| 'secondary'
| null
| undefined;

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-5', props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<h3 :class="cn('font-semibold leading-none tracking-tight', props.class)">
<slot></slot>
</h3>
</template>

View File

@@ -0,0 +1,6 @@
export { default as Card } from './Card.vue';
export { default as CardContent } from './CardContent.vue';
export { default as CardDescription } from './CardDescription.vue';
export { default as CardFooter } from './CardFooter.vue';
export { default as CardHeader } from './CardHeader.vue';
export { default as CardTitle } from './CardTitle.vue';

View File

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

View File

@@ -0,0 +1,14 @@
export { default as ContextMenu } from './ContextMenu.vue';
export { default as ContextMenuCheckboxItem } from './ContextMenuCheckboxItem.vue';
export { default as ContextMenuContent } from './ContextMenuContent.vue';
export { default as ContextMenuGroup } from './ContextMenuGroup.vue';
export { default as ContextMenuItem } from './ContextMenuItem.vue';
export { default as ContextMenuLabel } from './ContextMenuLabel.vue';
export { default as ContextMenuRadioGroup } from './ContextMenuRadioGroup.vue';
export { default as ContextMenuRadioItem } from './ContextMenuRadioItem.vue';
export { default as ContextMenuSeparator } from './ContextMenuSeparator.vue';
export { default as ContextMenuShortcut } from './ContextMenuShortcut.vue';
export { default as ContextMenuSub } from './ContextMenuSub.vue';
export { default as ContextMenuSubContent } from './ContextMenuSubContent.vue';
export { default as ContextMenuSubTrigger } from './ContextMenuSubTrigger.vue';
export { default as ContextMenuTrigger } from './ContextMenuTrigger.vue';

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{ class?: any }>();
</script>
<template>
<div
:class="
cn('flex flex-row flex-col-reverse justify-end gap-x-2', props.class)
"
>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<div
:class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)"
>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,9 @@
export { default as Dialog } from './Dialog.vue';
export { default as DialogClose } from './DialogClose.vue';
export { default as DialogContent } from './DialogContent.vue';
export { default as DialogDescription } from './DialogDescription.vue';
export { default as DialogFooter } from './DialogFooter.vue';
export { default as DialogHeader } from './DialogHeader.vue';
export { default as DialogScrollContent } from './DialogScrollContent.vue';
export { default as DialogTitle } from './DialogTitle.vue';
export { default as DialogTrigger } from './DialogTrigger.vue';

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest opacity-60', props.class)">
<slot></slot>
</span>
</template>

View File

@@ -0,0 +1,20 @@
<script lang="ts" setup>
import { provide, useId } from 'vue';
import { cn } from '@vben-core/shared/utils';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
const props = defineProps<{
class?: any;
}>();
const id = useId() as string;
provide(FORM_ITEM_INJECTION_KEY, id);
</script>
<template>
<div :class="cn(props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,11 @@
export { default as FormControl } from './FormControl.vue';
export { default as FormDescription } from './FormDescription.vue';
export { default as FormItem } from './FormItem.vue';
export { default as FormLabel } from './FormLabel.vue';
export { default as FormMessage } from './FormMessage.vue';
export { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
export {
Form,
Field as FormField,
FieldArray as FormFieldArray,
} from 'vee-validate';

View File

@@ -0,0 +1,4 @@
import type { InjectionKey } from 'vue';
// eslint-disable-next-line symbol-description
export const FORM_ITEM_INJECTION_KEY = Symbol() as InjectionKey<string>;

View File

@@ -0,0 +1,38 @@
import { inject } from 'vue';
import {
FieldContextKey,
useFieldError,
useIsFieldDirty,
useIsFieldTouched,
useIsFieldValid,
} from 'vee-validate';
import { FORM_ITEM_INJECTION_KEY } from './injectionKeys';
export function useFormField() {
const fieldContext = inject(FieldContextKey);
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY);
if (!fieldContext)
throw new Error('useFormField should be used within <FormField>');
const { name } = fieldContext;
const id = fieldItemContext;
const fieldState = {
error: useFieldError(name),
isDirty: useIsFieldDirty(name),
isTouched: useIsFieldTouched(name),
valid: useIsFieldValid(name),
};
return {
formDescriptionId: `${id}-form-item-description`,
formItemId: `${id}-form-item`,
formMessageId: `${id}-form-item-message`,
id,
name,
...fieldState,
};
}

View File

@@ -0,0 +1,3 @@
export { default as HoverCard } from './HoverCard.vue';
export { default as HoverCardContent } from './HoverCardContent.vue';
export { default as HoverCardTrigger } from './HoverCardTrigger.vue';

View File

@@ -0,0 +1,31 @@
export * from './accordion';
export * from './alert-dialog';
export * from './avatar';
export * from './badge';
export * from './breadcrumb';
export * from './button';
export * from './card';
export * from './checkbox';
export * from './dialog';
export * from './dropdown-menu';
export * from './form';
export * from './hover-card';
export * from './input';
export * from './label';
export * from './number-field';
export * from './pagination';
export * from './pin-input';
export * from './popover';
export * from './radio-group';
export * from './resizable';
export * from './scroll-area';
export * from './select';
export * from './separator';
export * from './sheet';
export * from './switch';
export * from './tabs';
export * from './textarea';
export * from './toggle';
export * from './toggle-group';
export * from './tooltip';
export * from './tree';

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{
class?: any;
}>();
</script>
<template>
<div
:class="
cn(
'relative [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5 [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5',
props.class,
)
"
>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,5 @@
export { default as NumberField } from './NumberField.vue';
export { default as NumberFieldContent } from './NumberFieldContent.vue';
export { default as NumberFieldDecrement } from './NumberFieldDecrement.vue';
export { default as NumberFieldIncrement } from './NumberFieldIncrement.vue';
export { default as NumberFieldInput } from './NumberFieldInput.vue';

View File

@@ -0,0 +1,4 @@
export { default as PinInput } from './PinInput.vue';
export { default as PinInputGroup } from './PinInputGroup.vue';
export { default as PinInputInput } from './PinInputInput.vue';
export { default as PinInputSeparator } from './PinInputSeparator.vue';

View File

@@ -0,0 +1,2 @@
export { default as RadioGroup } from './RadioGroup.vue';
export { default as RadioGroupItem } from './RadioGroupItem.vue';

View File

@@ -0,0 +1,2 @@
export { default as ScrollArea } from './ScrollArea.vue';
export { default as ScrollBar } from './ScrollBar.vue';

View File

@@ -0,0 +1,11 @@
export { default as Select } from './Select.vue';
export { default as SelectContent } from './SelectContent.vue';
export { default as SelectGroup } from './SelectGroup.vue';
export { default as SelectItem } from './SelectItem.vue';
export { default as SelectItemText } from './SelectItemText.vue';
export { default as SelectLabel } from './SelectLabel.vue';
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue';
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue';
export { default as SelectSeparator } from './SelectSeparator.vue';
export { default as SelectTrigger } from './SelectTrigger.vue';
export { default as SelectValue } from './SelectValue.vue';

View File

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

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{ class?: any }>();
</script>
<template>
<div
:class="
cn('flex flex-row flex-col-reverse justify-end gap-x-2', props.class)
"
>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { cn } from '@vben-core/shared/utils';
const props = defineProps<{ class?: any }>();
</script>
<template>
<div :class="cn('flex flex-col text-center sm:text-left', props.class)">
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,10 @@
export * from './sheet';
export { default as Sheet } from './Sheet.vue';
export { default as SheetClose } from './SheetClose.vue';
export { default as SheetContent } from './SheetContent.vue';
export { default as SheetDescription } from './SheetDescription.vue';
export { default as SheetFooter } from './SheetFooter.vue';
export { default as SheetHeader } from './SheetHeader.vue';
export { default as SheetTitle } from './SheetTitle.vue';
export { default as SheetTrigger } from './SheetTrigger.vue';

View File

@@ -0,0 +1,24 @@
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
export const sheetVariants = cva(
'bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
{
defaultVariants: {
side: 'right',
},
variants: {
side: {
bottom:
'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
right:
'inset-y-0 right-0 w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
},
},
},
);
export type SheetVariants = VariantProps<typeof sheetVariants>;

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
export { default as ToggleGroup } from './ToggleGroup.vue';
export { default as ToggleGroupItem } from './ToggleGroupItem.vue';

View File

@@ -0,0 +1,2 @@
export * from './toggle';
export { default as Toggle } from './Toggle.vue';

View File

@@ -0,0 +1,27 @@
import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
export const toggleVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
{
defaultVariants: {
size: 'default',
variant: 'default',
},
variants: {
size: {
default: 'h-9 px-3',
lg: 'h-10 px-3',
sm: 'h-8 px-2',
},
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
},
},
},
);
export type ToggleVariants = VariantProps<typeof toggleVariants>;

View File

@@ -0,0 +1,4 @@
export { default as Tooltip } from './Tooltip.vue';
export { default as TooltipContent } from './TooltipContent.vue';
export { default as TooltipProvider } from './TooltipProvider.vue';
export { default as TooltipTrigger } from './TooltipTrigger.vue';

View File

@@ -0,0 +1 @@
export { default } from '@vben/tailwind-config';

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@vben-core/shadcn-ui/*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
}